diff --git a/.gitignore b/.gitignore index aa8061f0c..ea50878c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,258 +1,258 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ *.pyc \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5d7c2ee27..0feff59ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,15 @@ -language: csharp -solution: osu-framework.sln -mono: - - latest -before_install: - - git submodule update --init --recursive -install: - - nuget restore osu-framework.sln - - nuget install NUnit.Runners -Version 3.4.1 -OutputDirectory testrunner -script: - - xbuild osu-framework.sln - - | - mono \ - ./testrunner/NUnit.ConsoleRunner.3.4.1/tools/nunit3-console.exe \ - ./osu.Framework.Tests/osu.Framework.Tests.csproj +language: csharp +solution: osu-framework.sln +mono: + - latest +before_install: + - git submodule update --init --recursive +install: + - nuget restore osu-framework.sln + - nuget install NUnit.Runners -Version 3.4.1 -OutputDirectory testrunner +script: + - xbuild osu-framework.sln + - | + mono \ + ./testrunner/NUnit.ConsoleRunner.3.4.1/tools/nunit3-console.exe \ + ./osu.Framework.Tests/osu.Framework.Tests.csproj diff --git a/SampleGame/Program.cs b/SampleGame/Program.cs index 4752b6ea5..8f2ff05e2 100644 --- a/SampleGame/Program.cs +++ b/SampleGame/Program.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Platform; -using osu.Framework; - -namespace SampleGame -{ - public static class Program - { - [STAThread] - public static void Main() - { - using (Game game = new SampleGame()) - using (GameHost host = Host.GetSuitableHost(@"sample-game")) - host.Run(game); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Platform; +using osu.Framework; + +namespace SampleGame +{ + public static class Program + { + [STAThread] + public static void Main() + { + using (Game game = new SampleGame()) + using (GameHost host = Host.GetSuitableHost(@"sample-game")) + host.Run(game); + } + } +} diff --git a/SampleGame/SampleGame.cs b/SampleGame/SampleGame.cs index 308e83be3..fa938588e 100644 --- a/SampleGame/SampleGame.cs +++ b/SampleGame/SampleGame.cs @@ -1,35 +1,35 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework; -using osu.Framework.Graphics; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Allocation; - -namespace SampleGame -{ - internal class SampleGame : Game - { - private Box box; - - [BackgroundDependencyLoader] - private void load() - { - Add(box = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(150, 150), - Colour = Color4.Tomato - }); - } - - protected override void Update() - { - base.Update(); - box.Rotation += (float)Time.Elapsed / 10; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework; +using osu.Framework.Graphics; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Allocation; + +namespace SampleGame +{ + internal class SampleGame : Game + { + private Box box; + + [BackgroundDependencyLoader] + private void load() + { + Add(box = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(150, 150), + Colour = Color4.Tomato + }); + } + + protected override void Update() + { + base.Update(); + box.Rotation += (float)Time.Elapsed / 10; + } + } +} diff --git a/osu-framework.sln b/osu-framework.sln index 155e3f9d7..dab62f93f 100644 --- a/osu-framework.sln +++ b/osu-framework.sln @@ -1,63 +1,63 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework", "osu.Framework\osu.Framework.csproj", "{C76BF5B3-985E-4D39-95FE-97C9C879B83A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGame", "SampleGame\SampleGame.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework.Tests", "osu.Framework.Tests\osu.Framework.Tests.csproj", "{79803407-6F50-484F-93F5-641911EABD8A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.Build.0 = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU - {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.TextStylePolicy = $1 - $1.EolMarker = Windows - $1.inheritsSet = VisualStudio - $1.inheritsScope = text/plain - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.IndentSwitchSection = True - $2.NewLinesForBracesInProperties = True - $2.NewLinesForBracesInAccessors = True - $2.NewLinesForBracesInAnonymousMethods = True - $2.NewLinesForBracesInControlBlocks = True - $2.NewLinesForBracesInAnonymousTypes = True - $2.NewLinesForBracesInObjectCollectionArrayInitializers = True - $2.NewLinesForBracesInLambdaExpressionBody = True - $2.NewLineForElse = True - $2.NewLineForCatch = True - $2.NewLineForFinally = True - $2.NewLineForMembersInObjectInit = True - $2.NewLineForMembersInAnonymousTypes = True - $2.NewLineForClausesInQuery = True - $2.SpacingAfterMethodDeclarationName = False - $2.SpaceAfterMethodCallName = False - $2.SpaceBeforeOpenSquareBracket = False - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2002 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework", "osu.Framework\osu.Framework.csproj", "{C76BF5B3-985E-4D39-95FE-97C9C879B83A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleGame", "SampleGame\SampleGame.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework.Tests", "osu.Framework.Tests\osu.Framework.Tests.csproj", "{79803407-6F50-484F-93F5-641911EABD8A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.Build.0 = Release|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU + {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79803407-6F50-484F-93F5-641911EABD8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79803407-6F50-484F-93F5-641911EABD8A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.EolMarker = Windows + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchSection = True + $2.NewLinesForBracesInProperties = True + $2.NewLinesForBracesInAccessors = True + $2.NewLinesForBracesInAnonymousMethods = True + $2.NewLinesForBracesInControlBlocks = True + $2.NewLinesForBracesInAnonymousTypes = True + $2.NewLinesForBracesInObjectCollectionArrayInitializers = True + $2.NewLinesForBracesInLambdaExpressionBody = True + $2.NewLineForElse = True + $2.NewLineForCatch = True + $2.NewLineForFinally = True + $2.NewLineForMembersInObjectInit = True + $2.NewLineForMembersInAnonymousTypes = True + $2.NewLineForClausesInQuery = True + $2.SpacingAfterMethodDeclarationName = False + $2.SpaceAfterMethodCallName = False + $2.SpaceBeforeOpenSquareBracket = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + EndGlobalSection +EndGlobal diff --git a/osu.Framework.Tests/AutomatedVisualTestGame.cs b/osu.Framework.Tests/AutomatedVisualTestGame.cs index bd224cbc2..2970987bd 100644 --- a/osu.Framework.Tests/AutomatedVisualTestGame.cs +++ b/osu.Framework.Tests/AutomatedVisualTestGame.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Testing; - -namespace osu.Framework.Tests -{ - internal class AutomatedVisualTestGame : TestGame - { - public AutomatedVisualTestGame() - { - Add(new TestBrowserTestRunner(new TestBrowser())); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Testing; + +namespace osu.Framework.Tests +{ + internal class AutomatedVisualTestGame : TestGame + { + public AutomatedVisualTestGame() + { + Add(new TestBrowserTestRunner(new TestBrowser())); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableBoolTest.cs b/osu.Framework.Tests/Bindables/BindableBoolTest.cs index 6204c70b6..060ff6e34 100644 --- a/osu.Framework.Tests/Bindables/BindableBoolTest.cs +++ b/osu.Framework.Tests/Bindables/BindableBoolTest.cs @@ -1,44 +1,44 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableBoolTest - { - [TestCase(true)] - [TestCase(false)] - public void TestSet(bool value) - { - var bindable = new BindableBool { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("True", true)] - [TestCase("true", true)] - [TestCase("False", false)] - [TestCase("false", false)] - [TestCase("1", true)] - [TestCase("0", false)] - public void TestParsingString(string value, bool expected) - { - var bindable = new BindableBool(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(true)] - [TestCase(false)] - public void TestParsingBoolean(bool value) - { - var bindable = new BindableBool(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableBoolTest + { + [TestCase(true)] + [TestCase(false)] + public void TestSet(bool value) + { + var bindable = new BindableBool { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("True", true)] + [TestCase("true", true)] + [TestCase("False", false)] + [TestCase("false", false)] + [TestCase("1", true)] + [TestCase("0", false)] + public void TestParsingString(string value, bool expected) + { + var bindable = new BindableBool(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(true)] + [TestCase(false)] + public void TestParsingBoolean(bool value) + { + var bindable = new BindableBool(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableDoubleTest.cs b/osu.Framework.Tests/Bindables/BindableDoubleTest.cs index 8153edaef..a120f246b 100644 --- a/osu.Framework.Tests/Bindables/BindableDoubleTest.cs +++ b/osu.Framework.Tests/Bindables/BindableDoubleTest.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableDoubleTest - { - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105.123)] - [TestCase(105.123)] - [TestCase(double.MinValue)] - [TestCase(double.MaxValue)] - public void TestSet(double value) - { - var bindable = new BindableDouble { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("0", 0f)] - [TestCase("1", 1f)] - [TestCase("-0", 0f)] - [TestCase("-105.123", -105.123)] - [TestCase("105.123", 105.123)] - public void TestParsingString(string value, double expected) - { - var bindable = new BindableDouble(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase("0", -10, 10, 0)] - [TestCase("1", -10, 10, 1)] - [TestCase("-0", -10, 10, 0)] - [TestCase("-105.123", -10, 10, -10)] - [TestCase("105.123", -10, 10, 10)] - public void TestParsingStringWithRange(string value, double minValue, double maxValue, double expected) - { - var bindable = new BindableDouble { MinValue = minValue, MaxValue = maxValue }; - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105.123)] - [TestCase(105.123)] - [TestCase(double.MinValue)] - [TestCase(double.MaxValue)] - public void TestParsingDouble(double value) - { - var bindable = new BindableDouble(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableDoubleTest + { + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105.123)] + [TestCase(105.123)] + [TestCase(double.MinValue)] + [TestCase(double.MaxValue)] + public void TestSet(double value) + { + var bindable = new BindableDouble { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("0", 0f)] + [TestCase("1", 1f)] + [TestCase("-0", 0f)] + [TestCase("-105.123", -105.123)] + [TestCase("105.123", 105.123)] + public void TestParsingString(string value, double expected) + { + var bindable = new BindableDouble(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase("0", -10, 10, 0)] + [TestCase("1", -10, 10, 1)] + [TestCase("-0", -10, 10, 0)] + [TestCase("-105.123", -10, 10, -10)] + [TestCase("105.123", -10, 10, 10)] + public void TestParsingStringWithRange(string value, double minValue, double maxValue, double expected) + { + var bindable = new BindableDouble { MinValue = minValue, MaxValue = maxValue }; + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105.123)] + [TestCase(105.123)] + [TestCase(double.MinValue)] + [TestCase(double.MaxValue)] + public void TestParsingDouble(double value) + { + var bindable = new BindableDouble(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableEnumTest.cs b/osu.Framework.Tests/Bindables/BindableEnumTest.cs index 07d82deaf..2ddd53d3f 100644 --- a/osu.Framework.Tests/Bindables/BindableEnumTest.cs +++ b/osu.Framework.Tests/Bindables/BindableEnumTest.cs @@ -1,52 +1,52 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableEnumTest - { - [TestCase(TestEnum.Value1)] - [TestCase(TestEnum.Value2)] - [TestCase(TestEnum.Value1 - 1)] - [TestCase(TestEnum.Value2 + 1)] - public void TestSet(TestEnum value) - { - var bindable = new Bindable { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("Value1", TestEnum.Value1)] - [TestCase("Value2", TestEnum.Value2)] - [TestCase("-1", TestEnum.Value1 - 1)] - [TestCase("2", TestEnum.Value2 + 1)] - public void TestParsingString(string value, TestEnum expected) - { - var bindable = new Bindable(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(TestEnum.Value1)] - [TestCase(TestEnum.Value2)] - [TestCase(TestEnum.Value1 - 1)] - [TestCase(TestEnum.Value2 + 1)] - public void TestParsingEnum(TestEnum value) - { - var bindable = new Bindable(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - - public enum TestEnum - { - Value1 = 0, - Value2 = 1 - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableEnumTest + { + [TestCase(TestEnum.Value1)] + [TestCase(TestEnum.Value2)] + [TestCase(TestEnum.Value1 - 1)] + [TestCase(TestEnum.Value2 + 1)] + public void TestSet(TestEnum value) + { + var bindable = new Bindable { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("Value1", TestEnum.Value1)] + [TestCase("Value2", TestEnum.Value2)] + [TestCase("-1", TestEnum.Value1 - 1)] + [TestCase("2", TestEnum.Value2 + 1)] + public void TestParsingString(string value, TestEnum expected) + { + var bindable = new Bindable(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(TestEnum.Value1)] + [TestCase(TestEnum.Value2)] + [TestCase(TestEnum.Value1 - 1)] + [TestCase(TestEnum.Value2 + 1)] + public void TestParsingEnum(TestEnum value) + { + var bindable = new Bindable(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + + public enum TestEnum + { + Value1 = 0, + Value2 = 1 + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableFloatTest.cs b/osu.Framework.Tests/Bindables/BindableFloatTest.cs index dda4bc48a..012025f85 100644 --- a/osu.Framework.Tests/Bindables/BindableFloatTest.cs +++ b/osu.Framework.Tests/Bindables/BindableFloatTest.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableFloatTest - { - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105.123f)] - [TestCase(105.123f)] - [TestCase(float.MinValue)] - [TestCase(float.MaxValue)] - public void TestSet(float value) - { - var bindable = new BindableFloat { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("0", 0f)] - [TestCase("1", 1f)] - [TestCase("-0", 0f)] - [TestCase("-105.123", -105.123f)] - [TestCase("105.123", 105.123f)] - public void TestParsingString(string value, float expected) - { - var bindable = new BindableFloat(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase("0", -10, 10, 0)] - [TestCase("1", -10, 10, 1)] - [TestCase("-0", -10, 10, 0)] - [TestCase("-105.123", -10, 10, -10)] - [TestCase("105.123", -10, 10, 10)] - public void TestParsingStringWithRange(string value, float minValue, float maxValue, float expected) - { - var bindable = new BindableFloat { MinValue = minValue, MaxValue = maxValue }; - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105.123f)] - [TestCase(105.123f)] - [TestCase(float.MinValue)] - [TestCase(float.MaxValue)] - public void TestParsingFloat(float value) - { - var bindable = new BindableFloat(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableFloatTest + { + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105.123f)] + [TestCase(105.123f)] + [TestCase(float.MinValue)] + [TestCase(float.MaxValue)] + public void TestSet(float value) + { + var bindable = new BindableFloat { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("0", 0f)] + [TestCase("1", 1f)] + [TestCase("-0", 0f)] + [TestCase("-105.123", -105.123f)] + [TestCase("105.123", 105.123f)] + public void TestParsingString(string value, float expected) + { + var bindable = new BindableFloat(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase("0", -10, 10, 0)] + [TestCase("1", -10, 10, 1)] + [TestCase("-0", -10, 10, 0)] + [TestCase("-105.123", -10, 10, -10)] + [TestCase("105.123", -10, 10, 10)] + public void TestParsingStringWithRange(string value, float minValue, float maxValue, float expected) + { + var bindable = new BindableFloat { MinValue = minValue, MaxValue = maxValue }; + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105.123f)] + [TestCase(105.123f)] + [TestCase(float.MinValue)] + [TestCase(float.MaxValue)] + public void TestParsingFloat(float value) + { + var bindable = new BindableFloat(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableIntTest.cs b/osu.Framework.Tests/Bindables/BindableIntTest.cs index 697bd98b5..7310384f3 100644 --- a/osu.Framework.Tests/Bindables/BindableIntTest.cs +++ b/osu.Framework.Tests/Bindables/BindableIntTest.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableIntTest - { - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105)] - [TestCase(105)] - [TestCase(int.MinValue)] - [TestCase(int.MaxValue)] - public void TestSet(int value) - { - var bindable = new BindableInt { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("0", 0)] - [TestCase("1", 1)] - [TestCase("-0", 0)] - [TestCase("-105", -105)] - [TestCase("105", 105)] - public void TestParsingString(string value, int expected) - { - var bindable = new BindableInt(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase("0", -10, 10, 0)] - [TestCase("1", -10, 10, 1)] - [TestCase("-0", -10, 10, 0)] - [TestCase("-105", -10, 10, -10)] - [TestCase("105", -10, 10, 10)] - public void TestParsingStringWithRange(string value, int minValue, int maxValue, int expected) - { - var bindable = new BindableInt { MinValue = minValue, MaxValue = maxValue }; - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105)] - [TestCase(105)] - [TestCase(int.MinValue)] - [TestCase(int.MaxValue)] - public void TestParsingInt(int value) - { - var bindable = new BindableInt(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableIntTest + { + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105)] + [TestCase(105)] + [TestCase(int.MinValue)] + [TestCase(int.MaxValue)] + public void TestSet(int value) + { + var bindable = new BindableInt { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("0", 0)] + [TestCase("1", 1)] + [TestCase("-0", 0)] + [TestCase("-105", -105)] + [TestCase("105", 105)] + public void TestParsingString(string value, int expected) + { + var bindable = new BindableInt(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase("0", -10, 10, 0)] + [TestCase("1", -10, 10, 1)] + [TestCase("-0", -10, 10, 0)] + [TestCase("-105", -10, 10, -10)] + [TestCase("105", -10, 10, 10)] + public void TestParsingStringWithRange(string value, int minValue, int maxValue, int expected) + { + var bindable = new BindableInt { MinValue = minValue, MaxValue = maxValue }; + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105)] + [TestCase(105)] + [TestCase(int.MinValue)] + [TestCase(int.MaxValue)] + public void TestParsingInt(int value) + { + var bindable = new BindableInt(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableLongTest.cs b/osu.Framework.Tests/Bindables/BindableLongTest.cs index 630fca269..0565c6092 100644 --- a/osu.Framework.Tests/Bindables/BindableLongTest.cs +++ b/osu.Framework.Tests/Bindables/BindableLongTest.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableLongTest - { - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105)] - [TestCase(105)] - [TestCase(long.MinValue)] - [TestCase(long.MaxValue)] - public void TestSet(long value) - { - var bindable = new BindableLong { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("0", 0)] - [TestCase("1", 1)] - [TestCase("-0", 0)] - [TestCase("-105", -105)] - [TestCase("105", 105)] - public void TestParsingString(string value, long expected) - { - var bindable = new BindableLong(); - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase("0", -10, 10, 0)] - [TestCase("1", -10, 10, 1)] - [TestCase("-0", -10, 10, 0)] - [TestCase("-105", -10, 10, -10)] - [TestCase("105", -10, 10, 10)] - public void TestParsingStringWithRange(string value, long minValue, long maxValue, long expected) - { - var bindable = new BindableLong { MinValue = minValue, MaxValue = maxValue }; - bindable.Parse(value); - - Assert.AreEqual(expected, bindable.Value); - } - - [TestCase(0)] - [TestCase(-0)] - [TestCase(1)] - [TestCase(-105)] - [TestCase(105)] - [TestCase(long.MinValue)] - [TestCase(long.MaxValue)] - public void TestParsingLong(long value) - { - var bindable = new BindableLong(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableLongTest + { + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105)] + [TestCase(105)] + [TestCase(long.MinValue)] + [TestCase(long.MaxValue)] + public void TestSet(long value) + { + var bindable = new BindableLong { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("0", 0)] + [TestCase("1", 1)] + [TestCase("-0", 0)] + [TestCase("-105", -105)] + [TestCase("105", 105)] + public void TestParsingString(string value, long expected) + { + var bindable = new BindableLong(); + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase("0", -10, 10, 0)] + [TestCase("1", -10, 10, 1)] + [TestCase("-0", -10, 10, 0)] + [TestCase("-105", -10, 10, -10)] + [TestCase("105", -10, 10, 10)] + public void TestParsingStringWithRange(string value, long minValue, long maxValue, long expected) + { + var bindable = new BindableLong { MinValue = minValue, MaxValue = maxValue }; + bindable.Parse(value); + + Assert.AreEqual(expected, bindable.Value); + } + + [TestCase(0)] + [TestCase(-0)] + [TestCase(1)] + [TestCase(-105)] + [TestCase(105)] + [TestCase(long.MinValue)] + [TestCase(long.MaxValue)] + public void TestParsingLong(long value) + { + var bindable = new BindableLong(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/Bindables/BindableStringTest.cs b/osu.Framework.Tests/Bindables/BindableStringTest.cs index 77a98a9a7..e6c2eef28 100644 --- a/osu.Framework.Tests/Bindables/BindableStringTest.cs +++ b/osu.Framework.Tests/Bindables/BindableStringTest.cs @@ -1,32 +1,32 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Configuration; - -namespace osu.Framework.Tests.Bindables -{ - [TestFixture] - public class BindableStringTest - { - [TestCase("")] - [TestCase(null)] - [TestCase("this is a string")] - public void TestSet(string value) - { - var bindable = new Bindable { Value = value }; - Assert.AreEqual(value, bindable.Value); - } - - [TestCase("")] - [TestCase("null")] - [TestCase("this is a string")] - public void TestParsingString(string value) - { - var bindable = new Bindable(); - bindable.Parse(value); - - Assert.AreEqual(value, bindable.Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Configuration; + +namespace osu.Framework.Tests.Bindables +{ + [TestFixture] + public class BindableStringTest + { + [TestCase("")] + [TestCase(null)] + [TestCase("this is a string")] + public void TestSet(string value) + { + var bindable = new Bindable { Value = value }; + Assert.AreEqual(value, bindable.Value); + } + + [TestCase("")] + [TestCase("null")] + [TestCase("this is a string")] + public void TestParsingString(string value) + { + var bindable = new Bindable(); + bindable.Parse(value); + + Assert.AreEqual(value, bindable.Value); + } + } +} diff --git a/osu.Framework.Tests/IO/TestSortedListSerialization.cs b/osu.Framework.Tests/IO/TestSortedListSerialization.cs index 4f7f172cf..99af4d571 100644 --- a/osu.Framework.Tests/IO/TestSortedListSerialization.cs +++ b/osu.Framework.Tests/IO/TestSortedListSerialization.cs @@ -1,64 +1,64 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using Newtonsoft.Json; -using NUnit.Framework; -using osu.Framework.Lists; - -namespace osu.Framework.Tests.IO -{ - [TestFixture] - public class TestSortedListSerialization - { - [Test] - public void TestUnsortedSerialization() - { - var original = new SortedList(); - original.AddRange(new[] { 1, 2, 3, 4, 5, 6 }); - - var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); - - Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); - for (int i = 0; i < original.Count; i++) - Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); - } - - [Test] - public void TestSortedSerialization() - { - var original = new SortedList(); - original.AddRange(new[] { 6, 5, 4, 3, 2, 1 }); - - var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); - - Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); - for (int i = 0; i < original.Count; i++) - Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); - } - - [Test] - public void TestEmptySerialization() - { - var original = new SortedList(); - var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); - - Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); - } - - [Test] - public void TestCustomComparer() - { - int compare(int i1, int i2) => i2.CompareTo(i1); - - var original = new SortedList(compare); - original.AddRange(new[] { 1, 2, 3, 4, 5, 6 }); - - var deserialized = new SortedList(compare); - JsonConvert.PopulateObject(JsonConvert.SerializeObject(original), deserialized); - - Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); - for (int i = 0; i < original.Count; i++) - Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.Lists; + +namespace osu.Framework.Tests.IO +{ + [TestFixture] + public class TestSortedListSerialization + { + [Test] + public void TestUnsortedSerialization() + { + var original = new SortedList(); + original.AddRange(new[] { 1, 2, 3, 4, 5, 6 }); + + var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); + + Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); + for (int i = 0; i < original.Count; i++) + Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); + } + + [Test] + public void TestSortedSerialization() + { + var original = new SortedList(); + original.AddRange(new[] { 6, 5, 4, 3, 2, 1 }); + + var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); + + Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); + for (int i = 0; i < original.Count; i++) + Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); + } + + [Test] + public void TestEmptySerialization() + { + var original = new SortedList(); + var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(original)); + + Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); + } + + [Test] + public void TestCustomComparer() + { + int compare(int i1, int i2) => i2.CompareTo(i1); + + var original = new SortedList(compare); + original.AddRange(new[] { 1, 2, 3, 4, 5, 6 }); + + var deserialized = new SortedList(compare); + JsonConvert.PopulateObject(JsonConvert.SerializeObject(original), deserialized); + + Assert.AreEqual(original.Count, deserialized.Count, "Counts are not equal"); + for (int i = 0; i < original.Count; i++) + Assert.AreEqual(original[i], deserialized[i], $"Item at index {i} differs"); + } + } +} diff --git a/osu.Framework.Tests/IO/TestWebRequest.cs b/osu.Framework.Tests/IO/TestWebRequest.cs index b83ad0052..665a5b2f0 100644 --- a/osu.Framework.Tests/IO/TestWebRequest.cs +++ b/osu.Framework.Tests/IO/TestWebRequest.cs @@ -1,438 +1,438 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Reflection; -using Newtonsoft.Json; -using NUnit.Framework; -using osu.Framework.IO.Network; -using HttpMethod = osu.Framework.IO.Network.HttpMethod; -using WebRequest = osu.Framework.IO.Network.WebRequest; - -namespace osu.Framework.Tests.IO -{ - [TestFixture] - public class TestWebRequest - { - private const string valid_get_url = "httpbin.org/get"; - private const string invalid_get_url = "a.ppy.shhhhh"; - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestValidGet([Values("http", "https")] string protocol, [Values(true, false)] bool async) - { - var url = $"{protocol}://httpbin.org/get"; - var request = new JsonWebRequest(url) { Method = HttpMethod.GET }; - - bool hasThrown = false; - request.Failed += exception => hasThrown = exception != null; - - if (async) - Assert.DoesNotThrowAsync(request.PerformAsync); - else - Assert.DoesNotThrow(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsFalse(request.Aborted); - - var responseObject = request.ResponseObject; - - Assert.IsTrue(responseObject != null); - Assert.IsTrue(responseObject.Headers.UserAgent == "osu!"); - Assert.IsTrue(responseObject.Url == url); - - Assert.IsFalse(hasThrown); - } - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestInvalidGetExceptions([Values("http", "https")] string protocol, [Values(true, false)] bool async) - { - var request = new WebRequest($"{protocol}://{invalid_get_url}") { Method = HttpMethod.GET }; - - Exception finishedException = null; - request.Failed += exception => finishedException = exception; - - if (async) - Assert.ThrowsAsync(request.PerformAsync); - else - Assert.Throws(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - Assert.IsTrue(request.ResponseString == null); - Assert.IsNotNull(finishedException); - } - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestBadStatusCode([Values(true, false)] bool async) - { - var request = new WebRequest("https://httpbin.org/hidden-basic-auth/user/passwd"); - - bool hasThrown = false; - request.Failed += exception => hasThrown = exception != null; - - if (async) - Assert.ThrowsAsync(request.PerformAsync); - else - Assert.Throws(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - Assert.IsEmpty(request.ResponseString); - - Assert.IsTrue(hasThrown); - } - - /// - /// Tests aborting the after response has been received from the server - /// but before data has been read. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestAbortReceive([Values(true, false)] bool async) - { - var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; - - bool hasThrown = false; - request.Failed += exception => hasThrown = exception != null; - request.Started += () => request.Abort(); - - if (async) - Assert.DoesNotThrowAsync(request.PerformAsync); - else - Assert.DoesNotThrow(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - Assert.IsTrue(request.ResponseObject == null); - - Assert.IsFalse(hasThrown); - } - - /// - /// Tests aborting the before the request is sent to the server. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestAbortRequest() - { - var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; - - bool hasThrown = false; - request.Failed += exception => hasThrown = exception != null; - -#pragma warning disable 4014 - request.PerformAsync(); -#pragma warning restore 4014 - - Assert.DoesNotThrow(request.Abort); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - Assert.IsTrue(request.ResponseObject == null); - - Assert.IsFalse(hasThrown); - } - - /// - /// Tests being able to abort + restart a request. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestRestartAfterAbort([Values(true, false)] bool async) - { - var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; - - bool hasThrown = false; - request.Failed += exception => hasThrown = exception != null; - -#pragma warning disable 4014 - request.PerformAsync(); -#pragma warning restore 4014 - - Assert.DoesNotThrow(request.Abort); - - if (async) - Assert.ThrowsAsync(request.PerformAsync); - else - Assert.Throws(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - var responseObject = request.ResponseObject; - - Assert.IsTrue(responseObject == null); - Assert.IsFalse(hasThrown); - } - - /// - /// Tests that specifically-crafted is completed after one timeout. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestOneTimeout() - { - var request = new DelayedWebRequest - { - Method = HttpMethod.GET, - Timeout = 1000, - Delay = 2 - }; - - Exception thrownException = null; - request.Failed += e => thrownException = e; - request.CompleteInvoked = () => request.Delay = 0; - - Assert.DoesNotThrow(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsFalse(request.Aborted); - - Assert.IsTrue(thrownException == null); - Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount); - } - - /// - /// Tests that a will only timeout a maximum of times before being aborted. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestFailTimeout() - { - var request = new WebRequest("https://httpbin.org/delay/4") - { - Method = HttpMethod.GET, - Timeout = 1000 - }; - - Exception thrownException = null; - request.Failed += e => thrownException = e; - - Assert.Throws(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsTrue(request.Aborted); - - Assert.IsTrue(thrownException != null); - Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount); - Assert.AreEqual(typeof(WebException), thrownException.GetType()); - } - - /// - /// Tests being able to abort + restart a request. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestEventUnbindOnCompletion([Values(true, false)] bool async) - { - var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; - - request.Started += () => { }; - request.Failed += e => { }; - request.DownloadProgress += (l1, l2) => { }; - request.UploadProgress += (l1, l2) => { }; - - Assert.DoesNotThrow(request.Perform); - - var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public); - foreach (var e in events) - { - var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public); - Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0); - } - } - - /// - /// Tests being able to abort + restart a request. - /// - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestUnbindOnDispose([Values(true, false)] bool async) - { - WebRequest request; - using (request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }) - { - request.Started += () => { }; - request.Failed += e => { }; - request.DownloadProgress += (l1, l2) => { }; - request.UploadProgress += (l1, l2) => { }; - - Assert.DoesNotThrow(request.Perform); - } - - var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public); - foreach (var e in events) - { - var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public); - Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0); - } - } - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestPostWithJsonResponse([Values(true, false)] bool async) - { - var request = new JsonWebRequest("https://httpbin.org/post") { Method = HttpMethod.POST }; - - request.AddParameter("testkey1", "testval1"); - request.AddParameter("testkey2", "testval2"); - - if (async) - Assert.DoesNotThrowAsync(request.PerformAsync); - else - Assert.DoesNotThrow(request.Perform); - - var responseObject = request.ResponseObject; - - Assert.IsTrue(request.Completed); - Assert.IsFalse(request.Aborted); - - Assert.IsTrue(responseObject.Form != null); - Assert.IsTrue(responseObject.Form.Count == 2); - - Assert.IsTrue(responseObject.Headers.ContentLength > 0); - - Assert.IsTrue(responseObject.Form.ContainsKey("testkey1")); - Assert.IsTrue(responseObject.Form["testkey1"] == "testval1"); - - Assert.IsTrue(responseObject.Form.ContainsKey("testkey2")); - Assert.IsTrue(responseObject.Form["testkey2"] == "testval2"); - - Assert.IsTrue(responseObject.Headers.ContentType.StartsWith("multipart/form-data; boundary=")); - } - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestPostWithJsonRequest([Values(true, false)] bool async) - { - var request = new JsonWebRequest("https://httpbin.org/post") { Method = HttpMethod.POST }; - - var testObject = new TestObject(); - request.AddRaw(JsonConvert.SerializeObject(testObject)); - - if (async) - Assert.DoesNotThrowAsync(request.PerformAsync); - else - Assert.DoesNotThrow(request.Perform); - - var responseObject = request.ResponseObject; - - Assert.IsTrue(request.Completed); - Assert.IsFalse(request.Aborted); - - Assert.IsTrue(responseObject.Headers.ContentLength > 0); - Assert.IsTrue(responseObject.Json != null); - Assert.AreEqual(testObject.TestString, responseObject.Json.TestString); - - Assert.IsTrue(responseObject.Headers.ContentType == null); - } - - [Test, Retry(5)] - [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] - public void TestGetBinaryData([Values(true, false)] bool async, [Values(true, false)] bool chunked) - { - const int bytes_count = 65536; - const int chunk_size = 1024; - - string endpoint = chunked ? "stream-bytes" : "bytes"; - - WebRequest request = new WebRequest($"http://httpbin.org/{endpoint}/{bytes_count}") { Method = HttpMethod.GET }; - if (chunked) - request.AddParameter("chunk_size", chunk_size.ToString()); - - if (async) - Assert.DoesNotThrowAsync(request.PerformAsync); - else - Assert.DoesNotThrow(request.Perform); - - Assert.IsTrue(request.Completed); - Assert.IsFalse(request.Aborted); - - Assert.AreEqual(bytes_count, request.ResponseStream.Length); - } - - [Serializable] - private class HttpBinGetResponse - { - [JsonProperty("headers")] - public HttpBinHeaders Headers { get; set; } - - [JsonProperty("url")] - public string Url { get; set; } - } - - - [Serializable] - private class HttpBinPostResponse - { - [JsonProperty("data")] - public string Data { get; set; } - - [JsonProperty("form")] - public IDictionary Form { get; set; } - - [JsonProperty("headers")] - public HttpBinHeaders Headers { get; set; } - - [JsonProperty("json")] - public TestObject Json { get; set; } - } - - [Serializable] - public class HttpBinHeaders - { - [JsonProperty("Content-Length")] - public int ContentLength { get; set; } - - [JsonProperty("Content-Type")] - public string ContentType { get; set; } - - [JsonProperty("User-Agent")] - public string UserAgent { get; set; } - } - - [Serializable] - public class TestObject - { - public string TestString = "readable"; - } - - private class DelayedWebRequest : WebRequest - { - public Action CompleteInvoked; - - private int delay; - - public int Delay - { - get { return delay; } - set - { - delay = value; - Url = $"http://httpbin.org/delay/{delay}"; - } - } - - public DelayedWebRequest() - : base("http://httpbin.org/delay/0") - { - } - - protected override void Complete(Exception e = null) - { - CompleteInvoked?.Invoke(); - base.Complete(e); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Reflection; +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.IO.Network; +using HttpMethod = osu.Framework.IO.Network.HttpMethod; +using WebRequest = osu.Framework.IO.Network.WebRequest; + +namespace osu.Framework.Tests.IO +{ + [TestFixture] + public class TestWebRequest + { + private const string valid_get_url = "httpbin.org/get"; + private const string invalid_get_url = "a.ppy.shhhhh"; + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestValidGet([Values("http", "https")] string protocol, [Values(true, false)] bool async) + { + var url = $"{protocol}://httpbin.org/get"; + var request = new JsonWebRequest(url) { Method = HttpMethod.GET }; + + bool hasThrown = false; + request.Failed += exception => hasThrown = exception != null; + + if (async) + Assert.DoesNotThrowAsync(request.PerformAsync); + else + Assert.DoesNotThrow(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsFalse(request.Aborted); + + var responseObject = request.ResponseObject; + + Assert.IsTrue(responseObject != null); + Assert.IsTrue(responseObject.Headers.UserAgent == "osu!"); + Assert.IsTrue(responseObject.Url == url); + + Assert.IsFalse(hasThrown); + } + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestInvalidGetExceptions([Values("http", "https")] string protocol, [Values(true, false)] bool async) + { + var request = new WebRequest($"{protocol}://{invalid_get_url}") { Method = HttpMethod.GET }; + + Exception finishedException = null; + request.Failed += exception => finishedException = exception; + + if (async) + Assert.ThrowsAsync(request.PerformAsync); + else + Assert.Throws(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + Assert.IsTrue(request.ResponseString == null); + Assert.IsNotNull(finishedException); + } + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestBadStatusCode([Values(true, false)] bool async) + { + var request = new WebRequest("https://httpbin.org/hidden-basic-auth/user/passwd"); + + bool hasThrown = false; + request.Failed += exception => hasThrown = exception != null; + + if (async) + Assert.ThrowsAsync(request.PerformAsync); + else + Assert.Throws(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + Assert.IsEmpty(request.ResponseString); + + Assert.IsTrue(hasThrown); + } + + /// + /// Tests aborting the after response has been received from the server + /// but before data has been read. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestAbortReceive([Values(true, false)] bool async) + { + var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; + + bool hasThrown = false; + request.Failed += exception => hasThrown = exception != null; + request.Started += () => request.Abort(); + + if (async) + Assert.DoesNotThrowAsync(request.PerformAsync); + else + Assert.DoesNotThrow(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + Assert.IsTrue(request.ResponseObject == null); + + Assert.IsFalse(hasThrown); + } + + /// + /// Tests aborting the before the request is sent to the server. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestAbortRequest() + { + var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; + + bool hasThrown = false; + request.Failed += exception => hasThrown = exception != null; + +#pragma warning disable 4014 + request.PerformAsync(); +#pragma warning restore 4014 + + Assert.DoesNotThrow(request.Abort); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + Assert.IsTrue(request.ResponseObject == null); + + Assert.IsFalse(hasThrown); + } + + /// + /// Tests being able to abort + restart a request. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestRestartAfterAbort([Values(true, false)] bool async) + { + var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; + + bool hasThrown = false; + request.Failed += exception => hasThrown = exception != null; + +#pragma warning disable 4014 + request.PerformAsync(); +#pragma warning restore 4014 + + Assert.DoesNotThrow(request.Abort); + + if (async) + Assert.ThrowsAsync(request.PerformAsync); + else + Assert.Throws(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + var responseObject = request.ResponseObject; + + Assert.IsTrue(responseObject == null); + Assert.IsFalse(hasThrown); + } + + /// + /// Tests that specifically-crafted is completed after one timeout. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestOneTimeout() + { + var request = new DelayedWebRequest + { + Method = HttpMethod.GET, + Timeout = 1000, + Delay = 2 + }; + + Exception thrownException = null; + request.Failed += e => thrownException = e; + request.CompleteInvoked = () => request.Delay = 0; + + Assert.DoesNotThrow(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsFalse(request.Aborted); + + Assert.IsTrue(thrownException == null); + Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount); + } + + /// + /// Tests that a will only timeout a maximum of times before being aborted. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestFailTimeout() + { + var request = new WebRequest("https://httpbin.org/delay/4") + { + Method = HttpMethod.GET, + Timeout = 1000 + }; + + Exception thrownException = null; + request.Failed += e => thrownException = e; + + Assert.Throws(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsTrue(request.Aborted); + + Assert.IsTrue(thrownException != null); + Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount); + Assert.AreEqual(typeof(WebException), thrownException.GetType()); + } + + /// + /// Tests being able to abort + restart a request. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestEventUnbindOnCompletion([Values(true, false)] bool async) + { + var request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }; + + request.Started += () => { }; + request.Failed += e => { }; + request.DownloadProgress += (l1, l2) => { }; + request.UploadProgress += (l1, l2) => { }; + + Assert.DoesNotThrow(request.Perform); + + var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public); + foreach (var e in events) + { + var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public); + Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0); + } + } + + /// + /// Tests being able to abort + restart a request. + /// + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestUnbindOnDispose([Values(true, false)] bool async) + { + WebRequest request; + using (request = new JsonWebRequest("https://httpbin.org/get") { Method = HttpMethod.GET }) + { + request.Started += () => { }; + request.Failed += e => { }; + request.DownloadProgress += (l1, l2) => { }; + request.UploadProgress += (l1, l2) => { }; + + Assert.DoesNotThrow(request.Perform); + } + + var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public); + foreach (var e in events) + { + var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public); + Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0); + } + } + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestPostWithJsonResponse([Values(true, false)] bool async) + { + var request = new JsonWebRequest("https://httpbin.org/post") { Method = HttpMethod.POST }; + + request.AddParameter("testkey1", "testval1"); + request.AddParameter("testkey2", "testval2"); + + if (async) + Assert.DoesNotThrowAsync(request.PerformAsync); + else + Assert.DoesNotThrow(request.Perform); + + var responseObject = request.ResponseObject; + + Assert.IsTrue(request.Completed); + Assert.IsFalse(request.Aborted); + + Assert.IsTrue(responseObject.Form != null); + Assert.IsTrue(responseObject.Form.Count == 2); + + Assert.IsTrue(responseObject.Headers.ContentLength > 0); + + Assert.IsTrue(responseObject.Form.ContainsKey("testkey1")); + Assert.IsTrue(responseObject.Form["testkey1"] == "testval1"); + + Assert.IsTrue(responseObject.Form.ContainsKey("testkey2")); + Assert.IsTrue(responseObject.Form["testkey2"] == "testval2"); + + Assert.IsTrue(responseObject.Headers.ContentType.StartsWith("multipart/form-data; boundary=")); + } + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestPostWithJsonRequest([Values(true, false)] bool async) + { + var request = new JsonWebRequest("https://httpbin.org/post") { Method = HttpMethod.POST }; + + var testObject = new TestObject(); + request.AddRaw(JsonConvert.SerializeObject(testObject)); + + if (async) + Assert.DoesNotThrowAsync(request.PerformAsync); + else + Assert.DoesNotThrow(request.Perform); + + var responseObject = request.ResponseObject; + + Assert.IsTrue(request.Completed); + Assert.IsFalse(request.Aborted); + + Assert.IsTrue(responseObject.Headers.ContentLength > 0); + Assert.IsTrue(responseObject.Json != null); + Assert.AreEqual(testObject.TestString, responseObject.Json.TestString); + + Assert.IsTrue(responseObject.Headers.ContentType == null); + } + + [Test, Retry(5)] + [Ignore("Broken (appveyor or httpbin.org, see https://ci.appveyor.com/project/peppy/osu-framework/build/5155)")] + public void TestGetBinaryData([Values(true, false)] bool async, [Values(true, false)] bool chunked) + { + const int bytes_count = 65536; + const int chunk_size = 1024; + + string endpoint = chunked ? "stream-bytes" : "bytes"; + + WebRequest request = new WebRequest($"http://httpbin.org/{endpoint}/{bytes_count}") { Method = HttpMethod.GET }; + if (chunked) + request.AddParameter("chunk_size", chunk_size.ToString()); + + if (async) + Assert.DoesNotThrowAsync(request.PerformAsync); + else + Assert.DoesNotThrow(request.Perform); + + Assert.IsTrue(request.Completed); + Assert.IsFalse(request.Aborted); + + Assert.AreEqual(bytes_count, request.ResponseStream.Length); + } + + [Serializable] + private class HttpBinGetResponse + { + [JsonProperty("headers")] + public HttpBinHeaders Headers { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + } + + + [Serializable] + private class HttpBinPostResponse + { + [JsonProperty("data")] + public string Data { get; set; } + + [JsonProperty("form")] + public IDictionary Form { get; set; } + + [JsonProperty("headers")] + public HttpBinHeaders Headers { get; set; } + + [JsonProperty("json")] + public TestObject Json { get; set; } + } + + [Serializable] + public class HttpBinHeaders + { + [JsonProperty("Content-Length")] + public int ContentLength { get; set; } + + [JsonProperty("Content-Type")] + public string ContentType { get; set; } + + [JsonProperty("User-Agent")] + public string UserAgent { get; set; } + } + + [Serializable] + public class TestObject + { + public string TestString = "readable"; + } + + private class DelayedWebRequest : WebRequest + { + public Action CompleteInvoked; + + private int delay; + + public int Delay + { + get { return delay; } + set + { + delay = value; + Url = $"http://httpbin.org/delay/{delay}"; + } + } + + public DelayedWebRequest() + : base("http://httpbin.org/delay/0") + { + } + + protected override void Complete(Exception e = null) + { + CompleteInvoked?.Invoke(); + base.Complete(e); + } + } + } +} diff --git a/osu.Framework.Tests/Lists/TestArrayExtensions.cs b/osu.Framework.Tests/Lists/TestArrayExtensions.cs index 21adb7da5..2d4501788 100644 --- a/osu.Framework.Tests/Lists/TestArrayExtensions.cs +++ b/osu.Framework.Tests/Lists/TestArrayExtensions.cs @@ -1,140 +1,140 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Extensions; - -namespace osu.Framework.Tests.Lists -{ - [TestFixture] - public class TestArrayExtensions - { - [Test] - public void TestNullToJagged() - { - int[][] result = null; - Assert.DoesNotThrow(() => result = ((int[,])null).ToJagged()); - Assert.AreEqual(null, result); - } - - [Test] - public void TestNullToRectangular() - { - int[,] result = null; - Assert.DoesNotThrow(() => result = ((int[][])null).ToRectangular()); - Assert.AreEqual(null, result); - } - - [Test] - public void TestEmptyRectangularToJagged() - { - int[][] result = null; - Assert.DoesNotThrow(() => result = new int[0, 0].ToJagged()); - Assert.AreEqual(0, result.Length); - } - - [Test] - public void TestEmptyJaggedToRectangular() - { - int[,] result = null; - Assert.DoesNotThrow(() => result = new int[0][].ToRectangular()); - Assert.AreEqual(0, result.Length); - } - - [Test] - public void TestRectangularColumnToJagged() - { - int[][] result = null; - Assert.DoesNotThrow(() => result = new int[1, 10].ToJagged()); - Assert.AreEqual(1, result.Length); - Assert.AreEqual(10, result[0].Length); - } - - [Test] - public void TestJaggedColumnToRectangular() - { - var jagged = new int[10][]; - - int[,] result = null; - Assert.DoesNotThrow(() => result = jagged.ToRectangular()); - Assert.AreEqual(10, result.GetLength(0)); - } - - [Test] - public void TestRectangularRowToJagged() - { - int[][] result = null; - Assert.DoesNotThrow(() => result = new int[10, 0].ToJagged()); - Assert.AreEqual(10, result.Length); - for (int i = 0; i < 10; i++) - Assert.AreEqual(0, result[i].Length); - } - - [Test] - public void TestJaggedRowToRectangular() - { - var jagged = new int[1][]; - jagged[0] = new int[10]; - - int[,] result = null; - Assert.DoesNotThrow(() => result = jagged.ToRectangular()); - Assert.AreEqual(10, result.GetLength(1)); - Assert.AreEqual(1, result.GetLength(0)); - } - - [Test] - public void TestSquareRectangularToJagged() - { - int[][] result = null; - Assert.DoesNotThrow(() => result = new int[10, 10].ToJagged()); - Assert.AreEqual(10, result.Length); - for (int i = 0; i < 10; i++) - Assert.AreEqual(10, result[i].Length); - } - - [Test] - public void TestSquareJaggedToRectangular() - { - var jagged = new int[10][]; - for (int i = 0; i < 10; i++) - jagged[i] = new int[10]; - - int[,] result = null; - Assert.DoesNotThrow(() => result = jagged.ToRectangular()); - Assert.AreEqual(10, result.GetLength(0)); - Assert.AreEqual(10, result.GetLength(1)); - } - - [Test] - public void TestNonSquareJaggedToRectangular() - { - var jagged = new int[10][]; - for (int i = 0; i < 10; i++) - jagged[i] = new int[i]; - - int[,] result = null; - Assert.DoesNotThrow(() => result = jagged.ToRectangular()); - Assert.AreEqual(10, result.GetLength(0)); - Assert.AreEqual(9, result.GetLength(1)); - } - - [Test] - public void TestNonSquareJaggedWithNullRowsToRectangular() - { - var jagged = new int[10][]; - for (int i = 1; i < 10; i += 2) - { - if (i % 2 == 1) - jagged[i] = new int[i]; - } - - int[,] result = null; - Assert.DoesNotThrow(() => result = jagged.ToRectangular()); - Assert.AreEqual(10, result.GetLength(0)); - Assert.AreEqual(9, result.GetLength(1)); - - for (int i = 0; i < 10; i++) - Assert.AreEqual(0, result[i, 0]); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Extensions; + +namespace osu.Framework.Tests.Lists +{ + [TestFixture] + public class TestArrayExtensions + { + [Test] + public void TestNullToJagged() + { + int[][] result = null; + Assert.DoesNotThrow(() => result = ((int[,])null).ToJagged()); + Assert.AreEqual(null, result); + } + + [Test] + public void TestNullToRectangular() + { + int[,] result = null; + Assert.DoesNotThrow(() => result = ((int[][])null).ToRectangular()); + Assert.AreEqual(null, result); + } + + [Test] + public void TestEmptyRectangularToJagged() + { + int[][] result = null; + Assert.DoesNotThrow(() => result = new int[0, 0].ToJagged()); + Assert.AreEqual(0, result.Length); + } + + [Test] + public void TestEmptyJaggedToRectangular() + { + int[,] result = null; + Assert.DoesNotThrow(() => result = new int[0][].ToRectangular()); + Assert.AreEqual(0, result.Length); + } + + [Test] + public void TestRectangularColumnToJagged() + { + int[][] result = null; + Assert.DoesNotThrow(() => result = new int[1, 10].ToJagged()); + Assert.AreEqual(1, result.Length); + Assert.AreEqual(10, result[0].Length); + } + + [Test] + public void TestJaggedColumnToRectangular() + { + var jagged = new int[10][]; + + int[,] result = null; + Assert.DoesNotThrow(() => result = jagged.ToRectangular()); + Assert.AreEqual(10, result.GetLength(0)); + } + + [Test] + public void TestRectangularRowToJagged() + { + int[][] result = null; + Assert.DoesNotThrow(() => result = new int[10, 0].ToJagged()); + Assert.AreEqual(10, result.Length); + for (int i = 0; i < 10; i++) + Assert.AreEqual(0, result[i].Length); + } + + [Test] + public void TestJaggedRowToRectangular() + { + var jagged = new int[1][]; + jagged[0] = new int[10]; + + int[,] result = null; + Assert.DoesNotThrow(() => result = jagged.ToRectangular()); + Assert.AreEqual(10, result.GetLength(1)); + Assert.AreEqual(1, result.GetLength(0)); + } + + [Test] + public void TestSquareRectangularToJagged() + { + int[][] result = null; + Assert.DoesNotThrow(() => result = new int[10, 10].ToJagged()); + Assert.AreEqual(10, result.Length); + for (int i = 0; i < 10; i++) + Assert.AreEqual(10, result[i].Length); + } + + [Test] + public void TestSquareJaggedToRectangular() + { + var jagged = new int[10][]; + for (int i = 0; i < 10; i++) + jagged[i] = new int[10]; + + int[,] result = null; + Assert.DoesNotThrow(() => result = jagged.ToRectangular()); + Assert.AreEqual(10, result.GetLength(0)); + Assert.AreEqual(10, result.GetLength(1)); + } + + [Test] + public void TestNonSquareJaggedToRectangular() + { + var jagged = new int[10][]; + for (int i = 0; i < 10; i++) + jagged[i] = new int[i]; + + int[,] result = null; + Assert.DoesNotThrow(() => result = jagged.ToRectangular()); + Assert.AreEqual(10, result.GetLength(0)); + Assert.AreEqual(9, result.GetLength(1)); + } + + [Test] + public void TestNonSquareJaggedWithNullRowsToRectangular() + { + var jagged = new int[10][]; + for (int i = 1; i < 10; i += 2) + { + if (i % 2 == 1) + jagged[i] = new int[i]; + } + + int[,] result = null; + Assert.DoesNotThrow(() => result = jagged.ToRectangular()); + Assert.AreEqual(10, result.GetLength(0)); + Assert.AreEqual(9, result.GetLength(1)); + + for (int i = 0; i < 10; i++) + Assert.AreEqual(0, result[i, 0]); + } + } +} diff --git a/osu.Framework.Tests/Lists/TestSortedList.cs b/osu.Framework.Tests/Lists/TestSortedList.cs index 2971d7c50..a0fc6d52f 100644 --- a/osu.Framework.Tests/Lists/TestSortedList.cs +++ b/osu.Framework.Tests/Lists/TestSortedList.cs @@ -1,88 +1,88 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Lists; -using NUnit.Framework; - -namespace osu.Framework.Tests.Lists -{ - [TestFixture] - public class TestSortedList - { - [Test] - public void TestAdd() - { - var list = new SortedList(Comparer.Create((a, b) => a - b)) - { - 10, - 8, - 13, - -10 - }; - Assert.AreEqual(-10, list[0]); - Assert.AreEqual(8, list[1]); - Assert.AreEqual(10, list[2]); - Assert.AreEqual(13, list[3]); - } - - [Test] - public void TestRemove() - { - var list = new SortedList(Comparer.Create((a, b) => a - b)) - { - 10, - 8, - 13, - -10 - }; - list.Remove(8); - Assert.IsFalse(list.Any(i => i == 8)); - Assert.AreEqual(3, list.Count); - } - - [Test] - public void TestRemoveAt() - { - var list = new SortedList(Comparer.Create((a, b) => a - b)) - { - 10, - 8, - 13, - -10 - }; - list.RemoveAt(0); - Assert.IsFalse(list.Any(i => i == -10)); - Assert.AreEqual(3, list.Count); - } - - [Test] - public void TestClear() - { - var list = new SortedList(Comparer.Create((a, b) => a - b)) - { - 10, - 8, - 13, - -10 - }; - list.Clear(); - Assert.IsFalse(list.Any()); - Assert.AreEqual(0, list.Count); - } - - [Test] - public void TestIndexOf() - { - var list = new SortedList(Comparer.Default) - { - 10, - 10, - 10, - }; - - Assert.IsTrue(list.IndexOf(10) >= 0); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Lists; +using NUnit.Framework; + +namespace osu.Framework.Tests.Lists +{ + [TestFixture] + public class TestSortedList + { + [Test] + public void TestAdd() + { + var list = new SortedList(Comparer.Create((a, b) => a - b)) + { + 10, + 8, + 13, + -10 + }; + Assert.AreEqual(-10, list[0]); + Assert.AreEqual(8, list[1]); + Assert.AreEqual(10, list[2]); + Assert.AreEqual(13, list[3]); + } + + [Test] + public void TestRemove() + { + var list = new SortedList(Comparer.Create((a, b) => a - b)) + { + 10, + 8, + 13, + -10 + }; + list.Remove(8); + Assert.IsFalse(list.Any(i => i == 8)); + Assert.AreEqual(3, list.Count); + } + + [Test] + public void TestRemoveAt() + { + var list = new SortedList(Comparer.Create((a, b) => a - b)) + { + 10, + 8, + 13, + -10 + }; + list.RemoveAt(0); + Assert.IsFalse(list.Any(i => i == -10)); + Assert.AreEqual(3, list.Count); + } + + [Test] + public void TestClear() + { + var list = new SortedList(Comparer.Create((a, b) => a - b)) + { + 10, + 8, + 13, + -10 + }; + list.Clear(); + Assert.IsFalse(list.Any()); + Assert.AreEqual(0, list.Count); + } + + [Test] + public void TestIndexOf() + { + var list = new SortedList(Comparer.Default) + { + 10, + 10, + 10, + }; + + Assert.IsTrue(list.IndexOf(10) >= 0); + } + } +} diff --git a/osu.Framework.Tests/MathUtils/TestInterpolation.cs b/osu.Framework.Tests/MathUtils/TestInterpolation.cs index d2c7df4ea..703d26a9a 100644 --- a/osu.Framework.Tests/MathUtils/TestInterpolation.cs +++ b/osu.Framework.Tests/MathUtils/TestInterpolation.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.MathUtils; - -namespace osu.Framework.Tests.MathUtils -{ - [TestFixture] - public class TestInterpolation - { - [Test] - public void TestLerp() - { - Assert.AreEqual(5, Interpolation.Lerp(0, 10, 0.5f)); - Assert.AreEqual(0, Interpolation.Lerp(0, 10, 0)); - Assert.AreEqual(10, Interpolation.Lerp(0, 10, 1)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.MathUtils; + +namespace osu.Framework.Tests.MathUtils +{ + [TestFixture] + public class TestInterpolation + { + [Test] + public void TestLerp() + { + Assert.AreEqual(5, Interpolation.Lerp(0, 10, 0.5f)); + Assert.AreEqual(0, Interpolation.Lerp(0, 10, 0)); + Assert.AreEqual(10, Interpolation.Lerp(0, 10, 1)); + } + } +} diff --git a/osu.Framework.Tests/Platform/HeadlessGameHostTest.cs b/osu.Framework.Tests/Platform/HeadlessGameHostTest.cs index 37a8d54ce..703271cac 100644 --- a/osu.Framework.Tests/Platform/HeadlessGameHostTest.cs +++ b/osu.Framework.Tests/Platform/HeadlessGameHostTest.cs @@ -1,51 +1,51 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading; -using NUnit.Framework; -using osu.Framework.Platform; - -namespace osu.Framework.Tests.Platform -{ - [TestFixture] - public class HeadlessGameHostTest - { - private class Foobar - { - public string Bar; - } - - [Test] - public void TestIpc() - { - using (var server = new HeadlessGameHost(@"server", true)) - using (var client = new HeadlessGameHost(@"client", true)) - { - Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind"); - Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to"); - - var serverChannel = new IpcChannel(server); - var clientChannel = new IpcChannel(client); - - Action waitAction = () => - { - bool received = false; - serverChannel.MessageReceived += message => - { - Assert.AreEqual("example", message.Bar); - received = true; - }; - - clientChannel.SendMessageAsync(new Foobar { Bar = "example" }).Wait(); - - while (!received) - Thread.Sleep(1); - }; - - Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(10000), - @"Message was not received in a timely fashion"); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading; +using NUnit.Framework; +using osu.Framework.Platform; + +namespace osu.Framework.Tests.Platform +{ + [TestFixture] + public class HeadlessGameHostTest + { + private class Foobar + { + public string Bar; + } + + [Test] + public void TestIpc() + { + using (var server = new HeadlessGameHost(@"server", true)) + using (var client = new HeadlessGameHost(@"client", true)) + { + Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind"); + Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to"); + + var serverChannel = new IpcChannel(server); + var clientChannel = new IpcChannel(client); + + Action waitAction = () => + { + bool received = false; + serverChannel.MessageReceived += message => + { + Assert.AreEqual("example", message.Bar); + received = true; + }; + + clientChannel.SendMessageAsync(new Foobar { Bar = "example" }).Wait(); + + while (!received) + Thread.Sleep(1); + }; + + Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(10000), + @"Message was not received in a timely fashion"); + } + } + } +} diff --git a/osu.Framework.Tests/Program.cs b/osu.Framework.Tests/Program.cs index c0c2e3480..bc349fc11 100644 --- a/osu.Framework.Tests/Program.cs +++ b/osu.Framework.Tests/Program.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Platform; - -namespace osu.Framework.Tests -{ - public static class Program - { - [STAThread] - public static void Main(string[] args) - { - bool benchmark = args.Length > 0 && args[0] == @"-benchmark"; - - using (GameHost host = Host.GetSuitableHost(@"visual-tests")) - { - if (benchmark) - host.Run(new AutomatedVisualTestGame()); - else - host.Run(new VisualTestGame()); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Platform; + +namespace osu.Framework.Tests +{ + public static class Program + { + [STAThread] + public static void Main(string[] args) + { + bool benchmark = args.Length > 0 && args[0] == @"-benchmark"; + + using (GameHost host = Host.GetSuitableHost(@"visual-tests")) + { + if (benchmark) + host.Run(new AutomatedVisualTestGame()); + else + host.Run(new VisualTestGame()); + } + } + } +} diff --git a/osu.Framework.Tests/TestGame.cs b/osu.Framework.Tests/TestGame.cs index 5377c46ff..f3f1dd63e 100644 --- a/osu.Framework.Tests/TestGame.cs +++ b/osu.Framework.Tests/TestGame.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Reflection; -using osu.Framework.Allocation; -using osu.Framework.IO.Stores; - -namespace osu.Framework.Tests -{ - internal class TestGame : Game - { - [BackgroundDependencyLoader] - private void load() - { - Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(Assembly.GetExecutingAssembly().Location), "Resources")); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Reflection; +using osu.Framework.Allocation; +using osu.Framework.IO.Stores; + +namespace osu.Framework.Tests +{ + internal class TestGame : Game + { + [BackgroundDependencyLoader] + private void load() + { + Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(Assembly.GetExecutingAssembly().Location), "Resources")); + } + } +} diff --git a/osu.Framework.Tests/Visual/FrameworkTestCase.cs b/osu.Framework.Tests/Visual/FrameworkTestCase.cs index 4df57d871..6aac4149a 100644 --- a/osu.Framework.Tests/Visual/FrameworkTestCase.cs +++ b/osu.Framework.Tests/Visual/FrameworkTestCase.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.IO.Stores; -using osu.Framework.Platform; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public abstract class FrameworkTestCase : TestCase - { - public override void RunTest() - { - using (var host = new HeadlessGameHost()) - host.Run(new FrameworkTestCaseTestRunner(this)); - } - - private class FrameworkTestCaseTestRunner : TestCaseTestRunner - { - public FrameworkTestCaseTestRunner(TestCase testCase) - : base(testCase) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(@"osu.Framework.Tests.exe"), "Resources")); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public abstract class FrameworkTestCase : TestCase + { + public override void RunTest() + { + using (var host = new HeadlessGameHost()) + host.Run(new FrameworkTestCaseTestRunner(this)); + } + + private class FrameworkTestCaseTestRunner : TestCaseTestRunner + { + public FrameworkTestCaseTestRunner(TestCase testCase) + : base(testCase) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(@"osu.Framework.Tests.exe"), "Resources")); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseAnimation.cs b/osu.Framework.Tests/Visual/TestCaseAnimation.cs index e3f93e8d8..21f14f6f3 100644 --- a/osu.Framework.Tests/Visual/TestCaseAnimation.cs +++ b/osu.Framework.Tests/Visual/TestCaseAnimation.cs @@ -1,65 +1,65 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Textures; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("frame-based animations")] - public class TestCaseAnimation : TestCase - { - public TestCaseAnimation() - { - DrawableAnimation drawableAnimation; - - Add(new Container - { - Position = new Vector2(10f, 10f), - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new DelayedLoadWrapper(new AvatarAnimation - { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f) - }), - drawableAnimation = new DrawableAnimation - { - RelativePositionAxes = Axes.Both, - Position = new Vector2(0f, 0.3f), - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f) - } - } - }); - - drawableAnimation.AddFrames(new[] - { - new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Red }, 500), - new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Green }, 500), - new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Blue }, 500), - }); - } - - private class AvatarAnimation : TextureAnimation - { - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - AddFrame(textures.Get("https://a.ppy.sh/2"), 500); - AddFrame(textures.Get("https://a.ppy.sh/3"), 500); - AddFrame(textures.Get("https://a.ppy.sh/1876669"), 500); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("frame-based animations")] + public class TestCaseAnimation : TestCase + { + public TestCaseAnimation() + { + DrawableAnimation drawableAnimation; + + Add(new Container + { + Position = new Vector2(10f, 10f), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new DelayedLoadWrapper(new AvatarAnimation + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f) + }), + drawableAnimation = new DrawableAnimation + { + RelativePositionAxes = Axes.Both, + Position = new Vector2(0f, 0.3f), + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f) + } + } + }); + + drawableAnimation.AddFrames(new[] + { + new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Red }, 500), + new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Green }, 500), + new FrameData(new Box { Size = new Vector2(50f), Colour = Color4.Blue }, 500), + }); + } + + private class AvatarAnimation : TextureAnimation + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddFrame(textures.Get("https://a.ppy.sh/2"), 500); + AddFrame(textures.Get("https://a.ppy.sh/3"), 500); + AddFrame(textures.Get("https://a.ppy.sh/1876669"), 500); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseBindableNumbers.cs b/osu.Framework.Tests/Visual/TestCaseBindableNumbers.cs index 13211ebba..a29f13f65 100644 --- a/osu.Framework.Tests/Visual/TestCaseBindableNumbers.cs +++ b/osu.Framework.Tests/Visual/TestCaseBindableNumbers.cs @@ -1,232 +1,232 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Globalization; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseBindableNumbers : TestCase - { - private readonly BindableInt bindableInt = new BindableInt(); - private readonly BindableLong bindableLong = new BindableLong(); - private readonly BindableDouble bindableDouble = new BindableDouble(); - private readonly BindableFloat bindableFloat = new BindableFloat(); - - public TestCaseBindableNumbers() - { - AddStep("Reset", () => - { - setValue(0); - setPrecision(1); - }); - - testBasic(); - testPrecision3(); - testPrecision10(); - testMinMaxWithoutPrecision(); - testMinMaxWithPrecision(); - testInvalidPrecision(); - testFractionalPrecision(); - - AddSliderStep("Min value", -100, 100, -100, setMin); - AddSliderStep("Max value", -100, 100, 100, setMax); - AddSliderStep("Value", -100, 100, 0, setValue); - AddSliderStep("Precision", 1, 10, 1, setPrecision); - - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new BindableDisplayContainer(bindableInt), - new BindableDisplayContainer(bindableLong), - }, - new Drawable[] - { - new BindableDisplayContainer(bindableFloat), - new BindableDisplayContainer(bindableDouble), - } - } - }; - } - - /// - /// Tests basic setting of values. - /// - private void testBasic() - { - AddStep("Value = 10", () => setValue(10)); - AddAssert("Check = 10", () => checkExact(10)); - } - - /// - /// Tests that midpoint values are correctly rounded with a precision of 3. - /// - private void testPrecision3() - { - AddStep("Precision = 3", () => setPrecision(3)); - AddStep("Value = 4", () => setValue(3)); - AddAssert("Check = 3", () => checkExact(3)); - AddStep("Value = 5", () => setValue(5)); - AddAssert("Check = 6", () => checkExact(6)); - AddStep("Value = 59", () => setValue(59)); - AddAssert("Check = 60", () => checkExact(60)); - } - - /// - /// Tests that midpoint values are correctly rounded with a precision of 10. - /// - private void testPrecision10() - { - AddStep("Precision = 10", () => setPrecision(10)); - AddStep("Value = 6", () => setValue(6)); - AddAssert("Check = 10", () => checkExact(10)); - } - - /// - /// Tests that values are correctly clamped to min/max values. - /// - private void testMinMaxWithoutPrecision() - { - AddStep("Precision = 1", () => setPrecision(1)); - AddStep("Min = -30", () => setMin(-30)); - AddStep("Max = 30", () => setMax(30)); - AddStep("Value = -50", () => setValue(-50)); - AddAssert("Check = -30", () => checkExact(-30)); - AddStep("Value = 50", () => setValue(50)); - AddAssert("Check = 30", () => checkExact(30)); - } - - /// - /// Tests that values are correctly clamped to min/max values when precision is involved. - /// In this case, precision is preferred over min/max values. - /// - private void testMinMaxWithPrecision() - { - AddStep("Precision = 5", () => setPrecision(5)); - AddStep("Min = -27", () => setMin(-27)); - AddStep("Max = 27", () => setMax(27)); - AddStep("Value = -30", () => setValue(-30)); - AddAssert("Check = -25", () => checkExact(-25)); - AddStep("Value = 30", () => setValue(30)); - AddAssert("Check = 25", () => checkExact(25)); - } - - /// - /// Tests that invalid precisions cause exceptions. - /// - private void testInvalidPrecision() - { - AddAssert("Precision = 0 throws", () => - { - try - { - setPrecision(0); - return false; - } - catch (Exception) - { - return true; - } - }); - - AddAssert("Precision = -1 throws", () => - { - try - { - setPrecision(-1); - return false; - } - catch (Exception) - { - return true; - } - }); - } - - /// - /// Tests that fractional precisions are obeyed. - /// Note that int bindables are assigned int precisions/values, so their results will differ. - /// - private void testFractionalPrecision() - { - AddStep("Precision = 2.25/2", () => setPrecision(2.25)); - AddStep("Value = 3.3/3", () => setValue(3.3)); - AddAssert("Check = 2.25/4", () => checkExact(2.25m, 4)); - AddStep("Value = 4.17/4", () => setValue(4.17)); - AddAssert("Check = 4.5/4", () => checkExact(4.5m, 4)); - } - - private bool checkExact(decimal value) => checkExact(value, value); - - private bool checkExact(decimal floatValue, decimal intValue) - => bindableInt.Value == Convert.ToInt32(intValue) - && bindableLong.Value == Convert.ToInt64(intValue) - && bindableFloat.Value == Convert.ToSingle(floatValue) - && bindableDouble.Value == Convert.ToDouble(floatValue); - - private void setMin(T value) - { - bindableInt.MinValue = Convert.ToInt32(value); - bindableLong.MinValue = Convert.ToInt64(value); - bindableFloat.MinValue = Convert.ToSingle(value); - bindableDouble.MinValue = Convert.ToDouble(value); - } - - private void setMax(T value) - { - bindableInt.MaxValue = Convert.ToInt32(value); - bindableLong.MaxValue = Convert.ToInt64(value); - bindableFloat.MaxValue = Convert.ToSingle(value); - bindableDouble.MaxValue = Convert.ToDouble(value); - } - - private void setValue(T value) - { - bindableInt.Value = Convert.ToInt32(value); - bindableLong.Value = Convert.ToInt64(value); - bindableFloat.Value = Convert.ToSingle(value); - bindableDouble.Value = Convert.ToDouble(value); - } - - private void setPrecision(T precision) - { - bindableInt.Precision = Convert.ToInt32(precision); - bindableLong.Precision = Convert.ToInt64(precision); - bindableFloat.Precision = Convert.ToSingle(precision); - bindableDouble.Precision = Convert.ToDouble(precision); - } - - private class BindableDisplayContainer : CompositeDrawable - where T : struct, IComparable, IConvertible - { - public BindableDisplayContainer(BindableNumber bindable) - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - SpriteText valueText; - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new SpriteText { Text = $"{typeof(T).Name} value:" }, - valueText = new SpriteText { Text = bindable.Value.ToString(CultureInfo.InvariantCulture) } - } - }; - - bindable.ValueChanged += v => valueText.Text = v.ToString(CultureInfo.InvariantCulture); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Globalization; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseBindableNumbers : TestCase + { + private readonly BindableInt bindableInt = new BindableInt(); + private readonly BindableLong bindableLong = new BindableLong(); + private readonly BindableDouble bindableDouble = new BindableDouble(); + private readonly BindableFloat bindableFloat = new BindableFloat(); + + public TestCaseBindableNumbers() + { + AddStep("Reset", () => + { + setValue(0); + setPrecision(1); + }); + + testBasic(); + testPrecision3(); + testPrecision10(); + testMinMaxWithoutPrecision(); + testMinMaxWithPrecision(); + testInvalidPrecision(); + testFractionalPrecision(); + + AddSliderStep("Min value", -100, 100, -100, setMin); + AddSliderStep("Max value", -100, 100, 100, setMax); + AddSliderStep("Value", -100, 100, 0, setValue); + AddSliderStep("Precision", 1, 10, 1, setPrecision); + + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new BindableDisplayContainer(bindableInt), + new BindableDisplayContainer(bindableLong), + }, + new Drawable[] + { + new BindableDisplayContainer(bindableFloat), + new BindableDisplayContainer(bindableDouble), + } + } + }; + } + + /// + /// Tests basic setting of values. + /// + private void testBasic() + { + AddStep("Value = 10", () => setValue(10)); + AddAssert("Check = 10", () => checkExact(10)); + } + + /// + /// Tests that midpoint values are correctly rounded with a precision of 3. + /// + private void testPrecision3() + { + AddStep("Precision = 3", () => setPrecision(3)); + AddStep("Value = 4", () => setValue(3)); + AddAssert("Check = 3", () => checkExact(3)); + AddStep("Value = 5", () => setValue(5)); + AddAssert("Check = 6", () => checkExact(6)); + AddStep("Value = 59", () => setValue(59)); + AddAssert("Check = 60", () => checkExact(60)); + } + + /// + /// Tests that midpoint values are correctly rounded with a precision of 10. + /// + private void testPrecision10() + { + AddStep("Precision = 10", () => setPrecision(10)); + AddStep("Value = 6", () => setValue(6)); + AddAssert("Check = 10", () => checkExact(10)); + } + + /// + /// Tests that values are correctly clamped to min/max values. + /// + private void testMinMaxWithoutPrecision() + { + AddStep("Precision = 1", () => setPrecision(1)); + AddStep("Min = -30", () => setMin(-30)); + AddStep("Max = 30", () => setMax(30)); + AddStep("Value = -50", () => setValue(-50)); + AddAssert("Check = -30", () => checkExact(-30)); + AddStep("Value = 50", () => setValue(50)); + AddAssert("Check = 30", () => checkExact(30)); + } + + /// + /// Tests that values are correctly clamped to min/max values when precision is involved. + /// In this case, precision is preferred over min/max values. + /// + private void testMinMaxWithPrecision() + { + AddStep("Precision = 5", () => setPrecision(5)); + AddStep("Min = -27", () => setMin(-27)); + AddStep("Max = 27", () => setMax(27)); + AddStep("Value = -30", () => setValue(-30)); + AddAssert("Check = -25", () => checkExact(-25)); + AddStep("Value = 30", () => setValue(30)); + AddAssert("Check = 25", () => checkExact(25)); + } + + /// + /// Tests that invalid precisions cause exceptions. + /// + private void testInvalidPrecision() + { + AddAssert("Precision = 0 throws", () => + { + try + { + setPrecision(0); + return false; + } + catch (Exception) + { + return true; + } + }); + + AddAssert("Precision = -1 throws", () => + { + try + { + setPrecision(-1); + return false; + } + catch (Exception) + { + return true; + } + }); + } + + /// + /// Tests that fractional precisions are obeyed. + /// Note that int bindables are assigned int precisions/values, so their results will differ. + /// + private void testFractionalPrecision() + { + AddStep("Precision = 2.25/2", () => setPrecision(2.25)); + AddStep("Value = 3.3/3", () => setValue(3.3)); + AddAssert("Check = 2.25/4", () => checkExact(2.25m, 4)); + AddStep("Value = 4.17/4", () => setValue(4.17)); + AddAssert("Check = 4.5/4", () => checkExact(4.5m, 4)); + } + + private bool checkExact(decimal value) => checkExact(value, value); + + private bool checkExact(decimal floatValue, decimal intValue) + => bindableInt.Value == Convert.ToInt32(intValue) + && bindableLong.Value == Convert.ToInt64(intValue) + && bindableFloat.Value == Convert.ToSingle(floatValue) + && bindableDouble.Value == Convert.ToDouble(floatValue); + + private void setMin(T value) + { + bindableInt.MinValue = Convert.ToInt32(value); + bindableLong.MinValue = Convert.ToInt64(value); + bindableFloat.MinValue = Convert.ToSingle(value); + bindableDouble.MinValue = Convert.ToDouble(value); + } + + private void setMax(T value) + { + bindableInt.MaxValue = Convert.ToInt32(value); + bindableLong.MaxValue = Convert.ToInt64(value); + bindableFloat.MaxValue = Convert.ToSingle(value); + bindableDouble.MaxValue = Convert.ToDouble(value); + } + + private void setValue(T value) + { + bindableInt.Value = Convert.ToInt32(value); + bindableLong.Value = Convert.ToInt64(value); + bindableFloat.Value = Convert.ToSingle(value); + bindableDouble.Value = Convert.ToDouble(value); + } + + private void setPrecision(T precision) + { + bindableInt.Precision = Convert.ToInt32(precision); + bindableLong.Precision = Convert.ToInt64(precision); + bindableFloat.Precision = Convert.ToSingle(precision); + bindableDouble.Precision = Convert.ToDouble(precision); + } + + private class BindableDisplayContainer : CompositeDrawable + where T : struct, IComparable, IConvertible + { + public BindableDisplayContainer(BindableNumber bindable) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + SpriteText valueText; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SpriteText { Text = $"{typeof(T).Name} value:" }, + valueText = new SpriteText { Text = bindable.Value.ToString(CultureInfo.InvariantCulture) } + } + }; + + bindable.ValueChanged += v => valueText.Text = v.ToString(CultureInfo.InvariantCulture); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseBlending.cs b/osu.Framework.Tests/Visual/TestCaseBlending.cs index 97b9ccc5c..a2d802a1d 100644 --- a/osu.Framework.Tests/Visual/TestCaseBlending.cs +++ b/osu.Framework.Tests/Visual/TestCaseBlending.cs @@ -1,177 +1,177 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseBlending : TestCase - { - private readonly Dropdown colourModeDropdown; - private readonly Dropdown colourEquation; - private readonly Dropdown alphaEquation; - private readonly BufferedContainer foregroundContainer; - - public TestCaseBlending() - { - Children = new Drawable[] - { - new FillFlowContainer - { - Name = "Settings", - AutoSizeAxes = Axes.Both, - Y = 50, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] - { - new SpriteText { Text = "Blending mode" }, - colourModeDropdown = new BasicDropdown { Width = 200 } - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] - { - new SpriteText { Text = "Blending equation (colour)" }, - colourEquation = new BasicDropdown { Width = 200 } - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] - { - new SpriteText { Text = "Blending equation (alpha)" }, - alphaEquation = new BasicDropdown { Width = 200 } - } - } - } - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Behind background" - }, - new BufferedContainer - { - Name = "Background", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Size = new Vector2(0.85f), - Masking = true, - Children = new Drawable[] - { - new GradientPart(0, Color4.Orange, Color4.Yellow), - new GradientPart(1, Color4.Yellow, Color4.Green), - new GradientPart(2, Color4.Green, Color4.Cyan), - new GradientPart(3, Color4.Cyan, Color4.Blue), - new GradientPart(4, Color4.Blue, Color4.Violet), - foregroundContainer = new BufferedContainer - { - Name = "Foreground", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Alpha = 0.8f, - Children = new[] - { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.45f), - Y = -0.15f, - Colour = Color4.Cyan - }, - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.45f), - X = -0.15f, - Colour = Color4.Magenta - }, - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.45f), - X = 0.15f, - Colour = Color4.Yellow - }, - } - }, - } - }, - }; - - colourModeDropdown.Items = Enum.GetNames(typeof(BlendingMode)).Select(n => new KeyValuePair(n, (BlendingMode)Enum.Parse(typeof(BlendingMode), n))); - colourEquation.Items = Enum.GetNames(typeof(BlendingEquation)).Select(n => new KeyValuePair(n, (BlendingEquation)Enum.Parse(typeof(BlendingEquation), n))); - alphaEquation.Items = Enum.GetNames(typeof(BlendingEquation)).Select(n => new KeyValuePair(n, (BlendingEquation)Enum.Parse(typeof(BlendingEquation), n))); - - colourModeDropdown.Current.Value = foregroundContainer.Blending.Mode; - colourEquation.Current.Value = foregroundContainer.Blending.RGBEquation; - alphaEquation.Current.Value = foregroundContainer.Blending.AlphaEquation; - - colourModeDropdown.Current.ValueChanged += v => updateBlending(); - colourEquation.Current.ValueChanged += v => updateBlending(); - alphaEquation.Current.ValueChanged += v => updateBlending(); - } - - private void updateBlending() - { - foregroundContainer.Blending = new BlendingParameters - { - Mode = colourModeDropdown.Current, - RGBEquation = colourEquation.Current, - AlphaEquation = alphaEquation.Current - }; - } - - private class GradientPart : Box - { - public GradientPart(int index, Color4 start, Color4 end) - { - RelativeSizeAxes = Axes.Both; - RelativePositionAxes = Axes.Both; - Width = 1 / 5f; // Assume 5 gradients - X = 1 / 5f * index; - - Colour = ColourInfo.GradientHorizontal(start, end); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseBlending : TestCase + { + private readonly Dropdown colourModeDropdown; + private readonly Dropdown colourEquation; + private readonly Dropdown alphaEquation; + private readonly BufferedContainer foregroundContainer; + + public TestCaseBlending() + { + Children = new Drawable[] + { + new FillFlowContainer + { + Name = "Settings", + AutoSizeAxes = Axes.Both, + Y = 50, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new SpriteText { Text = "Blending mode" }, + colourModeDropdown = new BasicDropdown { Width = 200 } + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new SpriteText { Text = "Blending equation (colour)" }, + colourEquation = new BasicDropdown { Width = 200 } + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new SpriteText { Text = "Blending equation (alpha)" }, + alphaEquation = new BasicDropdown { Width = 200 } + } + } + } + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Behind background" + }, + new BufferedContainer + { + Name = "Background", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Size = new Vector2(0.85f), + Masking = true, + Children = new Drawable[] + { + new GradientPart(0, Color4.Orange, Color4.Yellow), + new GradientPart(1, Color4.Yellow, Color4.Green), + new GradientPart(2, Color4.Green, Color4.Cyan), + new GradientPart(3, Color4.Cyan, Color4.Blue), + new GradientPart(4, Color4.Blue, Color4.Violet), + foregroundContainer = new BufferedContainer + { + Name = "Foreground", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Alpha = 0.8f, + Children = new[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.45f), + Y = -0.15f, + Colour = Color4.Cyan + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.45f), + X = -0.15f, + Colour = Color4.Magenta + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.45f), + X = 0.15f, + Colour = Color4.Yellow + }, + } + }, + } + }, + }; + + colourModeDropdown.Items = Enum.GetNames(typeof(BlendingMode)).Select(n => new KeyValuePair(n, (BlendingMode)Enum.Parse(typeof(BlendingMode), n))); + colourEquation.Items = Enum.GetNames(typeof(BlendingEquation)).Select(n => new KeyValuePair(n, (BlendingEquation)Enum.Parse(typeof(BlendingEquation), n))); + alphaEquation.Items = Enum.GetNames(typeof(BlendingEquation)).Select(n => new KeyValuePair(n, (BlendingEquation)Enum.Parse(typeof(BlendingEquation), n))); + + colourModeDropdown.Current.Value = foregroundContainer.Blending.Mode; + colourEquation.Current.Value = foregroundContainer.Blending.RGBEquation; + alphaEquation.Current.Value = foregroundContainer.Blending.AlphaEquation; + + colourModeDropdown.Current.ValueChanged += v => updateBlending(); + colourEquation.Current.ValueChanged += v => updateBlending(); + alphaEquation.Current.ValueChanged += v => updateBlending(); + } + + private void updateBlending() + { + foregroundContainer.Blending = new BlendingParameters + { + Mode = colourModeDropdown.Current, + RGBEquation = colourEquation.Current, + AlphaEquation = alphaEquation.Current + }; + } + + private class GradientPart : Box + { + public GradientPart(int index, Color4 start, Color4 end) + { + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + Width = 1 / 5f; // Assume 5 gradients + X = 1 / 5f * index; + + Colour = ColourInfo.GradientHorizontal(start, end); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseBufferedContainer.cs b/osu.Framework.Tests/Visual/TestCaseBufferedContainer.cs index 26fe844dc..68ba3e3aa 100644 --- a/osu.Framework.Tests/Visual/TestCaseBufferedContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseBufferedContainer.cs @@ -1,32 +1,32 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseBufferedContainer : TestCaseMasking - { - private readonly BufferedContainer buffer; - - public TestCaseBufferedContainer() - { - Remove(TestContainer); - - Add(buffer = new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] { TestContainer } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - buffer.BlurTo(new Vector2(20), 1000).Then().BlurTo(Vector2.Zero, 1000).Loop(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseBufferedContainer : TestCaseMasking + { + private readonly BufferedContainer buffer; + + public TestCaseBufferedContainer() + { + Remove(TestContainer); + + Add(buffer = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] { TestContainer } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + buffer.BlurTo(new Vector2(20), 1000).Then().BlurTo(Vector2.Zero, 1000).Loop(); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCachedBufferedContainer.cs b/osu.Framework.Tests/Visual/TestCaseCachedBufferedContainer.cs index 36762c343..ff2957ce6 100644 --- a/osu.Framework.Tests/Visual/TestCaseCachedBufferedContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseCachedBufferedContainer.cs @@ -1,168 +1,168 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseCachedBufferedContainer : GridTestCase - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BufferedContainer), - typeof(BufferedContainerDrawNode), - }; - - public TestCaseCachedBufferedContainer() - : base(5, 2) - { - string[] labels = - { - "uncached", - "cached", - "uncached with rotation", - "cached with rotation", - "uncached with movement", - "cached with movement", - "uncached with parent scale", - "cached with parent scale", - "uncached with parent scale&fade", - "cached with parent scale&fade", - }; - - var boxes = new List(); - - for (int i = 0; i < Rows * Cols; ++i) - { - ContainingBox box; - - Cell(i).AddRange(new Drawable[] - { - new SpriteText - { - Text = labels[i], - TextSize = 20, - }, - box = new ContainingBox(i >= 6, i >= 8) - { - Child = new CountingBox(i == 2 || i == 3, i == 4 || i == 5) - { - CacheDrawnFrameBuffer = i % 2 == 1, - }, - } - }); - - boxes.Add(box); - } - - AddWaitStep(5); - - // ensure uncached is always updating children. - AddAssert("box 0 count > 0", () => boxes[0].Count > 0); - AddAssert("even box counts equal", () => - boxes[0].Count == boxes[2].Count && - boxes[2].Count == boxes[4].Count && - boxes[4].Count == boxes[6].Count); - - // ensure cached is never updating children. - AddAssert("box 1 count is 1", () => boxes[1].Count == 1); - - // ensure rotation changes are invalidating cache (for now). - AddAssert("box 2 count > 0", () => boxes[2].Count > 0); - AddAssert("box 2 count equals box 3 count", () => boxes[2].Count == boxes[3].Count); - - // ensure cached with only translation is never updating children. - AddAssert("box 5 count is 1", () => boxes[1].Count == 1); - - // ensure a parent scaling is invalidating cache. - AddAssert("box 5 count equals box 6 count", () => boxes[5].Count == boxes[6].Count); - - // ensure we don't break on colour invalidations (due to blanket invalidation logic in Drawable.Invalidate). - AddAssert("box 7 count equals box 8 count", () => boxes[7].Count == boxes[8].Count); - } - - private class ContainingBox : Container - { - private readonly bool scaling; - private readonly bool fading; - - public ContainingBox(bool scaling, bool fading) - { - this.scaling = scaling; - this.fading = fading; - - RelativeSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - if (scaling) this.ScaleTo(1.2f, 1000).Then().ScaleTo(1, 1000).Loop(); - if (fading) this.FadeTo(0.5f, 1000).Then().FadeTo(1, 1000).Loop(); - } - } - - private class CountingBox : BufferedContainer - { - private readonly bool rotating; - private readonly bool moving; - private readonly SpriteText count; - public new int Count; - - public CountingBox(bool rotating = false, bool moving = false) - { - this.rotating = rotating; - this.moving = moving; - RelativeSizeAxes = Axes.Both; - Origin = Anchor.Centre; - Anchor = Anchor.Centre; - - Scale = new Vector2(0.5f); - - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.NavajoWhite, - }, - count = new SpriteText - { - Colour = Color4.Black, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - TextSize = 80 - } - }; - } - - protected override void Update() - { - base.Update(); - - if (RequiresChildrenUpdate) - { - Count++; - count.Text = Count.ToString(); - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - if (rotating) this.RotateTo(360, 1000).Loop(); - if (moving) this.MoveTo(new Vector2(100, 0), 2000, Easing.InOutSine).Then().MoveTo(new Vector2(0, 0), 2000, Easing.InOutSine).Loop(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseCachedBufferedContainer : GridTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(BufferedContainer), + typeof(BufferedContainerDrawNode), + }; + + public TestCaseCachedBufferedContainer() + : base(5, 2) + { + string[] labels = + { + "uncached", + "cached", + "uncached with rotation", + "cached with rotation", + "uncached with movement", + "cached with movement", + "uncached with parent scale", + "cached with parent scale", + "uncached with parent scale&fade", + "cached with parent scale&fade", + }; + + var boxes = new List(); + + for (int i = 0; i < Rows * Cols; ++i) + { + ContainingBox box; + + Cell(i).AddRange(new Drawable[] + { + new SpriteText + { + Text = labels[i], + TextSize = 20, + }, + box = new ContainingBox(i >= 6, i >= 8) + { + Child = new CountingBox(i == 2 || i == 3, i == 4 || i == 5) + { + CacheDrawnFrameBuffer = i % 2 == 1, + }, + } + }); + + boxes.Add(box); + } + + AddWaitStep(5); + + // ensure uncached is always updating children. + AddAssert("box 0 count > 0", () => boxes[0].Count > 0); + AddAssert("even box counts equal", () => + boxes[0].Count == boxes[2].Count && + boxes[2].Count == boxes[4].Count && + boxes[4].Count == boxes[6].Count); + + // ensure cached is never updating children. + AddAssert("box 1 count is 1", () => boxes[1].Count == 1); + + // ensure rotation changes are invalidating cache (for now). + AddAssert("box 2 count > 0", () => boxes[2].Count > 0); + AddAssert("box 2 count equals box 3 count", () => boxes[2].Count == boxes[3].Count); + + // ensure cached with only translation is never updating children. + AddAssert("box 5 count is 1", () => boxes[1].Count == 1); + + // ensure a parent scaling is invalidating cache. + AddAssert("box 5 count equals box 6 count", () => boxes[5].Count == boxes[6].Count); + + // ensure we don't break on colour invalidations (due to blanket invalidation logic in Drawable.Invalidate). + AddAssert("box 7 count equals box 8 count", () => boxes[7].Count == boxes[8].Count); + } + + private class ContainingBox : Container + { + private readonly bool scaling; + private readonly bool fading; + + public ContainingBox(bool scaling, bool fading) + { + this.scaling = scaling; + this.fading = fading; + + RelativeSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + if (scaling) this.ScaleTo(1.2f, 1000).Then().ScaleTo(1, 1000).Loop(); + if (fading) this.FadeTo(0.5f, 1000).Then().FadeTo(1, 1000).Loop(); + } + } + + private class CountingBox : BufferedContainer + { + private readonly bool rotating; + private readonly bool moving; + private readonly SpriteText count; + public new int Count; + + public CountingBox(bool rotating = false, bool moving = false) + { + this.rotating = rotating; + this.moving = moving; + RelativeSizeAxes = Axes.Both; + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + + Scale = new Vector2(0.5f); + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.NavajoWhite, + }, + count = new SpriteText + { + Colour = Color4.Black, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + TextSize = 80 + } + }; + } + + protected override void Update() + { + base.Update(); + + if (RequiresChildrenUpdate) + { + Count++; + count.Text = Count.ToString(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + if (rotating) this.RotateTo(360, 1000).Loop(); + if (moving) this.MoveTo(new Vector2(100, 0), 2000, Easing.InOutSine).Then().MoveTo(new Vector2(0, 0), 2000, Easing.InOutSine).Loop(); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCheckboxes.cs b/osu.Framework.Tests/Visual/TestCaseCheckboxes.cs index 5b83a6354..a0bb1bf34 100644 --- a/osu.Framework.Tests/Visual/TestCaseCheckboxes.cs +++ b/osu.Framework.Tests/Visual/TestCaseCheckboxes.cs @@ -1,54 +1,54 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseCheckboxes : TestCase - { - public TestCaseCheckboxes() - { - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Padding = new MarginPadding(10), - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new BasicCheckbox - { - LabelText = @"Basic Test" - }, - new BasicCheckbox - { - LabelText = @"FadeDuration Test", - FadeDuration = 300 - }, - new ActionsTestCheckbox - { - LabelText = @"Enabled/Disabled Actions Test", - }, - } - } - }; - } - } - - public class ActionsTestCheckbox : BasicCheckbox - { - public ActionsTestCheckbox() - { - Current.ValueChanged += v => this.RotateTo(v ? 45 : 0, 100); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseCheckboxes : TestCase + { + public TestCaseCheckboxes() + { + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Padding = new MarginPadding(10), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new BasicCheckbox + { + LabelText = @"Basic Test" + }, + new BasicCheckbox + { + LabelText = @"FadeDuration Test", + FadeDuration = 300 + }, + new ActionsTestCheckbox + { + LabelText = @"Enabled/Disabled Actions Test", + }, + } + } + }; + } + } + + public class ActionsTestCheckbox : BasicCheckbox + { + public ActionsTestCheckbox() + { + Current.ValueChanged += v => this.RotateTo(v ? 45 : 0, 100); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCircularContainer.cs b/osu.Framework.Tests/Visual/TestCaseCircularContainer.cs index ebd768773..10d5a370a 100644 --- a/osu.Framework.Tests/Visual/TestCaseCircularContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseCircularContainer.cs @@ -1,53 +1,53 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using OpenTK; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.MathUtils; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description(@"Checking for bugged corner radius")] - public class TestCaseCircularContainer : TestCase - { - private SingleUpdateCircularContainer container; - - public TestCaseCircularContainer() - { - AddStep("128x128 box", () => addContainer(new Vector2(128))); - AddAssert("Expect CornerRadius = 64", () => Precision.AlmostEquals(container.CornerRadius, 64)); - AddStep("128x64 box", () => addContainer(new Vector2(128, 64))); - AddAssert("Expect CornerRadius = 32", () => Precision.AlmostEquals(container.CornerRadius, 32)); - AddStep("64x128 box", () => addContainer(new Vector2(64, 128))); - AddAssert("Expect CornerRadius = 32", () => Precision.AlmostEquals(container.CornerRadius, 32)); - } - - private void addContainer(Vector2 size) - { - Clear(); - Add(container = new SingleUpdateCircularContainer - { - Masking = true, - AutoSizeAxes = Axes.Both, - Child = new Box { Size = size } - }); - } - - private class SingleUpdateCircularContainer : CircularContainer - { - private bool firstUpdate = true; - - public override bool UpdateSubTree() - { - if (!firstUpdate) - return true; - firstUpdate = false; - - return base.UpdateSubTree(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description(@"Checking for bugged corner radius")] + public class TestCaseCircularContainer : TestCase + { + private SingleUpdateCircularContainer container; + + public TestCaseCircularContainer() + { + AddStep("128x128 box", () => addContainer(new Vector2(128))); + AddAssert("Expect CornerRadius = 64", () => Precision.AlmostEquals(container.CornerRadius, 64)); + AddStep("128x64 box", () => addContainer(new Vector2(128, 64))); + AddAssert("Expect CornerRadius = 32", () => Precision.AlmostEquals(container.CornerRadius, 32)); + AddStep("64x128 box", () => addContainer(new Vector2(64, 128))); + AddAssert("Expect CornerRadius = 32", () => Precision.AlmostEquals(container.CornerRadius, 32)); + } + + private void addContainer(Vector2 size) + { + Clear(); + Add(container = new SingleUpdateCircularContainer + { + Masking = true, + AutoSizeAxes = Axes.Both, + Child = new Box { Size = size } + }); + } + + private class SingleUpdateCircularContainer : CircularContainer + { + private bool firstUpdate = true; + + public override bool UpdateSubTree() + { + if (!firstUpdate) + return true; + firstUpdate = false; + + return base.UpdateSubTree(); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCircularProgress.cs b/osu.Framework.Tests/Visual/TestCaseCircularProgress.cs index 49420f3af..f4ddad40e 100644 --- a/osu.Framework.Tests/Visual/TestCaseCircularProgress.cs +++ b/osu.Framework.Tests/Visual/TestCaseCircularProgress.cs @@ -1,191 +1,191 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseCircularProgress : TestCase - { - public override IReadOnlyList RequiredTypes => new[] { typeof(CircularProgress), typeof(CircularProgressDrawNode), typeof(CircularProgressDrawNodeSharedData) }; - - private readonly CircularProgress clock; - - private int rotateMode; - private const double period = 4000; - private const double transition_period = 2000; - - private readonly Texture gradientTextureHorizontal; - private readonly Texture gradientTextureVertical; - private readonly Texture gradientTextureBoth; - - public TestCaseCircularProgress() - { - const int width = 128; - byte[] data = new byte[width * 4]; - - gradientTextureHorizontal = new Texture(width, 1, true); - for (int i = 0; i < width; ++i) - { - float brightness = (float)i / (width - 1); - int index = i * 4; - data[index + 0] = (byte)(128 + (1 - brightness) * 127); - data[index + 1] = (byte)(128 + brightness * 127); - data[index + 2] = 128; - data[index + 3] = 255; - } - gradientTextureHorizontal.SetData(new TextureUpload(data)); - - gradientTextureVertical = new Texture(1, width, true); - for (int i = 0; i < width; ++i) - { - float brightness = (float)i / (width - 1); - int index = i * 4; - data[index + 0] = (byte)(128 + (1 - brightness) * 127); - data[index + 1] = (byte)(128 + brightness * 127); - data[index + 2] = 128; - data[index + 3] = 255; - } - gradientTextureVertical.SetData(new TextureUpload(data)); - - byte[] data2 = new byte[width * width * 4]; - gradientTextureBoth = new Texture(width, width, true); - for (int i = 0; i < width; ++i) - { - for (int j = 0; j < width; ++j) - { - float brightness = (float)i / (width - 1); - float brightness2 = (float)j / (width - 1); - int index = i * 4 * width + j * 4; - data2[index + 0] = (byte)(128 + (1 + brightness - brightness2) / 2 * 127); - data2[index + 1] = (byte)(128 + (1 + brightness2 - brightness) / 2 * 127); - data2[index + 2] = (byte)(128 + (brightness + brightness2) / 2 * 127); - data2[index + 3] = 255; - } - } - gradientTextureBoth.SetData(new TextureUpload(data2)); - - - Children = new Drawable[] - { - clock = new CircularProgress - { - Width = 0.8f, - Height = 0.8f, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - }; - - AddStep("Forward", delegate { rotateMode = 1; }); - AddStep("Backward", delegate { rotateMode = 2; }); - AddStep("Transition Focus", delegate { rotateMode = 3; }); - AddStep("Transition Focus 2", delegate { rotateMode = 4; }); - AddStep("Forward/Backward", delegate { rotateMode = 0; }); - - AddStep("Horizontal Gradient Texture", delegate { setTexture(1); }); - AddStep("Vertical Gradient Texture", delegate { setTexture(2); }); - AddStep("2D Graident Texture", delegate { setTexture(3); }); - AddStep("White Texture", delegate { setTexture(0); }); - - AddStep("Red Colour", delegate { setColour(1); }); - AddStep("Horzontal Gradient Colour", delegate { setColour(2); }); - AddStep("Vertical Gradient Colour", delegate { setColour(3); }); - AddStep("2D Gradient Colour", delegate { setColour(4); }); - AddStep("White Colour", delegate { setColour(0); }); - - AddSliderStep("Fill", 0, 10, 10, fill => clock.InnerRadius = fill / 10f); - } - - protected override void Update() - { - base.Update(); - switch (rotateMode) - { - case 0: - clock.Current.Value = Time.Current % (period * 2) / period - 1; - break; - case 1: - clock.Current.Value = Time.Current % period / period; - break; - case 2: - clock.Current.Value = Time.Current % period / period - 1; - break; - case 3: - clock.Current.Value = Time.Current % transition_period / transition_period / 5 - 0.1f; - break; - case 4: - clock.Current.Value = (Time.Current % transition_period / transition_period / 5 - 0.1f + 2) % 2 - 1; - break; - } - } - - private void setTexture(int textureMode) - { - switch (textureMode) - { - case 0: - clock.Texture = Texture.WhitePixel; - break; - case 1: - clock.Texture = gradientTextureHorizontal; - break; - case 2: - clock.Texture = gradientTextureVertical; - break; - case 3: - clock.Texture = gradientTextureBoth; - break; - } - } - - private void setColour(int colourMode) - { - switch (colourMode) - { - case 0: - clock.Colour = new Color4(255, 255, 255, 255); - break; - case 1: - clock.Colour = new Color4(255, 128, 128, 255); - break; - case 2: - clock.Colour = new ColourInfo - { - TopLeft = new Color4(255, 128, 128, 255), - TopRight = new Color4(128, 255, 128, 255), - BottomLeft = new Color4(255, 128, 128, 255), - BottomRight = new Color4(128, 255, 128, 255), - }; - break; - case 3: - clock.Colour = new ColourInfo - { - TopLeft = new Color4(255, 128, 128, 255), - TopRight = new Color4(255, 128, 128, 255), - BottomLeft = new Color4(128, 255, 128, 255), - BottomRight = new Color4(128, 255, 128, 255), - }; - break; - case 4: - clock.Colour = new ColourInfo - { - TopLeft = new Color4(255, 128, 128, 255), - TopRight = new Color4(128, 255, 128, 255), - BottomLeft = new Color4(128, 128, 255, 255), - BottomRight = new Color4(255, 255, 255, 255), - }; - break; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseCircularProgress : TestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(CircularProgress), typeof(CircularProgressDrawNode), typeof(CircularProgressDrawNodeSharedData) }; + + private readonly CircularProgress clock; + + private int rotateMode; + private const double period = 4000; + private const double transition_period = 2000; + + private readonly Texture gradientTextureHorizontal; + private readonly Texture gradientTextureVertical; + private readonly Texture gradientTextureBoth; + + public TestCaseCircularProgress() + { + const int width = 128; + byte[] data = new byte[width * 4]; + + gradientTextureHorizontal = new Texture(width, 1, true); + for (int i = 0; i < width; ++i) + { + float brightness = (float)i / (width - 1); + int index = i * 4; + data[index + 0] = (byte)(128 + (1 - brightness) * 127); + data[index + 1] = (byte)(128 + brightness * 127); + data[index + 2] = 128; + data[index + 3] = 255; + } + gradientTextureHorizontal.SetData(new TextureUpload(data)); + + gradientTextureVertical = new Texture(1, width, true); + for (int i = 0; i < width; ++i) + { + float brightness = (float)i / (width - 1); + int index = i * 4; + data[index + 0] = (byte)(128 + (1 - brightness) * 127); + data[index + 1] = (byte)(128 + brightness * 127); + data[index + 2] = 128; + data[index + 3] = 255; + } + gradientTextureVertical.SetData(new TextureUpload(data)); + + byte[] data2 = new byte[width * width * 4]; + gradientTextureBoth = new Texture(width, width, true); + for (int i = 0; i < width; ++i) + { + for (int j = 0; j < width; ++j) + { + float brightness = (float)i / (width - 1); + float brightness2 = (float)j / (width - 1); + int index = i * 4 * width + j * 4; + data2[index + 0] = (byte)(128 + (1 + brightness - brightness2) / 2 * 127); + data2[index + 1] = (byte)(128 + (1 + brightness2 - brightness) / 2 * 127); + data2[index + 2] = (byte)(128 + (brightness + brightness2) / 2 * 127); + data2[index + 3] = 255; + } + } + gradientTextureBoth.SetData(new TextureUpload(data2)); + + + Children = new Drawable[] + { + clock = new CircularProgress + { + Width = 0.8f, + Height = 0.8f, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + + AddStep("Forward", delegate { rotateMode = 1; }); + AddStep("Backward", delegate { rotateMode = 2; }); + AddStep("Transition Focus", delegate { rotateMode = 3; }); + AddStep("Transition Focus 2", delegate { rotateMode = 4; }); + AddStep("Forward/Backward", delegate { rotateMode = 0; }); + + AddStep("Horizontal Gradient Texture", delegate { setTexture(1); }); + AddStep("Vertical Gradient Texture", delegate { setTexture(2); }); + AddStep("2D Graident Texture", delegate { setTexture(3); }); + AddStep("White Texture", delegate { setTexture(0); }); + + AddStep("Red Colour", delegate { setColour(1); }); + AddStep("Horzontal Gradient Colour", delegate { setColour(2); }); + AddStep("Vertical Gradient Colour", delegate { setColour(3); }); + AddStep("2D Gradient Colour", delegate { setColour(4); }); + AddStep("White Colour", delegate { setColour(0); }); + + AddSliderStep("Fill", 0, 10, 10, fill => clock.InnerRadius = fill / 10f); + } + + protected override void Update() + { + base.Update(); + switch (rotateMode) + { + case 0: + clock.Current.Value = Time.Current % (period * 2) / period - 1; + break; + case 1: + clock.Current.Value = Time.Current % period / period; + break; + case 2: + clock.Current.Value = Time.Current % period / period - 1; + break; + case 3: + clock.Current.Value = Time.Current % transition_period / transition_period / 5 - 0.1f; + break; + case 4: + clock.Current.Value = (Time.Current % transition_period / transition_period / 5 - 0.1f + 2) % 2 - 1; + break; + } + } + + private void setTexture(int textureMode) + { + switch (textureMode) + { + case 0: + clock.Texture = Texture.WhitePixel; + break; + case 1: + clock.Texture = gradientTextureHorizontal; + break; + case 2: + clock.Texture = gradientTextureVertical; + break; + case 3: + clock.Texture = gradientTextureBoth; + break; + } + } + + private void setColour(int colourMode) + { + switch (colourMode) + { + case 0: + clock.Colour = new Color4(255, 255, 255, 255); + break; + case 1: + clock.Colour = new Color4(255, 128, 128, 255); + break; + case 2: + clock.Colour = new ColourInfo + { + TopLeft = new Color4(255, 128, 128, 255), + TopRight = new Color4(128, 255, 128, 255), + BottomLeft = new Color4(255, 128, 128, 255), + BottomRight = new Color4(128, 255, 128, 255), + }; + break; + case 3: + clock.Colour = new ColourInfo + { + TopLeft = new Color4(255, 128, 128, 255), + TopRight = new Color4(255, 128, 128, 255), + BottomLeft = new Color4(128, 255, 128, 255), + BottomRight = new Color4(128, 255, 128, 255), + }; + break; + case 4: + clock.Colour = new ColourInfo + { + TopLeft = new Color4(255, 128, 128, 255), + TopRight = new Color4(128, 255, 128, 255), + BottomLeft = new Color4(128, 128, 255, 255), + BottomRight = new Color4(255, 255, 255, 255), + }; + break; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseColourGradient.cs b/osu.Framework.Tests/Visual/TestCaseColourGradient.cs index 5639dda6c..f28ddf08a 100644 --- a/osu.Framework.Tests/Visual/TestCaseColourGradient.cs +++ b/osu.Framework.Tests/Visual/TestCaseColourGradient.cs @@ -1,92 +1,92 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseColourGradient : GridTestCase - { - public TestCaseColourGradient() : base(2, 2) - { - Color4 transparentBlack = new Color4(0, 0, 0, 0); - - ColourInfo[] colours = - { - new ColourInfo - { - TopLeft = Color4.White, - BottomLeft = Color4.Blue, - TopRight = Color4.Red, - BottomRight = Color4.Green, - }, - new ColourInfo - { - TopLeft = Color4.White, - BottomLeft = Color4.White, - TopRight = Color4.Black, - BottomRight = Color4.Black, - }, - new ColourInfo - { - TopLeft = Color4.White, - BottomLeft = Color4.White, - TopRight = Color4.Transparent, - BottomRight = Color4.Transparent, - }, - new ColourInfo - { - TopLeft = Color4.White, - BottomLeft = Color4.White, - TopRight = transparentBlack, - BottomRight = transparentBlack, - }, - }; - - string[] labels = - { - "Colours", - "White to black (linear brightness gradient)", - "White to transparent white (sRGB brightness gradient)", - "White to transparent black (mixed brightness gradient)", - }; - - for (int i = 0; i < Rows * Cols; ++i) - { - Cell(i).AddRange(new Drawable[] - { - new SpriteText - { - Text = labels[i], - TextSize = 20, - Colour = colours[0], - }, - boxes[i] = new Box - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.5f), - Colour = colours[i], - }, - }); - } - } - - private readonly Box[] boxes = new Box[4]; - - protected override void Update() - { - base.Update(); - - foreach (Box box in boxes) - box.Rotation += 0.01f; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseColourGradient : GridTestCase + { + public TestCaseColourGradient() : base(2, 2) + { + Color4 transparentBlack = new Color4(0, 0, 0, 0); + + ColourInfo[] colours = + { + new ColourInfo + { + TopLeft = Color4.White, + BottomLeft = Color4.Blue, + TopRight = Color4.Red, + BottomRight = Color4.Green, + }, + new ColourInfo + { + TopLeft = Color4.White, + BottomLeft = Color4.White, + TopRight = Color4.Black, + BottomRight = Color4.Black, + }, + new ColourInfo + { + TopLeft = Color4.White, + BottomLeft = Color4.White, + TopRight = Color4.Transparent, + BottomRight = Color4.Transparent, + }, + new ColourInfo + { + TopLeft = Color4.White, + BottomLeft = Color4.White, + TopRight = transparentBlack, + BottomRight = transparentBlack, + }, + }; + + string[] labels = + { + "Colours", + "White to black (linear brightness gradient)", + "White to transparent white (sRGB brightness gradient)", + "White to transparent black (mixed brightness gradient)", + }; + + for (int i = 0; i < Rows * Cols; ++i) + { + Cell(i).AddRange(new Drawable[] + { + new SpriteText + { + Text = labels[i], + TextSize = 20, + Colour = colours[0], + }, + boxes[i] = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f), + Colour = colours[i], + }, + }); + } + } + + private readonly Box[] boxes = new Box[4]; + + protected override void Update() + { + base.Update(); + + foreach (Box box in boxes) + box.Rotation += 0.01f; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseContainerState.cs b/osu.Framework.Tests/Visual/TestCaseContainerState.cs index 735661cbc..cb0edb5c0 100644 --- a/osu.Framework.Tests/Visual/TestCaseContainerState.cs +++ b/osu.Framework.Tests/Visual/TestCaseContainerState.cs @@ -1,123 +1,123 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("ensure valid container state in various scenarios")] - public class TestCaseContainerState : TestCase - { - private readonly Container container; - - public TestCaseContainerState() - { - Add(container = new Container()); - } - - [BackgroundDependencyLoader] - private void load() - { - testLoadedMultipleAdds(); - } - - /// - /// Tests if a drawable can be added to a container, removed, and then re-added to the same container. - /// - [Test] - public void TestPreLoadReAdding() - { - var sprite = new Sprite(); - - // Add - Assert.DoesNotThrow(() => container.Add(sprite)); - Assert.IsTrue(container.Contains(sprite)); - - // Remove - Assert.DoesNotThrow(() => container.Remove(sprite)); - Assert.IsFalse(container.Contains(sprite)); - - // Re-add - Assert.DoesNotThrow(() => container.Add(sprite)); - Assert.IsTrue(container.Contains(sprite)); - } - - /// - /// Tests whether adding a child to multiple containers by abusing - /// results in a . - /// - [Test] - public void TestPreLoadMultipleAdds() - { - // Non-async - Assert.Throws(() => - { - container.Add(new Container - { - // Container is an IReadOnlyList, so Children can accept a Container. - // This further means that CompositeDrawable.AddInternal will try to add all of - // the children of the Container that was set to Children, which should throw an exception - Children = new Container { Child = new Container() } - }); - }); - } - - /// - /// The same as however instead runs after the container is loaded. - /// - private void testLoadedMultipleAdds() - { - AddAssert("Test loaded multiple adds", () => - { - try - { - container.Add(new Container - { - // Container is an IReadOnlyList, so Children can accept a Container. - // This further means that CompositeDrawable.AddInternal will try to add all of - // the children of the Container that was set to Children, which should throw an exception - Children = new Container { Child = new Container() } - }); - - return false; - } - catch (InvalidOperationException) - { - return true; - } - }); - } - - /// - /// Tests whether the result of a operation is valid between multiple containers. - /// This tests whether the comparator + equality operation in is valid. - /// - [Test] - public void TestContainerContains() - { - var drawableA = new Sprite(); - var drawableB = new Sprite(); - var containerA = new Container { Child = drawableA }; - var containerB = new Container { Child = drawableB }; - - var newContainer = new Container { Children = new[] { containerA, containerB } }; - - // Because drawableA and drawableB have been added to separate containers, - // they will both have Depth = 0 and ChildID = 1, which leads to edge cases if a - // sorting comparer that doesn't compare references is used for Contains(). - // If this is not handled properly, it may have devastating effects in, e.g. Remove(). - - Assert.IsTrue(newContainer.First(c => c.Contains(drawableA)) == containerA); - Assert.IsTrue(newContainer.First(c => c.Contains(drawableB)) == containerB); - - Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableA)).Remove(drawableA)); - Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableB)).Remove(drawableB)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("ensure valid container state in various scenarios")] + public class TestCaseContainerState : TestCase + { + private readonly Container container; + + public TestCaseContainerState() + { + Add(container = new Container()); + } + + [BackgroundDependencyLoader] + private void load() + { + testLoadedMultipleAdds(); + } + + /// + /// Tests if a drawable can be added to a container, removed, and then re-added to the same container. + /// + [Test] + public void TestPreLoadReAdding() + { + var sprite = new Sprite(); + + // Add + Assert.DoesNotThrow(() => container.Add(sprite)); + Assert.IsTrue(container.Contains(sprite)); + + // Remove + Assert.DoesNotThrow(() => container.Remove(sprite)); + Assert.IsFalse(container.Contains(sprite)); + + // Re-add + Assert.DoesNotThrow(() => container.Add(sprite)); + Assert.IsTrue(container.Contains(sprite)); + } + + /// + /// Tests whether adding a child to multiple containers by abusing + /// results in a . + /// + [Test] + public void TestPreLoadMultipleAdds() + { + // Non-async + Assert.Throws(() => + { + container.Add(new Container + { + // Container is an IReadOnlyList, so Children can accept a Container. + // This further means that CompositeDrawable.AddInternal will try to add all of + // the children of the Container that was set to Children, which should throw an exception + Children = new Container { Child = new Container() } + }); + }); + } + + /// + /// The same as however instead runs after the container is loaded. + /// + private void testLoadedMultipleAdds() + { + AddAssert("Test loaded multiple adds", () => + { + try + { + container.Add(new Container + { + // Container is an IReadOnlyList, so Children can accept a Container. + // This further means that CompositeDrawable.AddInternal will try to add all of + // the children of the Container that was set to Children, which should throw an exception + Children = new Container { Child = new Container() } + }); + + return false; + } + catch (InvalidOperationException) + { + return true; + } + }); + } + + /// + /// Tests whether the result of a operation is valid between multiple containers. + /// This tests whether the comparator + equality operation in is valid. + /// + [Test] + public void TestContainerContains() + { + var drawableA = new Sprite(); + var drawableB = new Sprite(); + var containerA = new Container { Child = drawableA }; + var containerB = new Container { Child = drawableB }; + + var newContainer = new Container { Children = new[] { containerA, containerB } }; + + // Because drawableA and drawableB have been added to separate containers, + // they will both have Depth = 0 and ChildID = 1, which leads to edge cases if a + // sorting comparer that doesn't compare references is used for Contains(). + // If this is not handled properly, it may have devastating effects in, e.g. Remove(). + + Assert.IsTrue(newContainer.First(c => c.Contains(drawableA)) == containerA); + Assert.IsTrue(newContainer.First(c => c.Contains(drawableB)) == containerB); + + Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableA)).Remove(drawableA)); + Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableB)).Remove(drawableB)); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseContextMenu.cs b/osu.Framework.Tests/Visual/TestCaseContextMenu.cs index e8e2eb340..cc9fe6a2e 100644 --- a/osu.Framework.Tests/Visual/TestCaseContextMenu.cs +++ b/osu.Framework.Tests/Visual/TestCaseContextMenu.cs @@ -1,79 +1,79 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseContextMenu : TestCase - { - private const int start_time = 0; - private const int duration = 1000; - - private readonly ContextMenuBox movingBox; - - private ContextMenuBox makeBox(Anchor anchor) - { - return new ContextMenuBox - { - Size = new Vector2(200), - Anchor = anchor, - Origin = anchor, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Blue, - } - } - }; - } - - public TestCaseContextMenu() - { - Add(new ContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - makeBox(Anchor.TopLeft), - makeBox(Anchor.TopRight), - makeBox(Anchor.BottomLeft), - makeBox(Anchor.BottomRight), - movingBox = makeBox(Anchor.Centre), - } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // Move box along a square trajectory - movingBox.MoveTo(new Vector2(0, 100), duration) - .Then().MoveTo(new Vector2(100, 100), duration) - .Then().MoveTo(new Vector2(100, 0), duration) - .Then().MoveTo(Vector2.Zero, duration) - .Loop(); - } - - private class ContextMenuBox : Container, IHasContextMenu - { - public MenuItem[] ContextMenuItems => new[] - { - new MenuItem(@"Change width", () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), - new MenuItem(@"Change height", () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), - new MenuItem(@"Change width back", () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), - new MenuItem(@"Change height back", () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), - }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseContextMenu : TestCase + { + private const int start_time = 0; + private const int duration = 1000; + + private readonly ContextMenuBox movingBox; + + private ContextMenuBox makeBox(Anchor anchor) + { + return new ContextMenuBox + { + Size = new Vector2(200), + Anchor = anchor, + Origin = anchor, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Blue, + } + } + }; + } + + public TestCaseContextMenu() + { + Add(new ContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + makeBox(Anchor.TopLeft), + makeBox(Anchor.TopRight), + makeBox(Anchor.BottomLeft), + makeBox(Anchor.BottomRight), + movingBox = makeBox(Anchor.Centre), + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Move box along a square trajectory + movingBox.MoveTo(new Vector2(0, 100), duration) + .Then().MoveTo(new Vector2(100, 100), duration) + .Then().MoveTo(new Vector2(100, 0), duration) + .Then().MoveTo(Vector2.Zero, duration) + .Loop(); + } + + private class ContextMenuBox : Container, IHasContextMenu + { + public MenuItem[] ContextMenuItems => new[] + { + new MenuItem(@"Change width", () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), + new MenuItem(@"Change height", () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), + new MenuItem(@"Change width back", () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), + new MenuItem(@"Change height back", () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), + }; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCoordinateSpaces.cs b/osu.Framework.Tests/Visual/TestCaseCoordinateSpaces.cs index d756bb80d..4eb5b5e80 100644 --- a/osu.Framework.Tests/Visual/TestCaseCoordinateSpaces.cs +++ b/osu.Framework.Tests/Visual/TestCaseCoordinateSpaces.cs @@ -1,219 +1,219 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Globalization; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseCoordinateSpaces : TestCase - { - public TestCaseCoordinateSpaces() - { - AddStep("0-1 space", () => loadCase(0)); - AddStep("0-150 space", () => loadCase(1)); - AddStep("50-200 space", () => loadCase(2)); - AddStep("150-(-50) space", () => loadCase(3)); - AddStep("0-300 space", () => loadCase(4)); - AddStep("-250-250 space", () => loadCase(5)); - } - - private void loadCase(int i) - { - Clear(); - - HorizontalVisualiser h; - Add(h = new HorizontalVisualiser - { - Size = new Vector2(200, 50), - X = 150 - }); - - switch (i) - { - case 0: - h.CreateMarkerAt(-0.1f); - h.CreateMarkerAt(0); - h.CreateMarkerAt(0.1f); - h.CreateMarkerAt(0.3f); - h.CreateMarkerAt(0.7f); - h.CreateMarkerAt(0.9f); - h.CreateMarkerAt(1f); - h.CreateMarkerAt(1.1f); - break; - case 1: - h.RelativeChildSize = new Vector2(150, 1); - h.CreateMarkerAt(0); - h.CreateMarkerAt(50); - h.CreateMarkerAt(100); - h.CreateMarkerAt(150); - h.CreateMarkerAt(200); - h.CreateMarkerAt(250); - break; - case 2: - h.RelativeChildOffset = new Vector2(50, 0); - h.RelativeChildSize = new Vector2(150, 1); - h.CreateMarkerAt(0); - h.CreateMarkerAt(50); - h.CreateMarkerAt(100); - h.CreateMarkerAt(150); - h.CreateMarkerAt(200); - h.CreateMarkerAt(250); - break; - case 3: - h.RelativeChildOffset = new Vector2(150, 0); - h.RelativeChildSize = new Vector2(-200, 1); - h.CreateMarkerAt(0); - h.CreateMarkerAt(50); - h.CreateMarkerAt(100); - h.CreateMarkerAt(150); - h.CreateMarkerAt(200); - h.CreateMarkerAt(250); - break; - case 4: - h.RelativeChildOffset = new Vector2(0, 0); - h.RelativeChildSize = new Vector2(300, 1); - h.CreateMarkerAt(0); - h.CreateMarkerAt(50); - h.CreateMarkerAt(100); - h.CreateMarkerAt(150); - h.CreateMarkerAt(200); - h.CreateMarkerAt(250); - break; - case 5: - h.RelativeChildOffset = new Vector2(-250, 0); - h.RelativeChildSize = new Vector2(500, 1); - h.CreateMarkerAt(-300); - h.CreateMarkerAt(-200); - h.CreateMarkerAt(-100); - h.CreateMarkerAt(0); - h.CreateMarkerAt(100); - h.CreateMarkerAt(200); - h.CreateMarkerAt(300); - break; - } - } - - private class HorizontalVisualiser : Visualiser - { - protected override void Update() - { - base.Update(); - - Left.Text = $"X = {RelativeChildOffset.X.ToString(CultureInfo.InvariantCulture)}"; - Right.Text = $"X = {(RelativeChildOffset.X + RelativeChildSize.X).ToString(CultureInfo.InvariantCulture)}"; - } - } - - private abstract class Visualiser : Container - { - public new Vector2 RelativeChildSize - { - protected get { return innerContainer.RelativeChildSize; } - set { innerContainer.RelativeChildSize = value; } - } - - public new Vector2 RelativeChildOffset - { - protected get { return innerContainer.RelativeChildOffset; } - set { innerContainer.RelativeChildOffset = value; } - } - - private readonly Container innerContainer; - - protected readonly SpriteText Left; - protected readonly SpriteText Right; - - protected Visualiser() - { - Height = 50; - - InternalChildren = new Drawable[] - { - new Box - { - Name = "Left marker", - Colour = Color4.Gray, - RelativeSizeAxes = Axes.Y, - }, - Left = new SpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopCentre, - Y = 6 - }, - new Box - { - Name = "Centre line", - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.Gray, - RelativeSizeAxes = Axes.X - }, - innerContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - new Box - { - Name = "Right marker", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = Color4.Gray, - RelativeSizeAxes = Axes.Y - }, - Right = new SpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.TopCentre, - Y = 6 - }, - }; - } - - public void CreateMarkerAt(float x) - { - innerContainer.Add(new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Both, - AutoSizeAxes = Axes.Both, - X = x, - Colour = Color4.Yellow, - Children = new Drawable[] - { - new Box - { - Name = "Centre marker horizontal", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(8, 1) - }, - new Box - { - Name = "Centre marker vertical", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, 8) - }, - new SpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, - Y = 6, - BypassAutoSizeAxes = Axes.Both, - Text = x.ToString(CultureInfo.InvariantCulture) - } - } - }); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Globalization; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseCoordinateSpaces : TestCase + { + public TestCaseCoordinateSpaces() + { + AddStep("0-1 space", () => loadCase(0)); + AddStep("0-150 space", () => loadCase(1)); + AddStep("50-200 space", () => loadCase(2)); + AddStep("150-(-50) space", () => loadCase(3)); + AddStep("0-300 space", () => loadCase(4)); + AddStep("-250-250 space", () => loadCase(5)); + } + + private void loadCase(int i) + { + Clear(); + + HorizontalVisualiser h; + Add(h = new HorizontalVisualiser + { + Size = new Vector2(200, 50), + X = 150 + }); + + switch (i) + { + case 0: + h.CreateMarkerAt(-0.1f); + h.CreateMarkerAt(0); + h.CreateMarkerAt(0.1f); + h.CreateMarkerAt(0.3f); + h.CreateMarkerAt(0.7f); + h.CreateMarkerAt(0.9f); + h.CreateMarkerAt(1f); + h.CreateMarkerAt(1.1f); + break; + case 1: + h.RelativeChildSize = new Vector2(150, 1); + h.CreateMarkerAt(0); + h.CreateMarkerAt(50); + h.CreateMarkerAt(100); + h.CreateMarkerAt(150); + h.CreateMarkerAt(200); + h.CreateMarkerAt(250); + break; + case 2: + h.RelativeChildOffset = new Vector2(50, 0); + h.RelativeChildSize = new Vector2(150, 1); + h.CreateMarkerAt(0); + h.CreateMarkerAt(50); + h.CreateMarkerAt(100); + h.CreateMarkerAt(150); + h.CreateMarkerAt(200); + h.CreateMarkerAt(250); + break; + case 3: + h.RelativeChildOffset = new Vector2(150, 0); + h.RelativeChildSize = new Vector2(-200, 1); + h.CreateMarkerAt(0); + h.CreateMarkerAt(50); + h.CreateMarkerAt(100); + h.CreateMarkerAt(150); + h.CreateMarkerAt(200); + h.CreateMarkerAt(250); + break; + case 4: + h.RelativeChildOffset = new Vector2(0, 0); + h.RelativeChildSize = new Vector2(300, 1); + h.CreateMarkerAt(0); + h.CreateMarkerAt(50); + h.CreateMarkerAt(100); + h.CreateMarkerAt(150); + h.CreateMarkerAt(200); + h.CreateMarkerAt(250); + break; + case 5: + h.RelativeChildOffset = new Vector2(-250, 0); + h.RelativeChildSize = new Vector2(500, 1); + h.CreateMarkerAt(-300); + h.CreateMarkerAt(-200); + h.CreateMarkerAt(-100); + h.CreateMarkerAt(0); + h.CreateMarkerAt(100); + h.CreateMarkerAt(200); + h.CreateMarkerAt(300); + break; + } + } + + private class HorizontalVisualiser : Visualiser + { + protected override void Update() + { + base.Update(); + + Left.Text = $"X = {RelativeChildOffset.X.ToString(CultureInfo.InvariantCulture)}"; + Right.Text = $"X = {(RelativeChildOffset.X + RelativeChildSize.X).ToString(CultureInfo.InvariantCulture)}"; + } + } + + private abstract class Visualiser : Container + { + public new Vector2 RelativeChildSize + { + protected get { return innerContainer.RelativeChildSize; } + set { innerContainer.RelativeChildSize = value; } + } + + public new Vector2 RelativeChildOffset + { + protected get { return innerContainer.RelativeChildOffset; } + set { innerContainer.RelativeChildOffset = value; } + } + + private readonly Container innerContainer; + + protected readonly SpriteText Left; + protected readonly SpriteText Right; + + protected Visualiser() + { + Height = 50; + + InternalChildren = new Drawable[] + { + new Box + { + Name = "Left marker", + Colour = Color4.Gray, + RelativeSizeAxes = Axes.Y, + }, + Left = new SpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre, + Y = 6 + }, + new Box + { + Name = "Centre line", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.Gray, + RelativeSizeAxes = Axes.X + }, + innerContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + new Box + { + Name = "Right marker", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = Color4.Gray, + RelativeSizeAxes = Axes.Y + }, + Right = new SpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.TopCentre, + Y = 6 + }, + }; + } + + public void CreateMarkerAt(float x) + { + innerContainer.Add(new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + AutoSizeAxes = Axes.Both, + X = x, + Colour = Color4.Yellow, + Children = new Drawable[] + { + new Box + { + Name = "Centre marker horizontal", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(8, 1) + }, + new Box + { + Name = "Centre marker vertical", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, 8) + }, + new SpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre, + Y = 6, + BypassAutoSizeAxes = Axes.Both, + Text = x.ToString(CultureInfo.InvariantCulture) + } + } + }); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseCountingText.cs b/osu.Framework.Tests/Visual/TestCaseCountingText.cs index 9c4d4e5ff..f60c06bf6 100644 --- a/osu.Framework.Tests/Visual/TestCaseCountingText.cs +++ b/osu.Framework.Tests/Visual/TestCaseCountingText.cs @@ -1,100 +1,100 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using OpenTK; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseCountingText : TestCase - { - private readonly Bindable countType = new Bindable(); - - public TestCaseCountingText() - { - Counter counter; - - BasicDropdown typeDropdown; - Children = new Drawable[] - { - typeDropdown = new BasicDropdown - { - Position = new Vector2(10), - Width = 150, - }, - counter = new TestTextCounter(createResult) - { - Position = new Vector2(180) - } - }; - - typeDropdown.Items = Enum.GetNames(typeof(CountType)).Select(n => new KeyValuePair(n, (CountType)Enum.Parse(typeof(CountType), n))); - countType.BindTo(typeDropdown.Current); - countType.ValueChanged += v => beginStep(lastStep)(); - - AddStep("1 -> 4 | 1 sec", beginStep(() => counter.CountTo(1).CountTo(4, 1000))); - AddStep("1 -> 4 | 3 sec", beginStep(() => counter.CountTo(1).CountTo(4, 3000))); - AddStep("4 -> 1 | 1 sec", beginStep(() => counter.CountTo(4).CountTo(1, 1000))); - AddStep("4 -> 1 | 3 sec", beginStep(() => counter.CountTo(4).CountTo(1, 3000))); - AddStep("1 -> 4 -> 1 | 6 sec", beginStep(() => counter.CountTo(1).CountTo(4, 3000).Then().CountTo(1, 3000))); - AddStep("1 -> 4 -> 1 | 2 sec", beginStep(() => counter.CountTo(1).CountTo(4, 1000).Then().CountTo(1, 1000))); - AddStep("1 -> 100 | 5 sec | OutQuint", beginStep(() => counter.CountTo(1).CountTo(100, 5000, Easing.OutQuint))); - } - - private Action lastStep; - private Action beginStep(Action stepAction) => () => - { - lastStep = stepAction; - stepAction?.Invoke(); - }; - - private string createResult(double value) - { - switch (countType.Value) - { - default: - case CountType.AsDouble: - return value.ToString(CultureInfo.InvariantCulture); - case CountType.AsInteger: - return ((int)value).ToString(); - case CountType.AsIntegerCeiling: - return ((int)Math.Ceiling(value)).ToString(); - case CountType.AsDouble2: - return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); - case CountType.AsDouble4: - return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture); - } - } - - private enum CountType - { - AsInteger, - AsIntegerCeiling, - AsDouble, - AsDouble2, - AsDouble4, - } - } - - public class TestTextCounter : Counter - { - private readonly Func resultFunction; - private readonly SpriteText text; - - public TestTextCounter(Func resultFunction) - { - this.resultFunction = resultFunction; - AddInternal(text = new SpriteText { TextSize = 24 }); - } - - protected override void OnCountChanged(double count) => text.Text = resultFunction(count); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenTK; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseCountingText : TestCase + { + private readonly Bindable countType = new Bindable(); + + public TestCaseCountingText() + { + Counter counter; + + BasicDropdown typeDropdown; + Children = new Drawable[] + { + typeDropdown = new BasicDropdown + { + Position = new Vector2(10), + Width = 150, + }, + counter = new TestTextCounter(createResult) + { + Position = new Vector2(180) + } + }; + + typeDropdown.Items = Enum.GetNames(typeof(CountType)).Select(n => new KeyValuePair(n, (CountType)Enum.Parse(typeof(CountType), n))); + countType.BindTo(typeDropdown.Current); + countType.ValueChanged += v => beginStep(lastStep)(); + + AddStep("1 -> 4 | 1 sec", beginStep(() => counter.CountTo(1).CountTo(4, 1000))); + AddStep("1 -> 4 | 3 sec", beginStep(() => counter.CountTo(1).CountTo(4, 3000))); + AddStep("4 -> 1 | 1 sec", beginStep(() => counter.CountTo(4).CountTo(1, 1000))); + AddStep("4 -> 1 | 3 sec", beginStep(() => counter.CountTo(4).CountTo(1, 3000))); + AddStep("1 -> 4 -> 1 | 6 sec", beginStep(() => counter.CountTo(1).CountTo(4, 3000).Then().CountTo(1, 3000))); + AddStep("1 -> 4 -> 1 | 2 sec", beginStep(() => counter.CountTo(1).CountTo(4, 1000).Then().CountTo(1, 1000))); + AddStep("1 -> 100 | 5 sec | OutQuint", beginStep(() => counter.CountTo(1).CountTo(100, 5000, Easing.OutQuint))); + } + + private Action lastStep; + private Action beginStep(Action stepAction) => () => + { + lastStep = stepAction; + stepAction?.Invoke(); + }; + + private string createResult(double value) + { + switch (countType.Value) + { + default: + case CountType.AsDouble: + return value.ToString(CultureInfo.InvariantCulture); + case CountType.AsInteger: + return ((int)value).ToString(); + case CountType.AsIntegerCeiling: + return ((int)Math.Ceiling(value)).ToString(); + case CountType.AsDouble2: + return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + case CountType.AsDouble4: + return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture); + } + } + + private enum CountType + { + AsInteger, + AsIntegerCeiling, + AsDouble, + AsDouble2, + AsDouble4, + } + } + + public class TestTextCounter : Counter + { + private readonly Func resultFunction; + private readonly SpriteText text; + + public TestTextCounter(Func resultFunction) + { + this.resultFunction = resultFunction; + AddInternal(text = new SpriteText { TextSize = 24 }); + } + + protected override void OnCountChanged(double count) => text.Text = resultFunction(count); + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseDelayedLoad.cs b/osu.Framework.Tests/Visual/TestCaseDelayedLoad.cs index 0b756bd30..c1a45d200 100644 --- a/osu.Framework.Tests/Visual/TestCaseDelayedLoad.cs +++ b/osu.Framework.Tests/Visual/TestCaseDelayedLoad.cs @@ -1,93 +1,93 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseDelayedLoad : TestCase - { - private const int panel_count = 2048; - - public TestCaseDelayedLoad() - { - FillFlowContainerNoInput flow; - ScrollContainer scroll; - - Children = new Drawable[] - { - scroll = new ScrollContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - flow = new FillFlowContainerNoInput - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - } - } - }; - - for (int i = 1; i < panel_count; i++) - flow.Add(new Container - { - Size = new Vector2(128), - Children = new Drawable[] - { - new DelayedLoadWrapper(new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TestBox{ RelativeSizeAxes = Axes.Both } - } - }), - new SpriteText { Text = i.ToString() }, - } - }); - - var childrenWithAvatarsLoaded = flow.Children.Where(c => c.Children.OfType().First().Content?.IsLoaded ?? false); - - AddWaitStep(10); - AddStep("scroll down", () => scroll.ScrollToEnd()); - AddWaitStep(10); - AddAssert("some loaded", () => childrenWithAvatarsLoaded.Count() > 5); - AddAssert("not too many loaded", () => childrenWithAvatarsLoaded.Count() < panel_count / 4); - } - - private class FillFlowContainerNoInput : FillFlowContainer - { - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - } - } - - public class TestBox : Container - { - public TestBox() - { - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - Child = new SpriteText - { - Colour = Color4.Yellow, - Text = @"loaded", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseDelayedLoad : TestCase + { + private const int panel_count = 2048; + + public TestCaseDelayedLoad() + { + FillFlowContainerNoInput flow; + ScrollContainer scroll; + + Children = new Drawable[] + { + scroll = new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + flow = new FillFlowContainerNoInput + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + } + }; + + for (int i = 1; i < panel_count; i++) + flow.Add(new Container + { + Size = new Vector2(128), + Children = new Drawable[] + { + new DelayedLoadWrapper(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TestBox{ RelativeSizeAxes = Axes.Both } + } + }), + new SpriteText { Text = i.ToString() }, + } + }); + + var childrenWithAvatarsLoaded = flow.Children.Where(c => c.Children.OfType().First().Content?.IsLoaded ?? false); + + AddWaitStep(10); + AddStep("scroll down", () => scroll.ScrollToEnd()); + AddWaitStep(10); + AddAssert("some loaded", () => childrenWithAvatarsLoaded.Count() > 5); + AddAssert("not too many loaded", () => childrenWithAvatarsLoaded.Count() < panel_count / 4); + } + + private class FillFlowContainerNoInput : FillFlowContainer + { + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + } + } + + public class TestBox : Container + { + public TestBox() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new SpriteText + { + Colour = Color4.Yellow, + Text = @"loaded", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseDrawSizePreservingFillContainer.cs b/osu.Framework.Tests/Visual/TestCaseDrawSizePreservingFillContainer.cs index 9d8513d5a..0ff3db893 100644 --- a/osu.Framework.Tests/Visual/TestCaseDrawSizePreservingFillContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseDrawSizePreservingFillContainer.cs @@ -1,61 +1,61 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseDrawSizePreservingFillContainer : TestCase - { - public TestCaseDrawSizePreservingFillContainer() - { - DrawSizePreservingFillContainer fillContainer; - Child = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Red, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - fillContainer = new DrawSizePreservingFillContainer - { - Child = new TestCaseSizing(), - }, - } - }, - } - }; - - AddStep("Strategy: Minimum", () => fillContainer.Strategy = DrawSizePreservationStrategy.Minimum); - AddStep("Strategy: Maximum", () => fillContainer.Strategy = DrawSizePreservationStrategy.Maximum); - AddStep("Strategy: Average", () => fillContainer.Strategy = DrawSizePreservationStrategy.Average); - AddStep("Strategy: Separate", () => fillContainer.Strategy = DrawSizePreservationStrategy.Separate); - - AddSliderStep("Width", 50, 650, 500, v => Child.Width = v); - AddSliderStep("Height", 50, 650, 500, v => Child.Height = v); - - AddStep("Override Size to 1x1", () => Child.Size = Vector2.One); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseDrawSizePreservingFillContainer : TestCase + { + public TestCaseDrawSizePreservingFillContainer() + { + DrawSizePreservingFillContainer fillContainer; + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + fillContainer = new DrawSizePreservingFillContainer + { + Child = new TestCaseSizing(), + }, + } + }, + } + }; + + AddStep("Strategy: Minimum", () => fillContainer.Strategy = DrawSizePreservationStrategy.Minimum); + AddStep("Strategy: Maximum", () => fillContainer.Strategy = DrawSizePreservationStrategy.Maximum); + AddStep("Strategy: Average", () => fillContainer.Strategy = DrawSizePreservationStrategy.Average); + AddStep("Strategy: Separate", () => fillContainer.Strategy = DrawSizePreservationStrategy.Separate); + + AddSliderStep("Width", 50, 650, 500, v => Child.Width = v); + AddSliderStep("Height", 50, 650, 500, v => Child.Height = v); + + AddStep("Override Size to 1x1", () => Child.Size = Vector2.One); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseDrawablePath.cs b/osu.Framework.Tests/Visual/TestCaseDrawablePath.cs index 7ce84e64b..b642faaec 100644 --- a/osu.Framework.Tests/Visual/TestCaseDrawablePath.cs +++ b/osu.Framework.Tests/Visual/TestCaseDrawablePath.cs @@ -1,128 +1,128 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseDrawablePath : GridTestCase - { - public TestCaseDrawablePath() : base(2, 2) - { - const int width = 20; - Texture gradientTexture = new Texture(width, 1, true); - byte[] data = new byte[width * 4]; - for (int i = 0; i < width; ++i) - { - float brightness = (float)i / (width - 1); - int index = i * 4; - data[index + 0] = (byte)(brightness * 255); - data[index + 1] = (byte)(brightness * 255); - data[index + 2] = (byte)(brightness * 255); - data[index + 3] = 255; - } - gradientTexture.SetData(new TextureUpload(data)); - - Cell(0).AddRange(new[] - { - createLabel("Simple path"), - new Path - { - RelativeSizeAxes = Axes.Both, - Positions = new List { Vector2.One * 50, Vector2.One * 100 }, - Texture = gradientTexture, - Colour = Color4.Green, - }, - }); - - Cell(1).AddRange(new[] - { - createLabel("Curved path"), - new Path - { - RelativeSizeAxes = Axes.Both, - Positions = new List - { - new Vector2(50, 50), - new Vector2(50, 250), - new Vector2(250, 250), - new Vector2(250, 50), - new Vector2(50, 50), - }, - Texture = gradientTexture, - Colour = Color4.Blue, - }, - }); - - Cell(2).AddRange(new[] - { - createLabel("Self-overlapping path"), - new Path - { - RelativeSizeAxes = Axes.Both, - Positions = new List - { - new Vector2(50, 50), - new Vector2(50, 250), - new Vector2(250, 250), - new Vector2(250, 150), - new Vector2(20, 150), - }, - Texture = gradientTexture, - Colour = Color4.Red, - }, - }); - - Cell(3).AddRange(new[] - { - createLabel("Draw something ;)"), - new UserDrawnPath - { - RelativeSizeAxes = Axes.Both, - Texture = gradientTexture, - Colour = Color4.White, - }, - }); - } - - private Drawable createLabel(string text) => new SpriteText - { - Text = text, - TextSize = 20, - Colour = Color4.White, - }; - - private class UserDrawnPath : Path - { - private Vector2 oldPos; - - protected override bool OnDragStart(InputState state) - { - AddVertex(state.Mouse.Position); - oldPos = state.Mouse.Position; - return true; - } - - protected override bool OnDrag(InputState state) - { - Vector2 pos = state.Mouse.Position; - if ((pos - oldPos).Length > 10) - { - AddVertex(pos); - oldPos = pos; - } - - return base.OnDrag(state); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseDrawablePath : GridTestCase + { + public TestCaseDrawablePath() : base(2, 2) + { + const int width = 20; + Texture gradientTexture = new Texture(width, 1, true); + byte[] data = new byte[width * 4]; + for (int i = 0; i < width; ++i) + { + float brightness = (float)i / (width - 1); + int index = i * 4; + data[index + 0] = (byte)(brightness * 255); + data[index + 1] = (byte)(brightness * 255); + data[index + 2] = (byte)(brightness * 255); + data[index + 3] = 255; + } + gradientTexture.SetData(new TextureUpload(data)); + + Cell(0).AddRange(new[] + { + createLabel("Simple path"), + new Path + { + RelativeSizeAxes = Axes.Both, + Positions = new List { Vector2.One * 50, Vector2.One * 100 }, + Texture = gradientTexture, + Colour = Color4.Green, + }, + }); + + Cell(1).AddRange(new[] + { + createLabel("Curved path"), + new Path + { + RelativeSizeAxes = Axes.Both, + Positions = new List + { + new Vector2(50, 50), + new Vector2(50, 250), + new Vector2(250, 250), + new Vector2(250, 50), + new Vector2(50, 50), + }, + Texture = gradientTexture, + Colour = Color4.Blue, + }, + }); + + Cell(2).AddRange(new[] + { + createLabel("Self-overlapping path"), + new Path + { + RelativeSizeAxes = Axes.Both, + Positions = new List + { + new Vector2(50, 50), + new Vector2(50, 250), + new Vector2(250, 250), + new Vector2(250, 150), + new Vector2(20, 150), + }, + Texture = gradientTexture, + Colour = Color4.Red, + }, + }); + + Cell(3).AddRange(new[] + { + createLabel("Draw something ;)"), + new UserDrawnPath + { + RelativeSizeAxes = Axes.Both, + Texture = gradientTexture, + Colour = Color4.White, + }, + }); + } + + private Drawable createLabel(string text) => new SpriteText + { + Text = text, + TextSize = 20, + Colour = Color4.White, + }; + + private class UserDrawnPath : Path + { + private Vector2 oldPos; + + protected override bool OnDragStart(InputState state) + { + AddVertex(state.Mouse.Position); + oldPos = state.Mouse.Position; + return true; + } + + protected override bool OnDrag(InputState state) + { + Vector2 pos = state.Mouse.Position; + if ((pos - oldPos).Length > 10) + { + AddVertex(pos); + oldPos = pos; + } + + return base.OnDrag(state); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseDropdownBox.cs b/osu.Framework.Tests/Visual/TestCaseDropdownBox.cs index 5ac74f242..d0af873d8 100644 --- a/osu.Framework.Tests/Visual/TestCaseDropdownBox.cs +++ b/osu.Framework.Tests/Visual/TestCaseDropdownBox.cs @@ -1,105 +1,105 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseDropdownBox : TestCase - { - private const int items_to_add = 10; - - public TestCaseDropdownBox() - { - StyledDropdown styledDropdown, styledDropdownMenu2; - - var testItems = new string[10]; - int i = 0; - while (i < items_to_add) - testItems[i] = @"test " + i++; - - Add(styledDropdown = new StyledDropdown - { - Width = 150, - Position = new Vector2(200, 70), - Items = testItems.Select(item => new KeyValuePair(item, item)), - }); - - Add(styledDropdownMenu2 = new StyledDropdown - { - Width = 150, - Position = new Vector2(400, 70), - Items = testItems.Select(item => new KeyValuePair(item, item)), - }); - - AddStep("click dropdown1", () => toggleDropdownViaClick(styledDropdown)); - AddAssert("dropdown is open", () => styledDropdown.Menu.State == MenuState.Open); - - AddRepeatStep("add item", () => styledDropdown.AddDropdownItem(@"test " + i, @"test " + i++), items_to_add); - AddAssert("item count is correct", () => styledDropdown.Items.Count() == items_to_add * 2); - - AddStep("click item 13", () => styledDropdown.SelectItem(styledDropdown.Menu.Items[13])); - - AddAssert("dropdown1 is closed", () => styledDropdown.Menu.State == MenuState.Closed); - AddAssert("item 13 is selected", () => styledDropdown.Current == styledDropdown.Items.ElementAt(13).Value); - - AddStep("select item 15", () => styledDropdown.Current.Value = styledDropdown.Items.ElementAt(15).Value); - AddAssert("item 15 is selected", () => styledDropdown.Current == styledDropdown.Items.ElementAt(15).Value); - - AddStep("click dropdown1", () => toggleDropdownViaClick(styledDropdown)); - AddAssert("dropdown1 is open", () => styledDropdown.Menu.State == MenuState.Open); - - AddStep("click dropdown2", () => toggleDropdownViaClick(styledDropdownMenu2)); - - AddAssert("dropdown1 is closed", () => styledDropdown.Menu.State == MenuState.Closed); - AddAssert("dropdown2 is open", () => styledDropdownMenu2.Menu.State == MenuState.Open); - } - - private void toggleDropdownViaClick(StyledDropdown dropdown) => dropdown.Children.First().TriggerOnClick(); - - private class StyledDropdown : BasicDropdown - { - public new DropdownMenu Menu => base.Menu; - - protected override DropdownMenu CreateMenu() => new StyledDropdownMenu(); - - protected override DropdownHeader CreateHeader() => new StyledDropdownHeader(); - - public void SelectItem(MenuItem item) => ((StyledDropdownMenu)Menu).SelectItem(item); - - private class StyledDropdownMenu : DropdownMenu - { - public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.TriggerOnClick(); - } - } - - private class StyledDropdownHeader : DropdownHeader - { - private readonly SpriteText label; - - protected internal override string Label - { - get { return label.Text; } - set { label.Text = value; } - } - - public StyledDropdownHeader() - { - Foreground.Padding = new MarginPadding(4); - BackgroundColour = new Color4(255, 255, 255, 100); - BackgroundColourHover = Color4.HotPink; - Children = new[] - { - label = new SpriteText(), - }; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseDropdownBox : TestCase + { + private const int items_to_add = 10; + + public TestCaseDropdownBox() + { + StyledDropdown styledDropdown, styledDropdownMenu2; + + var testItems = new string[10]; + int i = 0; + while (i < items_to_add) + testItems[i] = @"test " + i++; + + Add(styledDropdown = new StyledDropdown + { + Width = 150, + Position = new Vector2(200, 70), + Items = testItems.Select(item => new KeyValuePair(item, item)), + }); + + Add(styledDropdownMenu2 = new StyledDropdown + { + Width = 150, + Position = new Vector2(400, 70), + Items = testItems.Select(item => new KeyValuePair(item, item)), + }); + + AddStep("click dropdown1", () => toggleDropdownViaClick(styledDropdown)); + AddAssert("dropdown is open", () => styledDropdown.Menu.State == MenuState.Open); + + AddRepeatStep("add item", () => styledDropdown.AddDropdownItem(@"test " + i, @"test " + i++), items_to_add); + AddAssert("item count is correct", () => styledDropdown.Items.Count() == items_to_add * 2); + + AddStep("click item 13", () => styledDropdown.SelectItem(styledDropdown.Menu.Items[13])); + + AddAssert("dropdown1 is closed", () => styledDropdown.Menu.State == MenuState.Closed); + AddAssert("item 13 is selected", () => styledDropdown.Current == styledDropdown.Items.ElementAt(13).Value); + + AddStep("select item 15", () => styledDropdown.Current.Value = styledDropdown.Items.ElementAt(15).Value); + AddAssert("item 15 is selected", () => styledDropdown.Current == styledDropdown.Items.ElementAt(15).Value); + + AddStep("click dropdown1", () => toggleDropdownViaClick(styledDropdown)); + AddAssert("dropdown1 is open", () => styledDropdown.Menu.State == MenuState.Open); + + AddStep("click dropdown2", () => toggleDropdownViaClick(styledDropdownMenu2)); + + AddAssert("dropdown1 is closed", () => styledDropdown.Menu.State == MenuState.Closed); + AddAssert("dropdown2 is open", () => styledDropdownMenu2.Menu.State == MenuState.Open); + } + + private void toggleDropdownViaClick(StyledDropdown dropdown) => dropdown.Children.First().TriggerOnClick(); + + private class StyledDropdown : BasicDropdown + { + public new DropdownMenu Menu => base.Menu; + + protected override DropdownMenu CreateMenu() => new StyledDropdownMenu(); + + protected override DropdownHeader CreateHeader() => new StyledDropdownHeader(); + + public void SelectItem(MenuItem item) => ((StyledDropdownMenu)Menu).SelectItem(item); + + private class StyledDropdownMenu : DropdownMenu + { + public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.TriggerOnClick(); + } + } + + private class StyledDropdownHeader : DropdownHeader + { + private readonly SpriteText label; + + protected internal override string Label + { + get { return label.Text; } + set { label.Text = value; } + } + + public StyledDropdownHeader() + { + Foreground.Padding = new MarginPadding(4); + BackgroundColour = new Color4(255, 255, 255, 100); + BackgroundColourHover = Color4.HotPink; + Children = new[] + { + label = new SpriteText(), + }; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseDynamicDepth.cs b/osu.Framework.Tests/Visual/TestCaseDynamicDepth.cs index 5fa93da65..2a0a4c080 100644 --- a/osu.Framework.Tests/Visual/TestCaseDynamicDepth.cs +++ b/osu.Framework.Tests/Visual/TestCaseDynamicDepth.cs @@ -1,83 +1,83 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("changing depth of child dynamically")] - public class TestCaseDynamicDepth : TestCase - { - private void addDepthSteps(DepthBox box, Container container) - { - AddStep($@"bring forward {box.Name}", () => container.ChangeChildDepth(box, box.Depth - 1)); - AddStep($@"send backward {box.Name}", () => container.ChangeChildDepth(box, box.Depth + 1)); - } - - public TestCaseDynamicDepth() - { - DepthBox red, blue, green, purple; - Container container; - - AddRange(new[] - { - container = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(340), - Children = new[] - { - red = new DepthBox(Color4.Red, Anchor.TopLeft) { Name = "red" }, - blue = new DepthBox(Color4.Blue, Anchor.TopRight) { Name = "blue" }, - green = new DepthBox(Color4.Green, Anchor.BottomRight) { Name = "green" }, - purple = new DepthBox(Color4.Purple, Anchor.BottomLeft) { Name = "purple" }, - } - } - }); - - addDepthSteps(red, container); - addDepthSteps(blue, container); - addDepthSteps(green, container); - addDepthSteps(purple, container); - } - - private class DepthBox : Container - { - private readonly SpriteText depthText; - - public DepthBox(Color4 colour, Anchor anchor) - { - Size = new Vector2(240); - Anchor = Origin = anchor; - - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colour, - }, - depthText = new SpriteText - { - Anchor = anchor, - Origin = anchor, - } - }); - } - - protected override void Update() - { - base.Update(); - - depthText.Text = $@"Depth: {Depth}"; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("changing depth of child dynamically")] + public class TestCaseDynamicDepth : TestCase + { + private void addDepthSteps(DepthBox box, Container container) + { + AddStep($@"bring forward {box.Name}", () => container.ChangeChildDepth(box, box.Depth - 1)); + AddStep($@"send backward {box.Name}", () => container.ChangeChildDepth(box, box.Depth + 1)); + } + + public TestCaseDynamicDepth() + { + DepthBox red, blue, green, purple; + Container container; + + AddRange(new[] + { + container = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(340), + Children = new[] + { + red = new DepthBox(Color4.Red, Anchor.TopLeft) { Name = "red" }, + blue = new DepthBox(Color4.Blue, Anchor.TopRight) { Name = "blue" }, + green = new DepthBox(Color4.Green, Anchor.BottomRight) { Name = "green" }, + purple = new DepthBox(Color4.Purple, Anchor.BottomLeft) { Name = "purple" }, + } + } + }); + + addDepthSteps(red, container); + addDepthSteps(blue, container); + addDepthSteps(green, container); + addDepthSteps(purple, container); + } + + private class DepthBox : Container + { + private readonly SpriteText depthText; + + public DepthBox(Color4 colour, Anchor anchor) + { + Size = new Vector2(240); + Anchor = Origin = anchor; + + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour, + }, + depthText = new SpriteText + { + Anchor = anchor, + Origin = anchor, + } + }); + } + + protected override void Update() + { + base.Update(); + + depthText.Text = $@"Depth: {Depth}"; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseEffects.cs b/osu.Framework.Tests/Visual/TestCaseEffects.cs index 44dc244a6..096f93f50 100644 --- a/osu.Framework.Tests/Visual/TestCaseEffects.cs +++ b/osu.Framework.Tests/Visual/TestCaseEffects.cs @@ -1,238 +1,238 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("implementing the IEffect interface")] - public class TestCaseEffects : TestCase - { - public TestCaseEffects() - { - var effect = new EdgeEffect - { - CornerRadius = 3f, - Parameters = new EdgeEffectParameters - { - Colour = Color4.LightBlue, - Hollow = true, - Radius = 5f, - Type = EdgeEffectType.Glow - } - }; - Add(new FillFlowContainer - { - Position = new Vector2(10f, 10f), - Spacing = new Vector2(25f, 25f), - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new SpriteText - { - Text = "Blur Test", - TextSize = 32f - }.WithEffect(new BlurEffect - { - Sigma = new Vector2(2f, 0f), - Strength = 2f, - Rotation = 45f, - }), - new SpriteText - { - Text = "EdgeEffect Test", - TextSize = 32f - }.WithEffect(new EdgeEffect - { - CornerRadius = 3f, - Parameters = new EdgeEffectParameters - { - Colour = Color4.Yellow, - Hollow = true, - Radius = 5f, - Type = EdgeEffectType.Shadow - } - }), - new SpriteText - { - Text = "Repeated usage of same effect test", - TextSize = 32f - }.WithEffect(effect), - new SpriteText - { - Text = "Repeated usage of same effect test", - TextSize = 32f - }.WithEffect(effect), - new SpriteText - { - Text = "Repeated usage of same effect test", - TextSize = 32f - }.WithEffect(effect), - new SpriteText - { - Text = "Multiple effects Test", - TextSize = 32f - }.WithEffect(new BlurEffect - { - Sigma = new Vector2(2f, 2f), - Strength = 2f - }).WithEffect(new EdgeEffect - { - CornerRadius = 3f, - Parameters = new EdgeEffectParameters - { - Colour = Color4.Yellow, - Hollow = true, - Radius = 5f, - Type = EdgeEffectType.Shadow - } - }), - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.CornflowerBlue, - RelativeSizeAxes = Axes.Both, - }, - new SpriteText - { - Text = "Outlined Text", - TextSize = 32f - }.WithEffect(new OutlineEffect - { - BlurSigma = new Vector2(3f), - Strength = 3f, - Colour = Color4.Red, - PadExtent = true, - }) - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.CornflowerBlue, - RelativeSizeAxes = Axes.Both, - }, - new SpriteText - { - Text = "Glowing Text", - TextSize = 32f, - }.WithEffect(new GlowEffect - { - BlurSigma = new Vector2(3f), - Strength = 3f, - Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), - PadExtent = true, - }), - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Size = new Vector2(150, 40), - }.WithEffect(new GlowEffect - { - BlurSigma = new Vector2(3f), - Strength = 3f, - Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), - PadExtent = true, - }), - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Absolute Size", - TextSize = 32f, - Colour = Color4.Red, - Shadow = true, - } - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1.1f, 1.1f), - }.WithEffect(new GlowEffect - { - BlurSigma = new Vector2(3f), - Strength = 3f, - Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), - PadExtent = true, - }), - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Relative Size", - TextSize = 32f, - Colour = Color4.Red, - Shadow = true, - }, - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1.1f, 1.1f), - Rotation = 10, - }.WithEffect(new GlowEffect - { - BlurSigma = new Vector2(3f), - Strength = 3f, - Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), - PadExtent = true, - }), - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Rotation", - TextSize = 32f, - Colour = Color4.Red, - Shadow = true, - }, - } - }, - } - }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("implementing the IEffect interface")] + public class TestCaseEffects : TestCase + { + public TestCaseEffects() + { + var effect = new EdgeEffect + { + CornerRadius = 3f, + Parameters = new EdgeEffectParameters + { + Colour = Color4.LightBlue, + Hollow = true, + Radius = 5f, + Type = EdgeEffectType.Glow + } + }; + Add(new FillFlowContainer + { + Position = new Vector2(10f, 10f), + Spacing = new Vector2(25f, 25f), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new SpriteText + { + Text = "Blur Test", + TextSize = 32f + }.WithEffect(new BlurEffect + { + Sigma = new Vector2(2f, 0f), + Strength = 2f, + Rotation = 45f, + }), + new SpriteText + { + Text = "EdgeEffect Test", + TextSize = 32f + }.WithEffect(new EdgeEffect + { + CornerRadius = 3f, + Parameters = new EdgeEffectParameters + { + Colour = Color4.Yellow, + Hollow = true, + Radius = 5f, + Type = EdgeEffectType.Shadow + } + }), + new SpriteText + { + Text = "Repeated usage of same effect test", + TextSize = 32f + }.WithEffect(effect), + new SpriteText + { + Text = "Repeated usage of same effect test", + TextSize = 32f + }.WithEffect(effect), + new SpriteText + { + Text = "Repeated usage of same effect test", + TextSize = 32f + }.WithEffect(effect), + new SpriteText + { + Text = "Multiple effects Test", + TextSize = 32f + }.WithEffect(new BlurEffect + { + Sigma = new Vector2(2f, 2f), + Strength = 2f + }).WithEffect(new EdgeEffect + { + CornerRadius = 3f, + Parameters = new EdgeEffectParameters + { + Colour = Color4.Yellow, + Hollow = true, + Radius = 5f, + Type = EdgeEffectType.Shadow + } + }), + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.CornflowerBlue, + RelativeSizeAxes = Axes.Both, + }, + new SpriteText + { + Text = "Outlined Text", + TextSize = 32f + }.WithEffect(new OutlineEffect + { + BlurSigma = new Vector2(3f), + Strength = 3f, + Colour = Color4.Red, + PadExtent = true, + }) + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.CornflowerBlue, + RelativeSizeAxes = Axes.Both, + }, + new SpriteText + { + Text = "Glowing Text", + TextSize = 32f, + }.WithEffect(new GlowEffect + { + BlurSigma = new Vector2(3f), + Strength = 3f, + Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), + PadExtent = true, + }), + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Size = new Vector2(150, 40), + }.WithEffect(new GlowEffect + { + BlurSigma = new Vector2(3f), + Strength = 3f, + Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), + PadExtent = true, + }), + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Absolute Size", + TextSize = 32f, + Colour = Color4.Red, + Shadow = true, + } + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.1f, 1.1f), + }.WithEffect(new GlowEffect + { + BlurSigma = new Vector2(3f), + Strength = 3f, + Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), + PadExtent = true, + }), + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Relative Size", + TextSize = 32f, + Colour = Color4.Red, + Shadow = true, + }, + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.1f, 1.1f), + Rotation = 10, + }.WithEffect(new GlowEffect + { + BlurSigma = new Vector2(3f), + Strength = 3f, + Colour = ColourInfo.GradientHorizontal(new Color4(1.2f, 0, 0, 1f), new Color4(0, 1f, 0, 1f)), + PadExtent = true, + }), + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Rotation", + TextSize = 32f, + Colour = Color4.Red, + Shadow = true, + }, + } + }, + } + }); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseFillFlowContainer.cs b/osu.Framework.Tests/Visual/TestCaseFillFlowContainer.cs index 22dc65ae1..4f246d0a5 100644 --- a/osu.Framework.Tests/Visual/TestCaseFillFlowContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseFillFlowContainer.cs @@ -1,420 +1,420 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.MathUtils; -using osu.Framework.Testing; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseFillFlowContainer : TestCase - { - private FillDirectionDropdown selectionDropdown; - - private Anchor childAnchor = Anchor.TopLeft; - private AnchorDropdown anchorDropdown; - - private Anchor childOrigin = Anchor.TopLeft; - private AnchorDropdown originDropdown; - - private FillFlowContainer fillContainer; - private ScheduledDelegate scheduledAdder; - private bool doNotAddChildren; - - public TestCaseFillFlowContainer() - { - reset(); - } - - private void reset() - { - doNotAddChildren = false; - scheduledAdder?.Cancel(); - - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Width = 0.2f, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Depth = float.MinValue, - Children = new[] - { - new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new SpriteText { Text = @"Fill mode" }, - selectionDropdown = new FillDirectionDropdown - { - RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(typeof(FlowTestCase)).Cast() - .Select(value => new KeyValuePair(value.ToString(), value)), - }, - new SpriteText { Text = @"Child anchor" }, - anchorDropdown = new AnchorDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }.Select(anchor => new KeyValuePair(anchor.ToString(), anchor)), - }, - new SpriteText { Text = @"Child origin" }, - originDropdown = new AnchorDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }.Select(anchor => new KeyValuePair(anchor.ToString(), anchor)), - }, - } - } - } - }; - - selectionDropdown.Current.ValueChanged += changeTest; - buildTest(); - selectionDropdown.Current.Value = FlowTestCase.Full; - changeTest(FlowTestCase.Full); - } - - protected override void Update() - { - base.Update(); - - if (childAnchor != anchorDropdown.Current) - { - childAnchor = anchorDropdown.Current; - foreach (var child in fillContainer.Children) - child.Anchor = childAnchor; - } - - if (childOrigin != originDropdown.Current) - { - childOrigin = originDropdown.Current; - foreach (var child in fillContainer.Children) - child.Origin = childOrigin; - } - } - - private void changeTest(FlowTestCase testCase) - { - var method = - GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).SingleOrDefault(m => m.GetCustomAttribute()?.TestCase == testCase); - if (method != null) - method.Invoke(this, new object[0]); - } - - private void buildTest() - { - Add(new Container - { - Padding = new MarginPadding(25f), - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - fillContainer = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - AutoSizeAxes = Axes.None, - }, - new Box - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(3, 1), - Colour = Color4.HotPink, - }, - new Box - { - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(3, 1), - Colour = Color4.HotPink, - }, - new Box - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, 3), - Colour = Color4.HotPink, - }, - new Box - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, 3), - Colour = Color4.HotPink, - } - } - }); - - AddToggleStep("Rotate Container", state => { fillContainer.RotateTo(state ? 45f : 0, 1000); }); - AddToggleStep("Scale Container", state => { fillContainer.ScaleTo(state ? 1.2f : 1f, 1000); }); - AddToggleStep("Shear Container", state => { fillContainer.Shear = state ? new Vector2(0.5f, 0f) : new Vector2(0f, 0f); }); - AddToggleStep("Center Container Anchor", state => { fillContainer.Anchor = state ? Anchor.Centre : Anchor.TopLeft; }); - AddToggleStep("Center Container Origin", state => { fillContainer.Origin = state ? Anchor.Centre : Anchor.TopLeft; }); - AddToggleStep("Autosize Container", state => - { - if (state) - { - fillContainer.RelativeSizeAxes = Axes.None; - fillContainer.AutoSizeAxes = Axes.Both; - } - else - { - fillContainer.AutoSizeAxes = Axes.None; - fillContainer.RelativeSizeAxes = Axes.Both; - fillContainer.Width = 1; - fillContainer.Height = 1; - } - }); - AddToggleStep("Rotate children", state => - { - if (state) - { - foreach (var child in fillContainer.Children) - child.RotateTo(45f, 1000); - } - else - { - foreach (var child in fillContainer.Children) - child.RotateTo(0f, 1000); - } - }); - AddToggleStep("Shear children", state => - { - if (state) - { - foreach (var child in fillContainer.Children) - child.Shear = new Vector2(0.2f, 0.2f); - } - else - { - foreach (var child in fillContainer.Children) - child.Shear = Vector2.Zero; - } - }); - AddToggleStep("Scale children", state => - { - if (state) - { - foreach (var child in fillContainer.Children) - child.ScaleTo(1.25f, 1000); - } - else - { - foreach (var child in fillContainer.Children) - child.ScaleTo(1f, 1000); - } - }); - AddToggleStep("Randomly scale children", state => - { - if (state) - { - foreach (var child in fillContainer.Children) - child.ScaleTo(RNG.NextSingle(1, 2), 1000); - } - else - { - foreach (var child in fillContainer.Children) - child.ScaleTo(1f, 1000); - } - }); - AddToggleStep("Randomly set child origins", state => - { - if (state) - { - foreach (var child in fillContainer.Children) - { - switch (RNG.Next(9)) - { - case 0: child.Origin = Anchor.TopLeft; break; - case 1: child.Origin = Anchor.TopCentre; break; - case 2: child.Origin = Anchor.TopRight; break; - case 3: child.Origin = Anchor.CentreLeft; break; - case 4: child.Origin = Anchor.Centre; break; - case 5: child.Origin = Anchor.CentreRight; break; - case 6: child.Origin = Anchor.BottomLeft; break; - case 7: child.Origin = Anchor.BottomCentre; break; - case 8: child.Origin = Anchor.BottomRight; break; - } - } - } - else - { - foreach (var child in fillContainer.Children) - child.Origin = originDropdown.Current; - } - }); - - AddToggleStep("Stop adding children", state => { doNotAddChildren = state; }); - - scheduledAdder?.Cancel(); - scheduledAdder = Scheduler.AddDelayed( - () => - { - if (fillContainer.Parent == null) - scheduledAdder.Cancel(); - - if (doNotAddChildren) - { - fillContainer.Invalidate(); - } - - if (fillContainer.Children.Count < 1000 && !doNotAddChildren) - { - fillContainer.Add(new Container - { - Anchor = childAnchor, - Origin = childOrigin, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Width = 50, - Height = 50, - Colour = Color4.White - }, - new SpriteText - { - Colour = Color4.Black, - RelativePositionAxes = Axes.Both, - Position = new Vector2(0.5f, 0.5f), - Origin = Anchor.Centre, - Text = fillContainer.Children.Count.ToString() - } - } - }); - } - }, - 100, - true - ); - } - - [FlowTestCase(FlowTestCase.Full)] - private void test1() - { - fillContainer.Direction = FillDirection.Full; - fillContainer.Spacing = new Vector2(5, 5); - } - - [FlowTestCase(FlowTestCase.Horizontal)] - private void test2() - { - fillContainer.Direction = FillDirection.Horizontal; - fillContainer.Spacing = new Vector2(5, 5); - } - - [FlowTestCase(FlowTestCase.Vertical)] - private void test3() - { - fillContainer.Direction = FillDirection.Vertical; - fillContainer.Spacing = new Vector2(5, 5); - } - - private class TestCaseDropdownHeader : DropdownHeader - { - private readonly SpriteText label; - - protected internal override string Label - { - get { return label.Text; } - set { label.Text = value; } - } - - public TestCaseDropdownHeader() - { - Foreground.Padding = new MarginPadding(4); - BackgroundColour = new Color4(100, 100, 100, 255); - BackgroundColourHover = Color4.HotPink; - Children = new[] - { - label = new SpriteText(), - }; - } - } - - private class AnchorDropdown : BasicDropdown - { - protected override DropdownHeader CreateHeader() => new TestCaseDropdownHeader(); - } - - private class AnchorDropdownMenuItem : DropdownMenuItem - { - public AnchorDropdownMenuItem(Anchor anchor) - : base(anchor.ToString(), anchor) - { - } - } - - private class FillDirectionDropdown : BasicDropdown - { - protected override DropdownHeader CreateHeader() => new TestCaseDropdownHeader(); - } - - private class FillDirectionDropdownMenuItem : DropdownMenuItem - { - public FillDirectionDropdownMenuItem(FlowTestCase testCase) - : base(testCase.ToString(), testCase) - { - } - } - - [AttributeUsage(AttributeTargets.Method)] - private class FlowTestCaseAttribute : Attribute - { - public FlowTestCase TestCase { get; } - - public FlowTestCaseAttribute(FlowTestCase testCase) - { - TestCase = testCase; - } - } - - private enum FlowTestCase - { - Full, - Horizontal, - Vertical, - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseFillFlowContainer : TestCase + { + private FillDirectionDropdown selectionDropdown; + + private Anchor childAnchor = Anchor.TopLeft; + private AnchorDropdown anchorDropdown; + + private Anchor childOrigin = Anchor.TopLeft; + private AnchorDropdown originDropdown; + + private FillFlowContainer fillContainer; + private ScheduledDelegate scheduledAdder; + private bool doNotAddChildren; + + public TestCaseFillFlowContainer() + { + reset(); + } + + private void reset() + { + doNotAddChildren = false; + scheduledAdder?.Cancel(); + + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Width = 0.2f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Depth = float.MinValue, + Children = new[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SpriteText { Text = @"Fill mode" }, + selectionDropdown = new FillDirectionDropdown + { + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(typeof(FlowTestCase)).Cast() + .Select(value => new KeyValuePair(value.ToString(), value)), + }, + new SpriteText { Text = @"Child anchor" }, + anchorDropdown = new AnchorDropdown + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }.Select(anchor => new KeyValuePair(anchor.ToString(), anchor)), + }, + new SpriteText { Text = @"Child origin" }, + originDropdown = new AnchorDropdown + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }.Select(anchor => new KeyValuePair(anchor.ToString(), anchor)), + }, + } + } + } + }; + + selectionDropdown.Current.ValueChanged += changeTest; + buildTest(); + selectionDropdown.Current.Value = FlowTestCase.Full; + changeTest(FlowTestCase.Full); + } + + protected override void Update() + { + base.Update(); + + if (childAnchor != anchorDropdown.Current) + { + childAnchor = anchorDropdown.Current; + foreach (var child in fillContainer.Children) + child.Anchor = childAnchor; + } + + if (childOrigin != originDropdown.Current) + { + childOrigin = originDropdown.Current; + foreach (var child in fillContainer.Children) + child.Origin = childOrigin; + } + } + + private void changeTest(FlowTestCase testCase) + { + var method = + GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).SingleOrDefault(m => m.GetCustomAttribute()?.TestCase == testCase); + if (method != null) + method.Invoke(this, new object[0]); + } + + private void buildTest() + { + Add(new Container + { + Padding = new MarginPadding(25f), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + fillContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.None, + }, + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(3, 1), + Colour = Color4.HotPink, + }, + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(3, 1), + Colour = Color4.HotPink, + }, + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 3), + Colour = Color4.HotPink, + }, + new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 3), + Colour = Color4.HotPink, + } + } + }); + + AddToggleStep("Rotate Container", state => { fillContainer.RotateTo(state ? 45f : 0, 1000); }); + AddToggleStep("Scale Container", state => { fillContainer.ScaleTo(state ? 1.2f : 1f, 1000); }); + AddToggleStep("Shear Container", state => { fillContainer.Shear = state ? new Vector2(0.5f, 0f) : new Vector2(0f, 0f); }); + AddToggleStep("Center Container Anchor", state => { fillContainer.Anchor = state ? Anchor.Centre : Anchor.TopLeft; }); + AddToggleStep("Center Container Origin", state => { fillContainer.Origin = state ? Anchor.Centre : Anchor.TopLeft; }); + AddToggleStep("Autosize Container", state => + { + if (state) + { + fillContainer.RelativeSizeAxes = Axes.None; + fillContainer.AutoSizeAxes = Axes.Both; + } + else + { + fillContainer.AutoSizeAxes = Axes.None; + fillContainer.RelativeSizeAxes = Axes.Both; + fillContainer.Width = 1; + fillContainer.Height = 1; + } + }); + AddToggleStep("Rotate children", state => + { + if (state) + { + foreach (var child in fillContainer.Children) + child.RotateTo(45f, 1000); + } + else + { + foreach (var child in fillContainer.Children) + child.RotateTo(0f, 1000); + } + }); + AddToggleStep("Shear children", state => + { + if (state) + { + foreach (var child in fillContainer.Children) + child.Shear = new Vector2(0.2f, 0.2f); + } + else + { + foreach (var child in fillContainer.Children) + child.Shear = Vector2.Zero; + } + }); + AddToggleStep("Scale children", state => + { + if (state) + { + foreach (var child in fillContainer.Children) + child.ScaleTo(1.25f, 1000); + } + else + { + foreach (var child in fillContainer.Children) + child.ScaleTo(1f, 1000); + } + }); + AddToggleStep("Randomly scale children", state => + { + if (state) + { + foreach (var child in fillContainer.Children) + child.ScaleTo(RNG.NextSingle(1, 2), 1000); + } + else + { + foreach (var child in fillContainer.Children) + child.ScaleTo(1f, 1000); + } + }); + AddToggleStep("Randomly set child origins", state => + { + if (state) + { + foreach (var child in fillContainer.Children) + { + switch (RNG.Next(9)) + { + case 0: child.Origin = Anchor.TopLeft; break; + case 1: child.Origin = Anchor.TopCentre; break; + case 2: child.Origin = Anchor.TopRight; break; + case 3: child.Origin = Anchor.CentreLeft; break; + case 4: child.Origin = Anchor.Centre; break; + case 5: child.Origin = Anchor.CentreRight; break; + case 6: child.Origin = Anchor.BottomLeft; break; + case 7: child.Origin = Anchor.BottomCentre; break; + case 8: child.Origin = Anchor.BottomRight; break; + } + } + } + else + { + foreach (var child in fillContainer.Children) + child.Origin = originDropdown.Current; + } + }); + + AddToggleStep("Stop adding children", state => { doNotAddChildren = state; }); + + scheduledAdder?.Cancel(); + scheduledAdder = Scheduler.AddDelayed( + () => + { + if (fillContainer.Parent == null) + scheduledAdder.Cancel(); + + if (doNotAddChildren) + { + fillContainer.Invalidate(); + } + + if (fillContainer.Children.Count < 1000 && !doNotAddChildren) + { + fillContainer.Add(new Container + { + Anchor = childAnchor, + Origin = childOrigin, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Width = 50, + Height = 50, + Colour = Color4.White + }, + new SpriteText + { + Colour = Color4.Black, + RelativePositionAxes = Axes.Both, + Position = new Vector2(0.5f, 0.5f), + Origin = Anchor.Centre, + Text = fillContainer.Children.Count.ToString() + } + } + }); + } + }, + 100, + true + ); + } + + [FlowTestCase(FlowTestCase.Full)] + private void test1() + { + fillContainer.Direction = FillDirection.Full; + fillContainer.Spacing = new Vector2(5, 5); + } + + [FlowTestCase(FlowTestCase.Horizontal)] + private void test2() + { + fillContainer.Direction = FillDirection.Horizontal; + fillContainer.Spacing = new Vector2(5, 5); + } + + [FlowTestCase(FlowTestCase.Vertical)] + private void test3() + { + fillContainer.Direction = FillDirection.Vertical; + fillContainer.Spacing = new Vector2(5, 5); + } + + private class TestCaseDropdownHeader : DropdownHeader + { + private readonly SpriteText label; + + protected internal override string Label + { + get { return label.Text; } + set { label.Text = value; } + } + + public TestCaseDropdownHeader() + { + Foreground.Padding = new MarginPadding(4); + BackgroundColour = new Color4(100, 100, 100, 255); + BackgroundColourHover = Color4.HotPink; + Children = new[] + { + label = new SpriteText(), + }; + } + } + + private class AnchorDropdown : BasicDropdown + { + protected override DropdownHeader CreateHeader() => new TestCaseDropdownHeader(); + } + + private class AnchorDropdownMenuItem : DropdownMenuItem + { + public AnchorDropdownMenuItem(Anchor anchor) + : base(anchor.ToString(), anchor) + { + } + } + + private class FillDirectionDropdown : BasicDropdown + { + protected override DropdownHeader CreateHeader() => new TestCaseDropdownHeader(); + } + + private class FillDirectionDropdownMenuItem : DropdownMenuItem + { + public FillDirectionDropdownMenuItem(FlowTestCase testCase) + : base(testCase.ToString(), testCase) + { + } + } + + [AttributeUsage(AttributeTargets.Method)] + private class FlowTestCaseAttribute : Attribute + { + public FlowTestCase TestCase { get; } + + public FlowTestCaseAttribute(FlowTestCase testCase) + { + TestCase = testCase; + } + } + + private enum FlowTestCase + { + Full, + Horizontal, + Vertical, + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseFillModes.cs b/osu.Framework.Tests/Visual/TestCaseFillModes.cs index e3ff0372a..c1801a0a0 100644 --- a/osu.Framework.Tests/Visual/TestCaseFillModes.cs +++ b/osu.Framework.Tests/Visual/TestCaseFillModes.cs @@ -1,207 +1,207 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("sprite stretching")] - public class TestCaseFillModes : GridTestCase - { - public TestCaseFillModes() : base(3, 3) - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - FillMode[] fillModes = - { - FillMode.Stretch, - FillMode.Fit, - FillMode.Fill, - }; - - float[] aspects = { 1, 2, 0.5f }; - - for (int i = 0; i < Rows; ++i) - { - for (int j = 0; j < Cols; ++j) - { - Cell(i, j).AddRange(new Drawable[] - { - new SpriteText - { - Text = $"{nameof(FillMode)}=FillMode.{fillModes[i]}, {nameof(FillAspectRatio)}={aspects[j]}", - TextSize = 20, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Blue, - }, - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = texture, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = fillModes[i], - FillAspectRatio = aspects[j], - } - } - } - }); - } - } - } - - private Texture texture; - - [BackgroundDependencyLoader] - private void load(TextureStore store) - { - texture = store.Get(@"sample-texture"); - } - - private class PaddedBox : Container - { - private readonly SpriteText t1; - private readonly SpriteText t2; - private readonly SpriteText t3; - private readonly SpriteText t4; - - private readonly Container content; - - protected override Container Content => content; - - public PaddedBox(Color4 colour) - { - AddRangeInternal(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colour, - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - t1 = new SpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - t2 = new SpriteText - { - Rotation = 90, - Anchor = Anchor.CentreRight, - Origin = Anchor.TopCentre - }, - t3 = new SpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre - }, - t4 = new SpriteText - { - Rotation = -90, - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre - } - }); - - Masking = true; - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - t1.Text = (Padding.Top > 0 ? $"p{Padding.Top}" : string.Empty) + (Margin.Top > 0 ? $"m{Margin.Top}" : string.Empty); - t2.Text = (Padding.Right > 0 ? $"p{Padding.Right}" : string.Empty) + (Margin.Right > 0 ? $"m{Margin.Right}" : string.Empty); - t3.Text = (Padding.Bottom > 0 ? $"p{Padding.Bottom}" : string.Empty) + (Margin.Bottom > 0 ? $"m{Margin.Bottom}" : string.Empty); - t4.Text = (Padding.Left > 0 ? $"p{Padding.Left}" : string.Empty) + (Margin.Left > 0 ? $"m{Margin.Left}" : string.Empty); - - return base.Invalidate(invalidation, source, shallPropagate); - } - - protected override bool OnDrag(InputState state) - { - Position += state.Mouse.Delta; - return true; - } - - protected override bool OnDragEnd(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; - } - - #region Test Cases - - private const float container_width = 60; - private Box fitBox; - - /// - /// Tests that using inside a that is autosizing in one axis doesn't result in autosize feedback loops. - /// Various sizes of the box are tested to ensure that non-one sizes also don't lead to erroneous sizes. - /// - /// The relative size of the box that is fitting. - [TestCase(0f)] - [TestCase(0.5f)] - [TestCase(1f)] - public void TestFitInsideFlow(float value) - { - ClearInternal(); - AddInternal(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - Width = container_width, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - fitBox = new Box - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit - }, - // A box which forces the minimum dimension of the autosize flow container to be the horizontal dimension - new Box { Size = new Vector2(container_width, container_width * 2) } - } - }); - - AddStep("Set size", () => fitBox.Size = new Vector2(value)); - - var expectedSize = new Vector2(container_width * value, container_width * value); - - AddAssert("Check size before invalidate (1/2)", () => fitBox.DrawSize == expectedSize); - AddAssert("Check size before invalidate (2/2)", () => fitBox.DrawSize == expectedSize); - AddStep("Invalidate", () => fitBox.Invalidate()); - AddAssert("Check size after invalidate (1/2)", () => fitBox.DrawSize == expectedSize); - AddAssert("Check size after invalidate (2/2)", () => fitBox.DrawSize == expectedSize); - } - - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("sprite stretching")] + public class TestCaseFillModes : GridTestCase + { + public TestCaseFillModes() : base(3, 3) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FillMode[] fillModes = + { + FillMode.Stretch, + FillMode.Fit, + FillMode.Fill, + }; + + float[] aspects = { 1, 2, 0.5f }; + + for (int i = 0; i < Rows; ++i) + { + for (int j = 0; j < Cols; ++j) + { + Cell(i, j).AddRange(new Drawable[] + { + new SpriteText + { + Text = $"{nameof(FillMode)}=FillMode.{fillModes[i]}, {nameof(FillAspectRatio)}={aspects[j]}", + TextSize = 20, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Blue, + }, + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = texture, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = fillModes[i], + FillAspectRatio = aspects[j], + } + } + } + }); + } + } + } + + private Texture texture; + + [BackgroundDependencyLoader] + private void load(TextureStore store) + { + texture = store.Get(@"sample-texture"); + } + + private class PaddedBox : Container + { + private readonly SpriteText t1; + private readonly SpriteText t2; + private readonly SpriteText t3; + private readonly SpriteText t4; + + private readonly Container content; + + protected override Container Content => content; + + public PaddedBox(Color4 colour) + { + AddRangeInternal(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour, + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, + t1 = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + t2 = new SpriteText + { + Rotation = 90, + Anchor = Anchor.CentreRight, + Origin = Anchor.TopCentre + }, + t3 = new SpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre + }, + t4 = new SpriteText + { + Rotation = -90, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopCentre + } + }); + + Masking = true; + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + t1.Text = (Padding.Top > 0 ? $"p{Padding.Top}" : string.Empty) + (Margin.Top > 0 ? $"m{Margin.Top}" : string.Empty); + t2.Text = (Padding.Right > 0 ? $"p{Padding.Right}" : string.Empty) + (Margin.Right > 0 ? $"m{Margin.Right}" : string.Empty); + t3.Text = (Padding.Bottom > 0 ? $"p{Padding.Bottom}" : string.Empty) + (Margin.Bottom > 0 ? $"m{Margin.Bottom}" : string.Empty); + t4.Text = (Padding.Left > 0 ? $"p{Padding.Left}" : string.Empty) + (Margin.Left > 0 ? $"m{Margin.Left}" : string.Empty); + + return base.Invalidate(invalidation, source, shallPropagate); + } + + protected override bool OnDrag(InputState state) + { + Position += state.Mouse.Delta; + return true; + } + + protected override bool OnDragEnd(InputState state) => true; + + protected override bool OnDragStart(InputState state) => true; + } + + #region Test Cases + + private const float container_width = 60; + private Box fitBox; + + /// + /// Tests that using inside a that is autosizing in one axis doesn't result in autosize feedback loops. + /// Various sizes of the box are tested to ensure that non-one sizes also don't lead to erroneous sizes. + /// + /// The relative size of the box that is fitting. + [TestCase(0f)] + [TestCase(0.5f)] + [TestCase(1f)] + public void TestFitInsideFlow(float value) + { + ClearInternal(); + AddInternal(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = container_width, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + fitBox = new Box + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit + }, + // A box which forces the minimum dimension of the autosize flow container to be the horizontal dimension + new Box { Size = new Vector2(container_width, container_width * 2) } + } + }); + + AddStep("Set size", () => fitBox.Size = new Vector2(value)); + + var expectedSize = new Vector2(container_width * value, container_width * value); + + AddAssert("Check size before invalidate (1/2)", () => fitBox.DrawSize == expectedSize); + AddAssert("Check size before invalidate (2/2)", () => fitBox.DrawSize == expectedSize); + AddStep("Invalidate", () => fitBox.Invalidate()); + AddAssert("Check size after invalidate (1/2)", () => fitBox.DrawSize == expectedSize); + AddAssert("Check size after invalidate (2/2)", () => fitBox.DrawSize == expectedSize); + } + + #endregion + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseGridContainer.cs b/osu.Framework.Tests/Visual/TestCaseGridContainer.cs index 39932e874..522f694b0 100644 --- a/osu.Framework.Tests/Visual/TestCaseGridContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseGridContainer.cs @@ -1,370 +1,370 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.MathUtils; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseGridContainer : TestCase - { - private readonly GridContainer grid; - - public TestCaseGridContainer() - { - Add(new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - grid = new GridContainer { RelativeSizeAxes = Axes.Both } - } - }); - - AddStep("Blank grid", reset); - AddStep("1-cell (auto)", () => - { - reset(); - grid.Content = new[] { new Drawable[] { new FillBox() } }; - }); - - AddStep("1-cell (absolute)", () => - { - reset(); - grid.Content = new[] { new Drawable[] { new FillBox() } }; - grid.RowDimensions = grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Absolute, 100) }; - }); - - AddStep("1-cell (relative)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox() } }; - grid.RowDimensions = grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.5f) }; - }); - - AddStep("1-cell (mixed)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox() } }; - grid.RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 100) }; - grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.5f) }; - }); - - AddStep("1-cell (mixed) 2", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox() } }; - grid.RowDimensions = new [] { new Dimension(GridSizeMode.Relative, 0.5f) }; - }); - - AddStep("3-cell row (auto)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; - }); - - AddStep("3-cell row (absolute)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Absolute, 100), - new Dimension(GridSizeMode.Absolute, 150) - }; - }); - - AddStep("3-cell row (relative)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 0.1f), - new Dimension(GridSizeMode.Relative, 0.2f), - new Dimension(GridSizeMode.Relative, 0.3f) - }; - }); - - AddStep("3-cell row (mixed)", () => - { - reset(); - grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Relative, 0.2f) - }; - }); - - AddStep("3-cell column (auto)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() } - }; - }); - - AddStep("3-cell column (absolute)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Absolute, 100), - new Dimension(GridSizeMode.Absolute, 150) - }; - }); - - AddStep("3-cell column (relative)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 0.1f), - new Dimension(GridSizeMode.Relative, 0.2f), - new Dimension(GridSizeMode.Relative, 0.3f) - }; - }); - - AddStep("3-cell column (mixed)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() }, - new Drawable[] { new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Relative, 0.2f) - }; - }); - - AddStep("3x3-cell (auto)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - }); - - AddStep("3x3-cell (absolute)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Absolute, 100), - new Dimension(GridSizeMode.Absolute, 150) - }; - }); - - AddStep("3x3-cell (relative)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 0.1f), - new Dimension(GridSizeMode.Relative, 0.2f), - new Dimension(GridSizeMode.Relative, 0.3f) - }; - }); - - AddStep("3x3-cell (mixed)", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - - grid.RowDimensions = grid.ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(GridSizeMode.Relative, 0.2f) - }; - }); - - AddStep("Larger sides", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - - grid.ColumnDimensions = grid.RowDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 0.4f), - new Dimension(), - new Dimension(GridSizeMode.Relative, 0.4f) - }; - }); - - AddStep("Separated", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), null, new FillBox() }, - null, - new Drawable[] { new FillBox(), null, new FillBox() } - }; - }); - - AddStep("Separated 2", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new FillBox(), null, new FillBox(), null }, - null, - new Drawable[] { new FillBox(), null, new FillBox(), null }, - null - }; - }); - - AddStep("Nested grids", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] - { - new FillBox(), - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new FillBox(), new FillBox() }, - new Drawable[] - { - new FillBox(), - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox() } - } - } - } - } - }, - new FillBox() - } - }; - }); - - AddStep("Auto size", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] { new Box { Size = new Vector2(30) }, new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, - new Drawable[] { new FillBox(), new FillBox(), new FillBox() } - }; - - grid.RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Relative, 0.5f) }; - grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Relative, 0.5f) }; - }); - - AddStep("Autosizing child", () => - { - reset(); - grid.Content = new[] - { - new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Child = new Box { Size = new Vector2(100, 50) } - }, - new FillBox() - } - }; - - grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }; - }); - } - - private void reset() - { - grid.ClearInternal(); - grid.RowDimensions = grid.ColumnDimensions = new Dimension[] { }; - } - - private class FillBox : Box - { - public FillBox() - { - RelativeSizeAxes = Axes.Both; - Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseGridContainer : TestCase + { + private readonly GridContainer grid; + + public TestCaseGridContainer() + { + Add(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + grid = new GridContainer { RelativeSizeAxes = Axes.Both } + } + }); + + AddStep("Blank grid", reset); + AddStep("1-cell (auto)", () => + { + reset(); + grid.Content = new[] { new Drawable[] { new FillBox() } }; + }); + + AddStep("1-cell (absolute)", () => + { + reset(); + grid.Content = new[] { new Drawable[] { new FillBox() } }; + grid.RowDimensions = grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Absolute, 100) }; + }); + + AddStep("1-cell (relative)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox() } }; + grid.RowDimensions = grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.5f) }; + }); + + AddStep("1-cell (mixed)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox() } }; + grid.RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 100) }; + grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.Relative, 0.5f) }; + }); + + AddStep("1-cell (mixed) 2", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox() } }; + grid.RowDimensions = new [] { new Dimension(GridSizeMode.Relative, 0.5f) }; + }); + + AddStep("3-cell row (auto)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; + }); + + AddStep("3-cell row (absolute)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Absolute, 100), + new Dimension(GridSizeMode.Absolute, 150) + }; + }); + + AddStep("3-cell row (relative)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.1f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.3f) + }; + }); + + AddStep("3-cell row (mixed)", () => + { + reset(); + grid.Content = new [] { new Drawable[] { new FillBox(), new FillBox(), new FillBox() } }; + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Relative, 0.2f) + }; + }); + + AddStep("3-cell column (auto)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() } + }; + }); + + AddStep("3-cell column (absolute)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Absolute, 100), + new Dimension(GridSizeMode.Absolute, 150) + }; + }); + + AddStep("3-cell column (relative)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.1f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.3f) + }; + }); + + AddStep("3-cell column (mixed)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() }, + new Drawable[] { new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Relative, 0.2f) + }; + }); + + AddStep("3x3-cell (auto)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + }); + + AddStep("3x3-cell (absolute)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Absolute, 100), + new Dimension(GridSizeMode.Absolute, 150) + }; + }); + + AddStep("3x3-cell (relative)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.1f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.3f) + }; + }); + + AddStep("3x3-cell (mixed)", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + + grid.RowDimensions = grid.ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Relative, 0.2f) + }; + }); + + AddStep("Larger sides", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + + grid.ColumnDimensions = grid.RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.4f), + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.4f) + }; + }); + + AddStep("Separated", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), null, new FillBox() }, + null, + new Drawable[] { new FillBox(), null, new FillBox() } + }; + }); + + AddStep("Separated 2", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new FillBox(), null, new FillBox(), null }, + null, + new Drawable[] { new FillBox(), null, new FillBox(), null }, + null + }; + }); + + AddStep("Nested grids", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] + { + new FillBox(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new FillBox(), new FillBox() }, + new Drawable[] + { + new FillBox(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox() } + } + } + } + } + }, + new FillBox() + } + }; + }); + + AddStep("Auto size", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] { new Box { Size = new Vector2(30) }, new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() }, + new Drawable[] { new FillBox(), new FillBox(), new FillBox() } + }; + + grid.RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Relative, 0.5f) }; + grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Relative, 0.5f) }; + }); + + AddStep("Autosizing child", () => + { + reset(); + grid.Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Child = new Box { Size = new Vector2(100, 50) } + }, + new FillBox() + } + }; + + grid.ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }; + }); + } + + private void reset() + { + grid.ClearInternal(); + grid.RowDimensions = grid.ColumnDimensions = new Dimension[] { }; + } + + private class FillBox : Box + { + public FillBox() + { + RelativeSizeAxes = Axes.Both; + Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseHollowEdgeEffect.cs b/osu.Framework.Tests/Visual/TestCaseHollowEdgeEffect.cs index 8fa960b58..0fe89b73c 100644 --- a/osu.Framework.Tests/Visual/TestCaseHollowEdgeEffect.cs +++ b/osu.Framework.Tests/Visual/TestCaseHollowEdgeEffect.cs @@ -1,87 +1,87 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseHollowEdgeEffect : GridTestCase - { - public TestCaseHollowEdgeEffect() : base(2, 2) - { - const float size = 60; - - float[] cornerRadii = { 0, 0.5f, 0, 0.5f }; - float[] alphas = { 0.5f, 0.5f, 0, 0 }; - EdgeEffectParameters[] edgeEffects = - { - new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Color4.Khaki, - Radius = size, - Hollow = true, - }, - new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Color4.Khaki, - Radius = size, - Hollow = true, - }, - new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Color4.Khaki, - Radius = size, - Hollow = true, - }, - new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Color4.Khaki, - Radius = size, - Hollow = true, - }, - }; - - for (int i = 0; i < Rows * Cols; ++i) - { - Cell(i).AddRange(new Drawable[] - { - new SpriteText - { - Text = $"{nameof(CornerRadius)}={cornerRadii[i]} {nameof(Alpha)}={alphas[i]}", - TextSize = 20, - }, - new Container - { - Size = new Vector2(size), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - Masking = true, - EdgeEffect = edgeEffects[i], - CornerRadius = cornerRadii[i] * size, - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Aqua, - Alpha = alphas[i], - }, - }, - }, - }); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseHollowEdgeEffect : GridTestCase + { + public TestCaseHollowEdgeEffect() : base(2, 2) + { + const float size = 60; + + float[] cornerRadii = { 0, 0.5f, 0, 0.5f }; + float[] alphas = { 0.5f, 0.5f, 0, 0 }; + EdgeEffectParameters[] edgeEffects = + { + new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Color4.Khaki, + Radius = size, + Hollow = true, + }, + new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Color4.Khaki, + Radius = size, + Hollow = true, + }, + new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Color4.Khaki, + Radius = size, + Hollow = true, + }, + new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Color4.Khaki, + Radius = size, + Hollow = true, + }, + }; + + for (int i = 0; i < Rows * Cols; ++i) + { + Cell(i).AddRange(new Drawable[] + { + new SpriteText + { + Text = $"{nameof(CornerRadius)}={cornerRadii[i]} {nameof(Alpha)}={alphas[i]}", + TextSize = 20, + }, + new Container + { + Size = new Vector2(size), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + + Masking = true, + EdgeEffect = edgeEffects[i], + CornerRadius = cornerRadii[i] * size, + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Aqua, + Alpha = alphas[i], + }, + }, + }, + }); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseInputResampler.cs b/osu.Framework.Tests/Visual/TestCaseInputResampler.cs index 375ffe3a4..383b4c5f5 100644 --- a/osu.Framework.Tests/Visual/TestCaseInputResampler.cs +++ b/osu.Framework.Tests/Visual/TestCaseInputResampler.cs @@ -1,208 +1,208 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("live path optimiastion")] - public class TestCaseInputResampler : GridTestCase - { - public TestCaseInputResampler() : base(3, 3) - { - const int width = 2; - Texture gradientTexture = new Texture(width, 1, true); - byte[] data = new byte[width * 4]; - for (int i = 0; i < width; ++i) - { - float brightness = (float)i / (width - 1); - int index = i * 4; - data[index + 0] = (byte)(brightness * 255); - data[index + 1] = (byte)(brightness * 255); - data[index + 2] = (byte)(brightness * 255); - data[index + 3] = 255; - } - gradientTexture.SetData(new TextureUpload(data)); - - SpriteText[] text = new SpriteText[6]; - - Cell(0, 0).AddRange(new Drawable[] - { - text[0] = createLabel("Raw"), - new ArcPath(true, true, new InputResampler(), gradientTexture, Color4.Green, text[0]), - }); - - Cell(0, 1).AddRange(new Drawable[] - { - text[1] = createLabel("Rounded (resembles mouse input)"), - new ArcPath(true, false, new InputResampler(), gradientTexture, Color4.Blue, text[1]), - }); - - Cell(0, 2).AddRange(new Drawable[] - { - text[2] = createLabel("Custom: Smoothed=0, Raw=0"), - new UserDrawnPath - { - DrawText = text[2], - RelativeSizeAxes = Axes.Both, - Texture = gradientTexture, - Colour = Color4.White, - }, - }); - - Cell(1, 0).AddRange(new Drawable[] - { - text[3] = createLabel("Smoothed raw"), - new ArcPath(false, true, new InputResampler(), gradientTexture, Color4.Green, text[3]), - }); - - Cell(1, 1).AddRange(new Drawable[] - { - text[4] = createLabel("Smoothed rounded"), - new ArcPath(false, false, new InputResampler(), gradientTexture, Color4.Blue, text[4]), - }); - - Cell(1, 2).AddRange(new Drawable[] - { - text[5] = createLabel("Smoothed custom: Smoothed=0, Raw=0"), - new SmoothedUserDrawnPath - { - DrawText = text[5], - RelativeSizeAxes = Axes.Both, - Texture = gradientTexture, - Colour = Color4.White, - InputResampler = new InputResampler(), - }, - }); - - Cell(2, 0).AddRange(new Drawable[] - { - text[3] = createLabel("Force-smoothed raw"), - new ArcPath(false, true, new InputResampler { ResampleRawInput = true }, gradientTexture, Color4.Green, text[3]), - }); - - Cell(2, 1).AddRange(new Drawable[] - { - text[4] = createLabel("Force-smoothed rounded"), - new ArcPath(false, false, new InputResampler { ResampleRawInput = true }, gradientTexture, Color4.Blue, text[4]), - }); - - Cell(2, 2).AddRange(new Drawable[] - { - text[5] = createLabel("Force-smoothed custom: Smoothed=0, Raw=0"), - new SmoothedUserDrawnPath - { - DrawText = text[5], - RelativeSizeAxes = Axes.Both, - Texture = gradientTexture, - Colour = Color4.White, - InputResampler = new InputResampler - { - ResampleRawInput = true - }, - }, - }); - } - - private SpriteText createLabel(string text) => new SpriteText - { - Text = text, - TextSize = 14, - Colour = Color4.White, - }; - - private class SmoothedPath : Path - { - protected SmoothedPath() - { - PathWidth = 2; - } - - public InputResampler InputResampler { get; set; } = new InputResampler(); - - protected int NumVertices { get; set; } - - protected int NumRaw { get; set; } - - protected void AddRawVertex(Vector2 pos) - { - NumRaw++; - AddVertex(pos); - NumVertices++; - } - - protected bool AddSmoothedVertex(Vector2 pos) - { - NumRaw++; - bool foundOne = false; - foreach (Vector2 relevant in InputResampler.AddPosition(pos)) - { - AddVertex(relevant); - NumVertices++; - foundOne = true; - } - return foundOne; - } - } - - private class ArcPath : SmoothedPath - { - public ArcPath(bool raw, bool keepFraction, InputResampler inputResampler, Texture texture, Color4 colour, SpriteText output) - { - InputResampler = inputResampler; - const int target_raw = 1024; - RelativeSizeAxes = Axes.Both; - Texture = texture; - Colour = colour; - - for (int i = 0; i < target_raw; i++) - { - float x = (float)(Math.Sin(i / (double)target_raw * (Math.PI * 0.5)) * 200) + 50.5f; - float y = (float)(Math.Cos(i / (double)target_raw * (Math.PI * 0.5)) * 200) + 50.5f; - Vector2 v = keepFraction ? new Vector2(x, y) : new Vector2((int)x, (int)y); - if (raw) - AddRawVertex(v); - else - AddSmoothedVertex(v); - } - - output.Text += ": Smoothed=" + NumVertices + ", Raw=" + NumRaw; - } - } - - private class UserDrawnPath : SmoothedPath - { - public SpriteText DrawText; - - protected virtual void AddUserVertex(Vector2 v) => AddRawVertex(v); - - protected override bool OnDragStart(InputState state) - { - AddUserVertex(state.Mouse.Position); - DrawText.Text = "Custom Smoothed Drawn: Smoothed=" + NumVertices + ", Raw=" + NumRaw; - return true; - } - - protected override bool OnDrag(InputState state) - { - AddUserVertex(state.Mouse.Position); - DrawText.Text = "Custom Smoothed Drawn: Smoothed=" + NumVertices + ", Raw=" + NumRaw; - return base.OnDrag(state); - } - } - - private class SmoothedUserDrawnPath : UserDrawnPath - { - protected override void AddUserVertex(Vector2 v) => AddSmoothedVertex(v); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("live path optimiastion")] + public class TestCaseInputResampler : GridTestCase + { + public TestCaseInputResampler() : base(3, 3) + { + const int width = 2; + Texture gradientTexture = new Texture(width, 1, true); + byte[] data = new byte[width * 4]; + for (int i = 0; i < width; ++i) + { + float brightness = (float)i / (width - 1); + int index = i * 4; + data[index + 0] = (byte)(brightness * 255); + data[index + 1] = (byte)(brightness * 255); + data[index + 2] = (byte)(brightness * 255); + data[index + 3] = 255; + } + gradientTexture.SetData(new TextureUpload(data)); + + SpriteText[] text = new SpriteText[6]; + + Cell(0, 0).AddRange(new Drawable[] + { + text[0] = createLabel("Raw"), + new ArcPath(true, true, new InputResampler(), gradientTexture, Color4.Green, text[0]), + }); + + Cell(0, 1).AddRange(new Drawable[] + { + text[1] = createLabel("Rounded (resembles mouse input)"), + new ArcPath(true, false, new InputResampler(), gradientTexture, Color4.Blue, text[1]), + }); + + Cell(0, 2).AddRange(new Drawable[] + { + text[2] = createLabel("Custom: Smoothed=0, Raw=0"), + new UserDrawnPath + { + DrawText = text[2], + RelativeSizeAxes = Axes.Both, + Texture = gradientTexture, + Colour = Color4.White, + }, + }); + + Cell(1, 0).AddRange(new Drawable[] + { + text[3] = createLabel("Smoothed raw"), + new ArcPath(false, true, new InputResampler(), gradientTexture, Color4.Green, text[3]), + }); + + Cell(1, 1).AddRange(new Drawable[] + { + text[4] = createLabel("Smoothed rounded"), + new ArcPath(false, false, new InputResampler(), gradientTexture, Color4.Blue, text[4]), + }); + + Cell(1, 2).AddRange(new Drawable[] + { + text[5] = createLabel("Smoothed custom: Smoothed=0, Raw=0"), + new SmoothedUserDrawnPath + { + DrawText = text[5], + RelativeSizeAxes = Axes.Both, + Texture = gradientTexture, + Colour = Color4.White, + InputResampler = new InputResampler(), + }, + }); + + Cell(2, 0).AddRange(new Drawable[] + { + text[3] = createLabel("Force-smoothed raw"), + new ArcPath(false, true, new InputResampler { ResampleRawInput = true }, gradientTexture, Color4.Green, text[3]), + }); + + Cell(2, 1).AddRange(new Drawable[] + { + text[4] = createLabel("Force-smoothed rounded"), + new ArcPath(false, false, new InputResampler { ResampleRawInput = true }, gradientTexture, Color4.Blue, text[4]), + }); + + Cell(2, 2).AddRange(new Drawable[] + { + text[5] = createLabel("Force-smoothed custom: Smoothed=0, Raw=0"), + new SmoothedUserDrawnPath + { + DrawText = text[5], + RelativeSizeAxes = Axes.Both, + Texture = gradientTexture, + Colour = Color4.White, + InputResampler = new InputResampler + { + ResampleRawInput = true + }, + }, + }); + } + + private SpriteText createLabel(string text) => new SpriteText + { + Text = text, + TextSize = 14, + Colour = Color4.White, + }; + + private class SmoothedPath : Path + { + protected SmoothedPath() + { + PathWidth = 2; + } + + public InputResampler InputResampler { get; set; } = new InputResampler(); + + protected int NumVertices { get; set; } + + protected int NumRaw { get; set; } + + protected void AddRawVertex(Vector2 pos) + { + NumRaw++; + AddVertex(pos); + NumVertices++; + } + + protected bool AddSmoothedVertex(Vector2 pos) + { + NumRaw++; + bool foundOne = false; + foreach (Vector2 relevant in InputResampler.AddPosition(pos)) + { + AddVertex(relevant); + NumVertices++; + foundOne = true; + } + return foundOne; + } + } + + private class ArcPath : SmoothedPath + { + public ArcPath(bool raw, bool keepFraction, InputResampler inputResampler, Texture texture, Color4 colour, SpriteText output) + { + InputResampler = inputResampler; + const int target_raw = 1024; + RelativeSizeAxes = Axes.Both; + Texture = texture; + Colour = colour; + + for (int i = 0; i < target_raw; i++) + { + float x = (float)(Math.Sin(i / (double)target_raw * (Math.PI * 0.5)) * 200) + 50.5f; + float y = (float)(Math.Cos(i / (double)target_raw * (Math.PI * 0.5)) * 200) + 50.5f; + Vector2 v = keepFraction ? new Vector2(x, y) : new Vector2((int)x, (int)y); + if (raw) + AddRawVertex(v); + else + AddSmoothedVertex(v); + } + + output.Text += ": Smoothed=" + NumVertices + ", Raw=" + NumRaw; + } + } + + private class UserDrawnPath : SmoothedPath + { + public SpriteText DrawText; + + protected virtual void AddUserVertex(Vector2 v) => AddRawVertex(v); + + protected override bool OnDragStart(InputState state) + { + AddUserVertex(state.Mouse.Position); + DrawText.Text = "Custom Smoothed Drawn: Smoothed=" + NumVertices + ", Raw=" + NumRaw; + return true; + } + + protected override bool OnDrag(InputState state) + { + AddUserVertex(state.Mouse.Position); + DrawText.Text = "Custom Smoothed Drawn: Smoothed=" + NumVertices + ", Raw=" + NumRaw; + return base.OnDrag(state); + } + } + + private class SmoothedUserDrawnPath : UserDrawnPath + { + protected override void AddUserVertex(Vector2 v) => AddSmoothedVertex(v); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseIsMaskedAway.cs b/osu.Framework.Tests/Visual/TestCaseIsMaskedAway.cs index c9902acf8..0f010515c 100644 --- a/osu.Framework.Tests/Visual/TestCaseIsMaskedAway.cs +++ b/osu.Framework.Tests/Visual/TestCaseIsMaskedAway.cs @@ -1,341 +1,341 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseIsMaskedAway : TestCase - { - /// - /// Tests that a box which is within the bounds of a parent is never masked away, regardless of whether the parent is masking or not. - /// - /// Whether the box's parent is masking. - [TestCase(false)] - [TestCase(true)] - public void TestBoxInBounds(bool masking) - { - Box box; - Child = new Container - { - Size = new Vector2(200), - Masking = masking, - Child = box = new Box() - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - } - - /// - /// Tests that a box which is outside the bounds of a parent is never masked away if the parent is not masking. - /// - [Test] - public void TestBoxOutOfBoundsNoMasking() - { - Box box; - Child = new Container - { - Size = new Vector2(200), - Child = box = new Box { Position = new Vector2(-1) } - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - } - - /// - /// Tests that a box which is slightly outside the bounds of a masking parent is never masked away, regardless of its anchor/origin. - /// Ensures that all screen-space calculations are current by the time is calculated. - /// - /// The box's anchor in the masking parent. - [TestCase(Anchor.TopLeft)] - [TestCase(Anchor.TopRight)] - [TestCase(Anchor.BottomLeft)] - [TestCase(Anchor.BottomRight)] - public void TestBoxSlightlyOutOfBoundsMasking(Anchor anchor) - { - Box box; - Child = new Container - { - Size = new Vector2(200), - Masking = true, - Child = box = new Box - { - Anchor = anchor, - Origin = anchor, - Size = new Vector2(10), - Position = new Vector2((anchor & Anchor.x0) > 0 ? -5 : 5, (anchor & Anchor.y0) > 0 ? -5 : 5), - } - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - } - - /// - /// Tests that a box which is fully outside the bounds of a masking parent is always masked away, regardless of its anchor/origin. - /// Ensures that all screen-space calculations are current by the time is calculated. - /// - /// The box's anchor in the masking parent. - [TestCase(Anchor.TopLeft)] - [TestCase(Anchor.TopRight)] - [TestCase(Anchor.BottomLeft)] - [TestCase(Anchor.BottomRight)] - public void TestBoxFullyOutOfBoundsMasking(Anchor anchor) - { - Box box; - Child = new Container - { - Size = new Vector2(200), - Masking = true, - Child = box = new Box - { - Anchor = anchor, - Origin = anchor, - Size = new Vector2(10), - Position = new Vector2((anchor & Anchor.x0) > 0 ? -20 : 20, (anchor & Anchor.y0) > 0 ? -20 : 20), - } - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway); - } - - /// - /// Tests that a box is never masked away when it and its proxy are within the bounds of their parents, regardless of whether their parents - /// are masking or not. - /// - /// Whether the box's parent is masking. - /// Whether the proxy's parent is masking. - [TestCase(false, false)] - [TestCase(true, true)] - public void TestBoxInBoundsWithProxyInBounds(bool boxMasking, bool proxyMasking) - { - var box = new Box(); - - ProxyDrawable proxy; - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = boxMasking, - Child = box - }, - new Container - { - Size = new Vector2(200), - Masking = proxyMasking, - Child = proxy = box.CreateProxy() - }, - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); - } - - /// - /// Tests that a box is never masked away when its proxy is within the bounds of its parent, even if the box is outside the bounds of its parent. - /// - /// Whether the box's parent is masking. This does not affect the proxy's parent. - [TestCase(false)] - [TestCase(true)] - public void TestBoxOutOfBoundsWithProxyInBounds(bool masking) - { - var box = new Box { Position = new Vector2(-1) }; - - ProxyDrawable proxy; - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = masking, - Child = box - }, - proxy = box.CreateProxy() - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); - } - - /// - /// Tests that a box is only masked away when its proxy is masked away. - /// - /// Whether the box's parent is masking. - /// Whether the proxy's parent is masking. - /// Whether the box should be masked away. - [TestCase(false, false, false)] - [TestCase(false, true, true)] - [TestCase(true, false, false)] - [TestCase(true, true, true)] - public void TestBoxInBoundsWithProxyOutOfBounds(bool boxMasking, bool proxyMasking, bool shouldBeMaskedAway) - { - var box = new Box(); - - ProxyDrawable proxy; - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = boxMasking, - Child = box - }, - new Container - { - Position = new Vector2(10), - Size = new Vector2(200), - Masking = proxyMasking, - Child = proxy = box.CreateProxy() - }, - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); - AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); - } - - /// - /// Tests that whether the box is out of bounds of its parent is not a consideration for masking, only whether its proxy is out of bounds of its parent. - /// - /// Whether the box's parent is masking. - /// Whether the proxy's parent is masking. - /// Whether the box should be masked away - [TestCase(false, false, false)] - [TestCase(false, true, true)] - [TestCase(true, false, false)] - [TestCase(true, true, true)] - public void TestBoxOutOfBoundsWithProxyOutOfBounds(bool boxMasking, bool proxyMasking, bool shouldBeMaskedAway) - { - var box = new Box { Position = new Vector2(-1) }; - - ProxyDrawable proxy; - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = boxMasking, - Child = box - }, - new Container - { - Position = new Vector2(10), - Size = new Vector2(200), - Masking = proxyMasking, - Child = proxy = box.CreateProxy() - }, - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); - AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); - } - - /// - /// Tests that the box doesn't get masked away unless its most-proxied-proxy is masked away. - /// In this case, the most-proxied-proxy is never going to be masked away, because it is within the bounds of its parent. - /// - /// Whether the box's parent is masking. - /// Whether the parent of box's proxy is masking. - /// Whether the parent of the proxy's proxy is masking. - [TestCase(false, false, false)] - [TestCase(true, false, false)] - [TestCase(true, true, false)] - [TestCase(true, true, true)] - [TestCase(false, true, true)] - public void TestBoxInBoundsWithProxy1OutOfBoundsWithProxy2InBounds(bool boxMasking, bool proxy1Masking, bool proxy2Masking) - { - var box = new Box(); - var proxy1 = box.CreateProxy(); - var proxy2 = proxy1.CreateProxy(); - - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = boxMasking, - Child = box - }, - new Container - { - Size = new Vector2(200), - Position = new Vector2(10), - Masking = proxy1Masking, - Child = proxy1 - }, - new Container - { - Size = new Vector2(200), - Masking = proxy2Masking, - Child = proxy2 - } - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); - AddAssert("Check proxy1 IsMaskedAway", () => !proxy1.IsMaskedAway); - AddAssert("Check proxy2 IsMaskedAway", () => !proxy2.IsMaskedAway); - } - - /// - /// Tests that whether the box is out of bounds of its parent is not a consideration for masking, only whether its most-proxied-proxy is out of bounds of its parent, - /// and the most-proxied-proxy's parent is masking. - /// - /// Whether the box's parent is masking. - /// Whether the parent of box's proxy is masking. - /// Whether the parent of the proxy's proxy is masking. - /// Whether the box should be masked away. - [TestCase(false, false, false, false)] - [TestCase(true, false, false, false)] - [TestCase(true, true, false, false)] - [TestCase(true, true, true, true)] - [TestCase(false, true, true, true)] - [TestCase(false, false, true, true)] - public void TestBoxInBoundsWithProxy1OutOfBoundsWithProxy2OutOfBounds(bool boxMasking, bool proxy1Masking, bool proxy2Masking, bool shouldBeMaskedAway) - { - var box = new Box(); - var proxy1 = box.CreateProxy(); - var proxy2 = proxy1.CreateProxy(); - - Children = new Drawable[] - { - new Container - { - Size = new Vector2(200), - Masking = boxMasking, - Child = box - }, - new Container - { - Size = new Vector2(200), - Masking = proxy1Masking, - Child = proxy1 - }, - new Container - { - Size = new Vector2(200), - Position = new Vector2(10), - Masking = proxy2Masking, - Child = proxy2 - } - }; - - AddWaitStep(1, "Wait for UpdateSubTree"); - AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); - AddAssert("Check proxy1 IsMaskedAway", () => !proxy1.IsMaskedAway); - AddAssert("Check proxy2 IsMaskedAway", () => !proxy2.IsMaskedAway); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseIsMaskedAway : TestCase + { + /// + /// Tests that a box which is within the bounds of a parent is never masked away, regardless of whether the parent is masking or not. + /// + /// Whether the box's parent is masking. + [TestCase(false)] + [TestCase(true)] + public void TestBoxInBounds(bool masking) + { + Box box; + Child = new Container + { + Size = new Vector2(200), + Masking = masking, + Child = box = new Box() + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + } + + /// + /// Tests that a box which is outside the bounds of a parent is never masked away if the parent is not masking. + /// + [Test] + public void TestBoxOutOfBoundsNoMasking() + { + Box box; + Child = new Container + { + Size = new Vector2(200), + Child = box = new Box { Position = new Vector2(-1) } + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + } + + /// + /// Tests that a box which is slightly outside the bounds of a masking parent is never masked away, regardless of its anchor/origin. + /// Ensures that all screen-space calculations are current by the time is calculated. + /// + /// The box's anchor in the masking parent. + [TestCase(Anchor.TopLeft)] + [TestCase(Anchor.TopRight)] + [TestCase(Anchor.BottomLeft)] + [TestCase(Anchor.BottomRight)] + public void TestBoxSlightlyOutOfBoundsMasking(Anchor anchor) + { + Box box; + Child = new Container + { + Size = new Vector2(200), + Masking = true, + Child = box = new Box + { + Anchor = anchor, + Origin = anchor, + Size = new Vector2(10), + Position = new Vector2((anchor & Anchor.x0) > 0 ? -5 : 5, (anchor & Anchor.y0) > 0 ? -5 : 5), + } + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + } + + /// + /// Tests that a box which is fully outside the bounds of a masking parent is always masked away, regardless of its anchor/origin. + /// Ensures that all screen-space calculations are current by the time is calculated. + /// + /// The box's anchor in the masking parent. + [TestCase(Anchor.TopLeft)] + [TestCase(Anchor.TopRight)] + [TestCase(Anchor.BottomLeft)] + [TestCase(Anchor.BottomRight)] + public void TestBoxFullyOutOfBoundsMasking(Anchor anchor) + { + Box box; + Child = new Container + { + Size = new Vector2(200), + Masking = true, + Child = box = new Box + { + Anchor = anchor, + Origin = anchor, + Size = new Vector2(10), + Position = new Vector2((anchor & Anchor.x0) > 0 ? -20 : 20, (anchor & Anchor.y0) > 0 ? -20 : 20), + } + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway); + } + + /// + /// Tests that a box is never masked away when it and its proxy are within the bounds of their parents, regardless of whether their parents + /// are masking or not. + /// + /// Whether the box's parent is masking. + /// Whether the proxy's parent is masking. + [TestCase(false, false)] + [TestCase(true, true)] + public void TestBoxInBoundsWithProxyInBounds(bool boxMasking, bool proxyMasking) + { + var box = new Box(); + + ProxyDrawable proxy; + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = boxMasking, + Child = box + }, + new Container + { + Size = new Vector2(200), + Masking = proxyMasking, + Child = proxy = box.CreateProxy() + }, + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); + } + + /// + /// Tests that a box is never masked away when its proxy is within the bounds of its parent, even if the box is outside the bounds of its parent. + /// + /// Whether the box's parent is masking. This does not affect the proxy's parent. + [TestCase(false)] + [TestCase(true)] + public void TestBoxOutOfBoundsWithProxyInBounds(bool masking) + { + var box = new Box { Position = new Vector2(-1) }; + + ProxyDrawable proxy; + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = masking, + Child = box + }, + proxy = box.CreateProxy() + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); + } + + /// + /// Tests that a box is only masked away when its proxy is masked away. + /// + /// Whether the box's parent is masking. + /// Whether the proxy's parent is masking. + /// Whether the box should be masked away. + [TestCase(false, false, false)] + [TestCase(false, true, true)] + [TestCase(true, false, false)] + [TestCase(true, true, true)] + public void TestBoxInBoundsWithProxyOutOfBounds(bool boxMasking, bool proxyMasking, bool shouldBeMaskedAway) + { + var box = new Box(); + + ProxyDrawable proxy; + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = boxMasking, + Child = box + }, + new Container + { + Position = new Vector2(10), + Size = new Vector2(200), + Masking = proxyMasking, + Child = proxy = box.CreateProxy() + }, + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); + AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); + } + + /// + /// Tests that whether the box is out of bounds of its parent is not a consideration for masking, only whether its proxy is out of bounds of its parent. + /// + /// Whether the box's parent is masking. + /// Whether the proxy's parent is masking. + /// Whether the box should be masked away + [TestCase(false, false, false)] + [TestCase(false, true, true)] + [TestCase(true, false, false)] + [TestCase(true, true, true)] + public void TestBoxOutOfBoundsWithProxyOutOfBounds(bool boxMasking, bool proxyMasking, bool shouldBeMaskedAway) + { + var box = new Box { Position = new Vector2(-1) }; + + ProxyDrawable proxy; + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = boxMasking, + Child = box + }, + new Container + { + Position = new Vector2(10), + Size = new Vector2(200), + Masking = proxyMasking, + Child = proxy = box.CreateProxy() + }, + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); + AddAssert("Check proxy IsMaskedAway", () => !proxy.IsMaskedAway); + } + + /// + /// Tests that the box doesn't get masked away unless its most-proxied-proxy is masked away. + /// In this case, the most-proxied-proxy is never going to be masked away, because it is within the bounds of its parent. + /// + /// Whether the box's parent is masking. + /// Whether the parent of box's proxy is masking. + /// Whether the parent of the proxy's proxy is masking. + [TestCase(false, false, false)] + [TestCase(true, false, false)] + [TestCase(true, true, false)] + [TestCase(true, true, true)] + [TestCase(false, true, true)] + public void TestBoxInBoundsWithProxy1OutOfBoundsWithProxy2InBounds(bool boxMasking, bool proxy1Masking, bool proxy2Masking) + { + var box = new Box(); + var proxy1 = box.CreateProxy(); + var proxy2 = proxy1.CreateProxy(); + + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = boxMasking, + Child = box + }, + new Container + { + Size = new Vector2(200), + Position = new Vector2(10), + Masking = proxy1Masking, + Child = proxy1 + }, + new Container + { + Size = new Vector2(200), + Masking = proxy2Masking, + Child = proxy2 + } + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => !box.IsMaskedAway); + AddAssert("Check proxy1 IsMaskedAway", () => !proxy1.IsMaskedAway); + AddAssert("Check proxy2 IsMaskedAway", () => !proxy2.IsMaskedAway); + } + + /// + /// Tests that whether the box is out of bounds of its parent is not a consideration for masking, only whether its most-proxied-proxy is out of bounds of its parent, + /// and the most-proxied-proxy's parent is masking. + /// + /// Whether the box's parent is masking. + /// Whether the parent of box's proxy is masking. + /// Whether the parent of the proxy's proxy is masking. + /// Whether the box should be masked away. + [TestCase(false, false, false, false)] + [TestCase(true, false, false, false)] + [TestCase(true, true, false, false)] + [TestCase(true, true, true, true)] + [TestCase(false, true, true, true)] + [TestCase(false, false, true, true)] + public void TestBoxInBoundsWithProxy1OutOfBoundsWithProxy2OutOfBounds(bool boxMasking, bool proxy1Masking, bool proxy2Masking, bool shouldBeMaskedAway) + { + var box = new Box(); + var proxy1 = box.CreateProxy(); + var proxy2 = proxy1.CreateProxy(); + + Children = new Drawable[] + { + new Container + { + Size = new Vector2(200), + Masking = boxMasking, + Child = box + }, + new Container + { + Size = new Vector2(200), + Masking = proxy1Masking, + Child = proxy1 + }, + new Container + { + Size = new Vector2(200), + Position = new Vector2(10), + Masking = proxy2Masking, + Child = proxy2 + } + }; + + AddWaitStep(1, "Wait for UpdateSubTree"); + AddAssert("Check box IsMaskedAway", () => box.IsMaskedAway == shouldBeMaskedAway); + AddAssert("Check proxy1 IsMaskedAway", () => !proxy1.IsMaskedAway); + AddAssert("Check proxy2 IsMaskedAway", () => !proxy2.IsMaskedAway); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseKeyBindings.cs b/osu.Framework.Tests/Visual/TestCaseKeyBindings.cs index 7e9772f17..ee0c02db2 100644 --- a/osu.Framework.Tests/Visual/TestCaseKeyBindings.cs +++ b/osu.Framework.Tests/Visual/TestCaseKeyBindings.cs @@ -1,165 +1,165 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Bindings; -using osu.Framework.Testing; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseKeyBindings : GridTestCase - { - public TestCaseKeyBindings() - : base(2, 2) - { - - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Cell(0).Add(new KeyBindingTester(SimultaneousBindingMode.None)); - Cell(1).Add(new KeyBindingTester(SimultaneousBindingMode.Unique)); - Cell(2).Add(new KeyBindingTester(SimultaneousBindingMode.All)); - } - - private enum TestAction - { - A, - S, - D_or_F, - Ctrl_A, - Ctrl_S, - Ctrl_D_or_F, - Shift_A, - Shift_S, - Shift_D_or_F, - Ctrl_Shift_A, - Ctrl_Shift_S, - Ctrl_Shift_D_or_F, - Ctrl, - Shift, - Ctrl_And_Shift, - Ctrl_Or_Shift, - LeftMouse, - RightMouse - } - - private class TestInputManager : KeyBindingContainer - { - public TestInputManager(SimultaneousBindingMode concurrencyMode = SimultaneousBindingMode.None) : base(concurrencyMode) - { - } - - public override IEnumerable DefaultKeyBindings => new[] - { - new KeyBinding(InputKey.A, TestAction.A ), - new KeyBinding(InputKey.S, TestAction.S ), - new KeyBinding(InputKey.D, TestAction.D_or_F ), - new KeyBinding(InputKey.F, TestAction.D_or_F ), - - new KeyBinding(new[] { InputKey.Control, InputKey.A }, TestAction.Ctrl_A ), - new KeyBinding(new[] { InputKey.Control, InputKey.S }, TestAction.Ctrl_S ), - new KeyBinding(new[] { InputKey.Control, InputKey.D }, TestAction.Ctrl_D_or_F ), - new KeyBinding(new[] { InputKey.Control, InputKey.F }, TestAction.Ctrl_D_or_F ), - - new KeyBinding(new[] { InputKey.Shift, InputKey.A }, TestAction.Shift_A ), - new KeyBinding(new[] { InputKey.Shift, InputKey.S }, TestAction.Shift_S ), - new KeyBinding(new[] { InputKey.Shift, InputKey.D }, TestAction.Shift_D_or_F ), - new KeyBinding(new[] { InputKey.Shift, InputKey.F }, TestAction.Shift_D_or_F ), - - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, TestAction.Ctrl_Shift_A ), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, TestAction.Ctrl_Shift_S), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.D }, TestAction.Ctrl_Shift_D_or_F), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, TestAction.Ctrl_Shift_D_or_F), - - new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl), - new KeyBinding(new[] { InputKey.Shift }, TestAction.Shift), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift }, TestAction.Ctrl_And_Shift), - new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl_Or_Shift), - new KeyBinding(new[] { InputKey.Shift }, TestAction.Ctrl_Or_Shift), - - new KeyBinding(new[] { InputKey.MouseLeft }, TestAction.LeftMouse), - new KeyBinding(new[] { InputKey.MouseRight }, TestAction.RightMouse), - }; - } - - private class TestButton : Button, IKeyBindingHandler - { - private readonly TestAction action; - - public TestButton(TestAction action) - { - this.action = action; - - BackgroundColour = Color4.SkyBlue; - Text = action.ToString().Replace('_', ' '); - - RelativeSizeAxes = Axes.X; - Height = 40; - Width = 0.3f; - Padding = new MarginPadding(2); - - Background.Alpha = alphaTarget; - } - - private float alphaTarget = 0.5f; - - public bool OnPressed(TestAction action) - { - if (this.action == action) - { - alphaTarget += 0.2f; - Background.FadeTo(alphaTarget, 100, Easing.OutQuint); - } - - return false; - } - - public bool OnReleased(TestAction action) - { - if (this.action == action) - { - alphaTarget -= 0.2f; - Background.FadeTo(alphaTarget, 100, Easing.OutQuint); - } - - return false; - } - } - - private class KeyBindingTester : Container - { - public KeyBindingTester(SimultaneousBindingMode concurrency) - { - RelativeSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new SpriteText - { - Text = concurrency.ToString(), - }, - new TestInputManager(concurrency) - { - Y = 30, - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - ChildrenEnumerable = Enum.GetValues(typeof(TestAction)).Cast().Select(t => new TestButton(t)) - } - }, - }; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseKeyBindings : GridTestCase + { + public TestCaseKeyBindings() + : base(2, 2) + { + + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Cell(0).Add(new KeyBindingTester(SimultaneousBindingMode.None)); + Cell(1).Add(new KeyBindingTester(SimultaneousBindingMode.Unique)); + Cell(2).Add(new KeyBindingTester(SimultaneousBindingMode.All)); + } + + private enum TestAction + { + A, + S, + D_or_F, + Ctrl_A, + Ctrl_S, + Ctrl_D_or_F, + Shift_A, + Shift_S, + Shift_D_or_F, + Ctrl_Shift_A, + Ctrl_Shift_S, + Ctrl_Shift_D_or_F, + Ctrl, + Shift, + Ctrl_And_Shift, + Ctrl_Or_Shift, + LeftMouse, + RightMouse + } + + private class TestInputManager : KeyBindingContainer + { + public TestInputManager(SimultaneousBindingMode concurrencyMode = SimultaneousBindingMode.None) : base(concurrencyMode) + { + } + + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.A, TestAction.A ), + new KeyBinding(InputKey.S, TestAction.S ), + new KeyBinding(InputKey.D, TestAction.D_or_F ), + new KeyBinding(InputKey.F, TestAction.D_or_F ), + + new KeyBinding(new[] { InputKey.Control, InputKey.A }, TestAction.Ctrl_A ), + new KeyBinding(new[] { InputKey.Control, InputKey.S }, TestAction.Ctrl_S ), + new KeyBinding(new[] { InputKey.Control, InputKey.D }, TestAction.Ctrl_D_or_F ), + new KeyBinding(new[] { InputKey.Control, InputKey.F }, TestAction.Ctrl_D_or_F ), + + new KeyBinding(new[] { InputKey.Shift, InputKey.A }, TestAction.Shift_A ), + new KeyBinding(new[] { InputKey.Shift, InputKey.S }, TestAction.Shift_S ), + new KeyBinding(new[] { InputKey.Shift, InputKey.D }, TestAction.Shift_D_or_F ), + new KeyBinding(new[] { InputKey.Shift, InputKey.F }, TestAction.Shift_D_or_F ), + + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, TestAction.Ctrl_Shift_A ), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, TestAction.Ctrl_Shift_S), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.D }, TestAction.Ctrl_Shift_D_or_F), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, TestAction.Ctrl_Shift_D_or_F), + + new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl), + new KeyBinding(new[] { InputKey.Shift }, TestAction.Shift), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift }, TestAction.Ctrl_And_Shift), + new KeyBinding(new[] { InputKey.Control }, TestAction.Ctrl_Or_Shift), + new KeyBinding(new[] { InputKey.Shift }, TestAction.Ctrl_Or_Shift), + + new KeyBinding(new[] { InputKey.MouseLeft }, TestAction.LeftMouse), + new KeyBinding(new[] { InputKey.MouseRight }, TestAction.RightMouse), + }; + } + + private class TestButton : Button, IKeyBindingHandler + { + private readonly TestAction action; + + public TestButton(TestAction action) + { + this.action = action; + + BackgroundColour = Color4.SkyBlue; + Text = action.ToString().Replace('_', ' '); + + RelativeSizeAxes = Axes.X; + Height = 40; + Width = 0.3f; + Padding = new MarginPadding(2); + + Background.Alpha = alphaTarget; + } + + private float alphaTarget = 0.5f; + + public bool OnPressed(TestAction action) + { + if (this.action == action) + { + alphaTarget += 0.2f; + Background.FadeTo(alphaTarget, 100, Easing.OutQuint); + } + + return false; + } + + public bool OnReleased(TestAction action) + { + if (this.action == action) + { + alphaTarget -= 0.2f; + Background.FadeTo(alphaTarget, 100, Easing.OutQuint); + } + + return false; + } + } + + private class KeyBindingTester : Container + { + public KeyBindingTester(SimultaneousBindingMode concurrency) + { + RelativeSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new SpriteText + { + Text = concurrency.ToString(), + }, + new TestInputManager(concurrency) + { + Y = 30, + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + ChildrenEnumerable = Enum.GetValues(typeof(TestAction)).Cast().Select(t => new TestButton(t)) + } + }, + }; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseLayoutTransformRewinding.cs b/osu.Framework.Tests/Visual/TestCaseLayoutTransformRewinding.cs index 56cda044f..1b492fd21 100644 --- a/osu.Framework.Tests/Visual/TestCaseLayoutTransformRewinding.cs +++ b/osu.Framework.Tests/Visual/TestCaseLayoutTransformRewinding.cs @@ -1,97 +1,97 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.MathUtils; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("Rewinding of transforms that are important to layout.")] - public class TestCaseLayoutTransformRewinding : TestCase - { - private readonly ManualUpdateSubTreeContainer manualContainer; - - public TestCaseLayoutTransformRewinding() - { - Child = manualContainer = new ManualUpdateSubTreeContainer(); - - testAutoSizeInstant(); - testFlowInstant(); - } - - private void testAutoSizeInstant() - { - AddStep("Initialize autosize test", () => - { - manualContainer.Child = new Container - { - AutoSizeAxes = Axes.Both, - Masking = true, - Child = new Box { Size = new Vector2(150) } - }; - }); - - AddStep("Run to end", () => manualContainer.PerformUpdate(null)); - AddAssert("Size = 150", () => Precision.AlmostEquals(new Vector2(150), manualContainer.Child.Size)); - - AddStep("Rewind", () => manualContainer.PerformUpdate(() => manualContainer.ApplyTransformsAt(-1, true))); - AddAssert("Size = 150", () => Precision.AlmostEquals(new Vector2(150), manualContainer.Child.Size)); - } - - private void testFlowInstant() - { - Box box2 = null; - - AddStep("Initialize flow test", () => - { - manualContainer.Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Children = new[] - { - new Box { Size = new Vector2(150) }, - box2 = new Box { Size = new Vector2(150) } - } - }; - }); - - AddStep("Run to end", () => manualContainer.PerformUpdate(null)); - AddAssert("Box2 @ (150, 0)", () => Precision.AlmostEquals(new Vector2(150, 0), box2.Position)); - - AddStep("Rewind", () => manualContainer.PerformUpdate(() => manualContainer.ApplyTransformsAt(-1, true))); - AddAssert("Box2 @ (150, 0)", () => Precision.AlmostEquals(new Vector2(150, 0), box2.Position)); - } - - private class ManualUpdateSubTreeContainer : Container - { - public override bool RemoveCompletedTransforms => false; - - private Action onUpdateAfterChildren; - - public ManualUpdateSubTreeContainer() - { - RelativeSizeAxes = Axes.Both; - } - - public void PerformUpdate(Action afterChildren) - { - onUpdateAfterChildren = afterChildren; - base.UpdateSubTree(); - onUpdateAfterChildren = null; - } - - public override bool UpdateSubTree() => false; - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - onUpdateAfterChildren?.Invoke(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("Rewinding of transforms that are important to layout.")] + public class TestCaseLayoutTransformRewinding : TestCase + { + private readonly ManualUpdateSubTreeContainer manualContainer; + + public TestCaseLayoutTransformRewinding() + { + Child = manualContainer = new ManualUpdateSubTreeContainer(); + + testAutoSizeInstant(); + testFlowInstant(); + } + + private void testAutoSizeInstant() + { + AddStep("Initialize autosize test", () => + { + manualContainer.Child = new Container + { + AutoSizeAxes = Axes.Both, + Masking = true, + Child = new Box { Size = new Vector2(150) } + }; + }); + + AddStep("Run to end", () => manualContainer.PerformUpdate(null)); + AddAssert("Size = 150", () => Precision.AlmostEquals(new Vector2(150), manualContainer.Child.Size)); + + AddStep("Rewind", () => manualContainer.PerformUpdate(() => manualContainer.ApplyTransformsAt(-1, true))); + AddAssert("Size = 150", () => Precision.AlmostEquals(new Vector2(150), manualContainer.Child.Size)); + } + + private void testFlowInstant() + { + Box box2 = null; + + AddStep("Initialize flow test", () => + { + manualContainer.Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + new Box { Size = new Vector2(150) }, + box2 = new Box { Size = new Vector2(150) } + } + }; + }); + + AddStep("Run to end", () => manualContainer.PerformUpdate(null)); + AddAssert("Box2 @ (150, 0)", () => Precision.AlmostEquals(new Vector2(150, 0), box2.Position)); + + AddStep("Rewind", () => manualContainer.PerformUpdate(() => manualContainer.ApplyTransformsAt(-1, true))); + AddAssert("Box2 @ (150, 0)", () => Precision.AlmostEquals(new Vector2(150, 0), box2.Position)); + } + + private class ManualUpdateSubTreeContainer : Container + { + public override bool RemoveCompletedTransforms => false; + + private Action onUpdateAfterChildren; + + public ManualUpdateSubTreeContainer() + { + RelativeSizeAxes = Axes.Both; + } + + public void PerformUpdate(Action afterChildren) + { + onUpdateAfterChildren = afterChildren; + base.UpdateSubTree(); + onUpdateAfterChildren = null; + } + + public override bool UpdateSubTree() => false; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + onUpdateAfterChildren?.Invoke(); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseLocalisation.cs b/osu.Framework.Tests/Visual/TestCaseLocalisation.cs index 5d5de8b04..46d3014e9 100644 --- a/osu.Framework.Tests/Visual/TestCaseLocalisation.cs +++ b/osu.Framework.Tests/Visual/TestCaseLocalisation.cs @@ -1,104 +1,104 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Globalization; -using System.IO; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.IO.Stores; -using osu.Framework.Localisation; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseLocalisation : TestCase - { - // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable - private readonly LocalisationEngine engine; //keep a reference to avoid GC of the engine - - public TestCaseLocalisation() - { - var config = new FakeFrameworkConfigManager(); - engine = new LocalisationEngine(config); - - engine.AddLanguage("en", new FakeStorage()); - engine.AddLanguage("zh-CHS", new FakeStorage()); - engine.AddLanguage("ja", new FakeStorage()); - - Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Padding = new MarginPadding(10), - AutoSizeAxes = Axes.Both, - Children = new[] - { - new SpriteText - { - Text = "Not localisable", - TextSize = 48, - Colour = Color4.White - }, - new SpriteText - { - Current = engine.GetLocalisedString("localisable"), - TextSize = 48, - Colour = Color4.White - }, - new SpriteText - { - Current = engine.GetUnicodePreference("Unicode on", "Unicode off"), - TextSize = 48, - Colour = Color4.White - }, - new SpriteText - { - Current = engine.GetUnicodePreference(null, "I miss unicode"), - TextSize = 48, - Colour = Color4.White - }, - new SpriteText - { - Current = engine.Format($"{DateTime.Now}"), - TextSize = 48, - Colour = Color4.White - }, - } - }); - - AddStep("English", () => config.Set(FrameworkSetting.Locale, "en")); - AddStep("Japanese", () => config.Set(FrameworkSetting.Locale, "ja")); - AddStep("Simplified Chinese", () => config.Set(FrameworkSetting.Locale, "zh-CHS")); - AddToggleStep("ShowUnicode", b => config.Set(FrameworkSetting.ShowUnicode, b)); - } - - private class FakeFrameworkConfigManager : FrameworkConfigManager - { - protected override string Filename => null; - - public FakeFrameworkConfigManager() : base(null) { } - - protected override void InitialiseDefaults() - { - Set(FrameworkSetting.Locale, ""); - Set(FrameworkSetting.ShowUnicode, false); - } - } - - private class FakeStorage : IResourceStore - { - public string Get(string name) => $"{name} in {CultureInfo.CurrentCulture.EnglishName}"; - public Stream GetStream(string name) - { - throw new NotSupportedException(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Globalization; +using System.IO; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.IO.Stores; +using osu.Framework.Localisation; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseLocalisation : TestCase + { + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly LocalisationEngine engine; //keep a reference to avoid GC of the engine + + public TestCaseLocalisation() + { + var config = new FakeFrameworkConfigManager(); + engine = new LocalisationEngine(config); + + engine.AddLanguage("en", new FakeStorage()); + engine.AddLanguage("zh-CHS", new FakeStorage()); + engine.AddLanguage("ja", new FakeStorage()); + + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Padding = new MarginPadding(10), + AutoSizeAxes = Axes.Both, + Children = new[] + { + new SpriteText + { + Text = "Not localisable", + TextSize = 48, + Colour = Color4.White + }, + new SpriteText + { + Current = engine.GetLocalisedString("localisable"), + TextSize = 48, + Colour = Color4.White + }, + new SpriteText + { + Current = engine.GetUnicodePreference("Unicode on", "Unicode off"), + TextSize = 48, + Colour = Color4.White + }, + new SpriteText + { + Current = engine.GetUnicodePreference(null, "I miss unicode"), + TextSize = 48, + Colour = Color4.White + }, + new SpriteText + { + Current = engine.Format($"{DateTime.Now}"), + TextSize = 48, + Colour = Color4.White + }, + } + }); + + AddStep("English", () => config.Set(FrameworkSetting.Locale, "en")); + AddStep("Japanese", () => config.Set(FrameworkSetting.Locale, "ja")); + AddStep("Simplified Chinese", () => config.Set(FrameworkSetting.Locale, "zh-CHS")); + AddToggleStep("ShowUnicode", b => config.Set(FrameworkSetting.ShowUnicode, b)); + } + + private class FakeFrameworkConfigManager : FrameworkConfigManager + { + protected override string Filename => null; + + public FakeFrameworkConfigManager() : base(null) { } + + protected override void InitialiseDefaults() + { + Set(FrameworkSetting.Locale, ""); + Set(FrameworkSetting.ShowUnicode, false); + } + } + + private class FakeStorage : IResourceStore + { + public string Get(string name) => $"{name} in {CultureInfo.CurrentCulture.EnglishName}"; + public Stream GetStream(string name) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseMasking.cs b/osu.Framework.Tests/Visual/TestCaseMasking.cs index dc578d978..3ac0aae20 100644 --- a/osu.Framework.Tests/Visual/TestCaseMasking.cs +++ b/osu.Framework.Tests/Visual/TestCaseMasking.cs @@ -1,458 +1,458 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseMasking : TestCase - { - protected Container TestContainer; - - public TestCaseMasking() - { - Add(TestContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - string[] testNames = - { - @"Round corner masking", - @"Round corner AABB 1", - @"Round corner AABB 2", - @"Round corner AABB 3", - @"Edge/border blurriness", - @"Nested masking", - @"Rounded corner input" - }; - - for (int i = 0; i < testNames.Length; i++) - { - int test = i; - AddStep(testNames[i], delegate { loadTest(test); }); - } - - loadTest(0); - addCrosshair(); - } - - private void addCrosshair() - { - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(22, 4), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(4, 22), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(20, 2), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(2, 20), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - } - - private void loadTest(int testType) - { - TestContainer.Clear(); - - switch (testType) - { - default: - { - Container box; - TestContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - CornerRadius = 100, - BorderColour = Color4.Aquamarine, - BorderThickness = 3, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 100, - Colour = new Color4(0, 50, 100, 200), - }, - }); - - box.Add(box = new InfofulBox - { - Size = new Vector2(250, 250), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - }); - - box.OnUpdate += delegate { box.Rotation += 0.05f; }; - break; - } - - case 1: - { - Container box; - TestContainer.Add(new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - box = new InfofulBox - { - Masking = true, - CornerRadius = 100, - Size = new Vector2(400, 400), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - } - } - }); - - box.OnUpdate += delegate - { - box.Rotation += 0.05f; - box.CornerRadius = 100 + 100 * (float)Math.Sin(box.Rotation * 0.01); - }; - break; - } - - case 2: - { - Container box; - TestContainer.Add(new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - box = new InfofulBox - { - Masking = true, - CornerRadius = 25, - Shear = new Vector2(0.5f, 0), - Size = new Vector2(150, 150), - Scale = new Vector2(2.5f, 1.5f), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - } - } - }); - - box.OnUpdate += delegate { box.Rotation += 0.05f; }; - break; - } - - case 3: - { - Color4 glowColour = Color4.Aquamarine; - glowColour.A = 0.5f; - - Container box1; - Container box2; - - TestContainer.Add(new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 100, - Roundness = 50, - Colour = glowColour, - }, - BorderColour = Color4.Aquamarine, - BorderThickness = 3, - Children = new[] - { - box1 = new InfofulBoxAutoSize - { - Masking = true, - CornerRadius = 25, - Shear = new Vector2(0.5f, 0), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - Children = new[] - { - box2 = new InfofulBox - { - Masking = true, - CornerRadius = 25, - Shear = new Vector2(0.25f, 0.25f), - Size = new Vector2(100, 200), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.Blue, - } - } - } - } - }); - - box1.OnUpdate += delegate { box1.Rotation += 0.07f; }; - box2.OnUpdate += delegate { box2.Rotation -= 0.15f; }; - break; - } - - case 4: - { - Func createMaskingBox = delegate (float scale) - { - float size = 200 / scale; - return new Container - { - Masking = true, - CornerRadius = 25 / scale, - BorderThickness = 12.5f / scale, - BorderColour = Color4.Red, - Size = new Vector2(size), - Scale = new Vector2(scale), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new SpriteText - { - Text = @"Size: " + size + ", Scale: " + scale, - TextSize = 20 / scale, - Colour = Color4.Blue, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - }; - }; - - TestContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Masking = true, - Children = new[] { createMaskingBox(100) } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Masking = true, - Children = new[] { createMaskingBox(10) } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Masking = true, - Children = new[] { createMaskingBox(1) } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Masking = true, - Children = new[] { createMaskingBox(0.1f) } - }, - } - }); - - break; - } - - case 5: - { - TestContainer.Add(new Container - { - Masking = true, - Size = new Vector2(0.5f), - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - Masking = true, - CornerRadius = 100f, - BorderThickness = 50f, - BorderColour = Color4.Red, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1.5f), - Anchor = Anchor.BottomRight, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - } - } - } - }); - break; - } - - case 6: - { - TestContainer.Add(new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new SpriteText - { - Text = $"None of the folowing {nameof(CircularContainer)}s should trigger until the white part is hovered" - }, - new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 2), - Children = new Drawable[] - { - new SpriteText - { - Text = "No masking" - }, - new CircularContainerWithInput - { - Size = new Vector2(200), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Red - }, - new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both - } - } - } - } - } - } - }, - new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 2), - Children = new Drawable[] - { - new SpriteText - { - Text = "With masking" - }, - new CircularContainerWithInput - { - Size = new Vector2(200), - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Red - }, - new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both - } - } - } - } - } - } - } - } - }); - break; - } - } - -#if DEBUG - //if (toggleDebugAutosize.State) - // testContainer.Children.FindAll(c => c.HasAutosizeChildren).ForEach(c => c.AutoSizeDebug = true); -#endif - } - - private class CircularContainerWithInput : CircularContainer - { - protected override bool OnHover(InputState state) - { - this.ScaleTo(1.2f, 100); - return true; - } - - protected override void OnHoverLost(InputState state) - { - this.ScaleTo(1f, 100); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseMasking : TestCase + { + protected Container TestContainer; + + public TestCaseMasking() + { + Add(TestContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + string[] testNames = + { + @"Round corner masking", + @"Round corner AABB 1", + @"Round corner AABB 2", + @"Round corner AABB 3", + @"Edge/border blurriness", + @"Nested masking", + @"Rounded corner input" + }; + + for (int i = 0; i < testNames.Length; i++) + { + int test = i; + AddStep(testNames[i], delegate { loadTest(test); }); + } + + loadTest(0); + addCrosshair(); + } + + private void addCrosshair() + { + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(22, 4), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(4, 22), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(20, 2), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(2, 20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + private void loadTest(int testType) + { + TestContainer.Clear(); + + switch (testType) + { + default: + { + Container box; + TestContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + CornerRadius = 100, + BorderColour = Color4.Aquamarine, + BorderThickness = 3, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 100, + Colour = new Color4(0, 50, 100, 200), + }, + }); + + box.Add(box = new InfofulBox + { + Size = new Vector2(250, 250), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + }); + + box.OnUpdate += delegate { box.Rotation += 0.05f; }; + break; + } + + case 1: + { + Container box; + TestContainer.Add(new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + box = new InfofulBox + { + Masking = true, + CornerRadius = 100, + Size = new Vector2(400, 400), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + } + } + }); + + box.OnUpdate += delegate + { + box.Rotation += 0.05f; + box.CornerRadius = 100 + 100 * (float)Math.Sin(box.Rotation * 0.01); + }; + break; + } + + case 2: + { + Container box; + TestContainer.Add(new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + box = new InfofulBox + { + Masking = true, + CornerRadius = 25, + Shear = new Vector2(0.5f, 0), + Size = new Vector2(150, 150), + Scale = new Vector2(2.5f, 1.5f), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + } + } + }); + + box.OnUpdate += delegate { box.Rotation += 0.05f; }; + break; + } + + case 3: + { + Color4 glowColour = Color4.Aquamarine; + glowColour.A = 0.5f; + + Container box1; + Container box2; + + TestContainer.Add(new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 100, + Roundness = 50, + Colour = glowColour, + }, + BorderColour = Color4.Aquamarine, + BorderThickness = 3, + Children = new[] + { + box1 = new InfofulBoxAutoSize + { + Masking = true, + CornerRadius = 25, + Shear = new Vector2(0.5f, 0), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + Children = new[] + { + box2 = new InfofulBox + { + Masking = true, + CornerRadius = 25, + Shear = new Vector2(0.25f, 0.25f), + Size = new Vector2(100, 200), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Blue, + } + } + } + } + }); + + box1.OnUpdate += delegate { box1.Rotation += 0.07f; }; + box2.OnUpdate += delegate { box2.Rotation -= 0.15f; }; + break; + } + + case 4: + { + Func createMaskingBox = delegate (float scale) + { + float size = 200 / scale; + return new Container + { + Masking = true, + CornerRadius = 25 / scale, + BorderThickness = 12.5f / scale, + BorderColour = Color4.Red, + Size = new Vector2(size), + Scale = new Vector2(scale), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new SpriteText + { + Text = @"Size: " + size + ", Scale: " + scale, + TextSize = 20 / scale, + Colour = Color4.Blue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }; + }; + + TestContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Masking = true, + Children = new[] { createMaskingBox(100) } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Masking = true, + Children = new[] { createMaskingBox(10) } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Masking = true, + Children = new[] { createMaskingBox(1) } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Masking = true, + Children = new[] { createMaskingBox(0.1f) } + }, + } + }); + + break; + } + + case 5: + { + TestContainer.Add(new Container + { + Masking = true, + Size = new Vector2(0.5f), + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + Masking = true, + CornerRadius = 100f, + BorderThickness = 50f, + BorderColour = Color4.Red, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.5f), + Anchor = Anchor.BottomRight, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + } + } + } + }); + break; + } + + case 6: + { + TestContainer.Add(new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new SpriteText + { + Text = $"None of the folowing {nameof(CircularContainer)}s should trigger until the white part is hovered" + }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(0, 2), + Children = new Drawable[] + { + new SpriteText + { + Text = "No masking" + }, + new CircularContainerWithInput + { + Size = new Vector2(200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red + }, + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both + } + } + } + } + } + } + }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(0, 2), + Children = new Drawable[] + { + new SpriteText + { + Text = "With masking" + }, + new CircularContainerWithInput + { + Size = new Vector2(200), + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red + }, + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both + } + } + } + } + } + } + } + } + }); + break; + } + } + +#if DEBUG + //if (toggleDebugAutosize.State) + // testContainer.Children.FindAll(c => c.HasAutosizeChildren).ForEach(c => c.AutoSizeDebug = true); +#endif + } + + private class CircularContainerWithInput : CircularContainer + { + protected override bool OnHover(InputState state) + { + this.ScaleTo(1.2f, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.ScaleTo(1f, 100); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseNestedHover.cs b/osu.Framework.Tests/Visual/TestCaseNestedHover.cs index e8d25742f..f9a34b0a8 100644 --- a/osu.Framework.Tests/Visual/TestCaseNestedHover.cs +++ b/osu.Framework.Tests/Visual/TestCaseNestedHover.cs @@ -1,80 +1,80 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseNestedHover : TestCase - { - public TestCaseNestedHover() - { - HoverBox box1; - Add(box1 = new HoverBox(Color4.Gray, Color4.White) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(300, 300) - }); - - HoverBox box2; - box1.Add(box2 = new HoverBox(Color4.Pink, Color4.Red) - { - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Position = new Vector2(0.2f, 0.2f), - Size = new Vector2(0.6f, 0.6f) - }); - - box2.Add(new HoverBox(Color4.LightBlue, Color4.Blue, false) - { - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Position = new Vector2(0.2f, 0.2f), - Size = new Vector2(0.6f, 0.6f) - }); - } - - private class HoverBox : Container - { - private readonly Color4 normalColour; - private readonly Color4 hoveredColour; - - private readonly Box box; - private readonly bool propagateHover; - - public HoverBox(Color4 normalColour, Color4 hoveredColour, bool propagateHover = true) - { - this.normalColour = normalColour; - this.hoveredColour = hoveredColour; - this.propagateHover = propagateHover; - - Children = new Drawable[] - { - box = new Box - { - Colour = normalColour, - RelativeSizeAxes = Axes.Both - } - }; - } - - protected override bool OnHover(InputState state) - { - box.Colour = hoveredColour; - return !propagateHover; - } - - protected override void OnHoverLost(InputState state) - { - box.Colour = normalColour; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseNestedHover : TestCase + { + public TestCaseNestedHover() + { + HoverBox box1; + Add(box1 = new HoverBox(Color4.Gray, Color4.White) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 300) + }); + + HoverBox box2; + box1.Add(box2 = new HoverBox(Color4.Pink, Color4.Red) + { + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Position = new Vector2(0.2f, 0.2f), + Size = new Vector2(0.6f, 0.6f) + }); + + box2.Add(new HoverBox(Color4.LightBlue, Color4.Blue, false) + { + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Position = new Vector2(0.2f, 0.2f), + Size = new Vector2(0.6f, 0.6f) + }); + } + + private class HoverBox : Container + { + private readonly Color4 normalColour; + private readonly Color4 hoveredColour; + + private readonly Box box; + private readonly bool propagateHover; + + public HoverBox(Color4 normalColour, Color4 hoveredColour, bool propagateHover = true) + { + this.normalColour = normalColour; + this.hoveredColour = hoveredColour; + this.propagateHover = propagateHover; + + Children = new Drawable[] + { + box = new Box + { + Colour = normalColour, + RelativeSizeAxes = Axes.Both + } + }; + } + + protected override bool OnHover(InputState state) + { + box.Colour = hoveredColour; + return !propagateHover; + } + + protected override void OnHoverLost(InputState state) + { + box.Colour = normalColour; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseNestedMenus.cs b/osu.Framework.Tests/Visual/TestCaseNestedMenus.cs index 7ce538a6d..0975cbf27 100644 --- a/osu.Framework.Tests/Visual/TestCaseNestedMenus.cs +++ b/osu.Framework.Tests/Visual/TestCaseNestedMenus.cs @@ -1,493 +1,493 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using osu.Framework.Testing.Input; -using OpenTK; -using OpenTK.Input; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseNestedMenus : TestCase - { - private const int max_depth = 5; - private const int max_count = 5; - - public override IReadOnlyList RequiredTypes => new[] { typeof(Menu) }; - - private Random rng; - - private ManualInputManager inputManager; - private MenuStructure menus; - - [SetUp] - public void SetUp() - { - Clear(); - - rng = new Random(1337); - - Menu menu; - Add(inputManager = new ManualInputManager - { - Children = new Drawable[] - { - new CursorContainer(), - new Container - { - RelativeSizeAxes = Axes.Both, - Child = menu = createMenu() - } - } - }); - - menus = new MenuStructure(menu); - } - - private Menu createMenu() => new ClickOpenMenu(TimePerAction) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Items = new[] - { - generateRandomMenuItem("First"), - generateRandomMenuItem("Second"), - generateRandomMenuItem("Third"), - } - }; - - private class ClickOpenMenu : Menu - { - protected override Menu CreateSubMenu() => new ClickOpenMenu(HoverOpenDelay, false); - - public ClickOpenMenu(double timePerAction, bool topLevel = true) : base(Direction.Vertical, topLevel) - { - HoverOpenDelay = timePerAction; - } - } - - #region Test Cases - - /// - /// Tests if the respects = true, by not alowing it to be closed - /// when a click happens outside the . - /// - [Test] - public void TestAlwaysOpen() - { - AddStep("Click outside", () => inputManager.Click(MouseButton.Left)); - AddAssert("Check AlwaysOpen = true", () => menus.GetSubMenu(0).State == MenuState.Open); - } - - /// - /// Tests if the hover state on s is valid. - /// - [Test] - public void TestHoverState() - { - AddAssert("Check submenu closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetMenuItems()[0])); - AddAssert("Check item hovered", () => menus.GetMenuItems()[0].IsHovered); - } - - /// - /// Tests if the respects = true. - /// - [Test] - public void TestTopLevelMenu() - { - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[0])); - AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); - AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); - AddStep("Click item", () => inputManager.Click(MouseButton.Left)); - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - } - - /// - /// Tests if clicking once on a menu that has opens it, and clicking a second time - /// closes it. - /// - [Test] - public void TestDoubleClick() - { - AddStep("Click item", () => clickItem(0, 0)); - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - AddStep("Click item", () => clickItem(0, 0)); - AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); - } - - /// - /// Tests whether click on s causes sub-menus to instantly appear. - /// - [Test] - public void TestInstantOpen() - { - AddStep("Click item", () => clickItem(0, 1)); - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - AddStep("Click item", () => clickItem(1, 0)); - AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); - } - - /// - /// Tests if clicking on an item that has no sub-menu causes the menu to close. - /// - [Test] - public void TestActionClick() - { - AddStep("Click item", () => clickItem(0, 0)); - AddStep("Click item", () => clickItem(1, 0)); - AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); - } - - /// - /// Tests if hovering over menu items respects the . - /// - [Test] - public void TestHoverOpen() - { - AddStep("Click item", () => clickItem(0, 1)); - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); - AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); - AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(2).GetMenuItems()[0])); - AddAssert("Check closed", () => menus.GetSubMenu(3)?.State != MenuState.Open); - AddAssert("Check open", () => menus.GetSubMenu(3).State == MenuState.Open); - } - - /// - /// Tests if hovering over a different item on the main will instantly open another menu - /// and correctly changes the sub-menu items to the new items from the hovered item. - /// - [Test] - public void TestHoverChange() - { - IReadOnlyList currentItems = null; - AddStep("Click item", () => - { - clickItem(0, 0); - }); - - AddStep("Get items", () => - { - currentItems = menus.GetSubMenu(1).Items; - }); - - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[1])); - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - - AddAssert("Check new items", () => !menus.GetSubMenu(1).Items.SequenceEqual(currentItems)); - AddAssert("Check closed", () => - { - int currentSubMenu = 3; - while (true) - { - var subMenu = menus.GetSubMenu(currentSubMenu); - if (subMenu == null) - break; - - if (subMenu.State == MenuState.Open) - return false; - currentSubMenu++; - } - - return true; - }); - } - - /// - /// Tests whether hovering over a different item on a sub-menu opens a new sub-menu in a delayed fashion - /// and correctly changes the sub-menu items to the new items from the hovered item. - /// - [Test] - public void TestDelayedHoverChange() - { - AddStep("Click item", () => clickItem(0, 2)); - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); - AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); - AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); - - AddStep("Hover item", () => - { - inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1]); - }); - - AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); - AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); - - AddAssert("Check closed", () => - { - int currentSubMenu = 3; - while (true) - { - var subMenu = menus.GetSubMenu(currentSubMenu); - if (subMenu == null) - break; - - if (subMenu.State == MenuState.Open) - return false; - currentSubMenu++; - } - - return true; - }); - } - - /// - /// Tests whether clicking on s that have opened sub-menus don't close the sub-menus. - /// Then tests hovering in reverse order to make sure only the lower level menus close. - /// - [Test] - public void TestMenuClicksDontClose() - { - AddStep("Click item", () => clickItem(0, 1)); - AddStep("Click item", () => clickItem(1, 0)); - AddStep("Click item", () => clickItem(2, 0)); - AddStep("Click item", () => clickItem(3, 0)); - - for (int i = 3; i >= 1; i--) - { - int menuIndex = i; - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[0])); - AddAssert("Check submenu open", () => menus.GetSubMenu(menuIndex + 1).State == MenuState.Open); - AddStep("Click item", () => inputManager.Click(MouseButton.Left)); - AddAssert("Check all open", () => - { - for (int j = 0; j <= menuIndex; j++) - { - int menuIndex2 = j; - if (menus.GetSubMenu(menuIndex2)?.State != MenuState.Open) - return false; - } - - return true; - }); - } - } - - /// - /// Tests whether clicking on the that has closes all sub menus. - /// - [Test] - public void TestMenuClickClosesSubMenus() - { - AddStep("Click item", () => clickItem(0, 1)); - AddStep("Click item", () => clickItem(1, 0)); - AddStep("Click item", () => clickItem(2, 0)); - AddStep("Click item", () => clickItem(3, 0)); - AddStep("Click item", () => clickItem(0, 1)); - - AddAssert("Check submenus closed", () => - { - for (int j = 1; j <= 3; j++) - { - int menuIndex2 = j; - if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) - return false; - } - - return true; - }); - } - - /// - /// Tests whether clicking on an action in a sub-menu closes all s. - /// - [Test] - public void TestActionClickClosesMenus() - { - AddStep("Click item", () => clickItem(0, 1)); - AddStep("Click item", () => clickItem(1, 0)); - AddStep("Click item", () => clickItem(2, 0)); - AddStep("Click item", () => clickItem(3, 0)); - AddStep("Click item", () => clickItem(4, 0)); - - AddAssert("Check submenus closed", () => - { - for (int j = 1; j <= 3; j++) - { - int menuIndex2 = j; - if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) - return false; - } - - return true; - }); - } - - /// - /// Tests whether clicking outside the structure closes all sub-menus. - /// - /// Whether the previous menu should first be hovered before clicking outside. - [TestCase(false)] - [TestCase(true)] - public void TestClickingOutsideClosesMenus(bool hoverPrevious) - { - for (int i = 0; i <= 3; i++) - { - int i2 = i; - - for (int j = 0; j <= i; j++) - { - int menuToOpen = j; - int itemToOpen = menuToOpen == 0 ? 1 : 0; - AddStep("Click item", () => clickItem(menuToOpen, itemToOpen)); - } - - if (hoverPrevious && i > 0) - AddStep("Hover previous", () => inputManager.MoveMouseTo(menus.GetSubStructure(i2 - 1).GetMenuItems()[i2 > 1 ? 0 : 1])); - - AddStep("Remove hover", () => inputManager.MoveMouseTo(Vector2.Zero)); - AddStep("Click outside", () => inputManager.Click(MouseButton.Left)); - AddAssert("Check submenus closed", () => - { - for (int j = 1; j <= i2 + 1; j++) - { - int menuIndex2 = j; - if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) - return false; - } - - return true; - }); - } - } - - /// - /// Opens some menus and then changes the selected item. - /// - [Test] - public void TestSelectedState() - { - AddStep("Click item", () => clickItem(0, 2)); - AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); - - AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1])); - AddAssert("Check closed 1", () => menus.GetSubMenu(2)?.State != MenuState.Open); - AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); - AddAssert("Check selected index 1", () => menus.GetSubStructure(1).GetSelectedIndex() == 1); - - AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(0, MenuItemState.Selected)); - AddAssert("Check selected index", () => menus.GetSubStructure(1).GetSelectedIndex() == 0); - - AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(2, MenuItemState.Selected)); - AddAssert("Check selected index 2", () => menus.GetSubStructure(1).GetSelectedIndex() == 2); - - AddStep("Close menus", () => menus.GetSubMenu(0).Close()); - AddAssert("Check selected index 4", () => menus.GetSubStructure(1).GetSelectedIndex() == -1); - } - #endregion - - /// - /// Click an item in a menu. - /// - /// The level of menu our click targets. - /// The item to click in the menu. - private void clickItem(int menuIndex, int itemIndex) - { - inputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[itemIndex]); - inputManager.Click(MouseButton.Left); - } - - private MenuItem generateRandomMenuItem(string name = "Menu Item", int currDepth = 1) - { - var item = new MenuItem(name); - - if (currDepth == max_depth) - return item; - - int subCount = rng.Next(0, max_count); - var subItems = new List(); - for (int i = 0; i < subCount; i++) - subItems.Add(generateRandomMenuItem(item.Text + $" #{i + 1}", currDepth + 1)); - - item.Items = subItems; - return item; - } - - /// - /// Helper class used to retrieve various internal properties/items from a . - /// - private class MenuStructure - { - private readonly Menu menu; - - public MenuStructure(Menu menu) - { - this.menu = menu; - } - - /// - /// Retrieves the s of the represented by this . - /// - public IReadOnlyList GetMenuItems() - { - var contents = (CompositeDrawable)menu.InternalChildren[0]; - var contentContainer = (CompositeDrawable)contents.InternalChildren[1]; - return ((CompositeDrawable)((CompositeDrawable)contentContainer.InternalChildren[0]).InternalChildren[0]).InternalChildren; - } - - /// - /// Finds the index in the represented by this that - /// has set to . - /// - public int GetSelectedIndex() - { - var items = GetMenuItems(); - - for (int i = 0; i < items.Count; i++) - { - var state = (MenuItemState)(items[i]?.GetType().GetProperty("State")?.GetValue(items[i]) ?? MenuItemState.NotSelected); - if (state == MenuItemState.Selected) - return i; - } - - return -1; - } - - /// - /// Sets the at the specified index to a specified state. - /// - /// The index of the to set the state of. - /// The state to be set. - public void SetSelectedState(int index, MenuItemState state) - { - var item = GetMenuItems()[index]; - item.GetType().GetProperty("State")?.SetValue(item, state); - } - - /// - /// Retrieves the sub- at an index-offset from the current . - /// - /// The sub- index. An index of 0 is the represented by this . - public Menu GetSubMenu(int index) - { - var currentMenu = menu; - for (int i = 0; i < index; i++) - { - if (currentMenu == null) - break; - - var container = (CompositeDrawable)currentMenu.InternalChildren[1]; - currentMenu = (container.InternalChildren.Count > 0 ? container.InternalChildren[0] : null) as Menu; - } - - return currentMenu; - } - - /// - /// Generates a new for the a sub-. - /// - /// The sub- index to generate the for. An index of 0 is the represented by this . - public MenuStructure GetSubStructure(int index) => new MenuStructure(GetSubMenu(index)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Framework.Testing.Input; +using OpenTK; +using OpenTK.Input; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseNestedMenus : TestCase + { + private const int max_depth = 5; + private const int max_count = 5; + + public override IReadOnlyList RequiredTypes => new[] { typeof(Menu) }; + + private Random rng; + + private ManualInputManager inputManager; + private MenuStructure menus; + + [SetUp] + public void SetUp() + { + Clear(); + + rng = new Random(1337); + + Menu menu; + Add(inputManager = new ManualInputManager + { + Children = new Drawable[] + { + new CursorContainer(), + new Container + { + RelativeSizeAxes = Axes.Both, + Child = menu = createMenu() + } + } + }); + + menus = new MenuStructure(menu); + } + + private Menu createMenu() => new ClickOpenMenu(TimePerAction) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Items = new[] + { + generateRandomMenuItem("First"), + generateRandomMenuItem("Second"), + generateRandomMenuItem("Third"), + } + }; + + private class ClickOpenMenu : Menu + { + protected override Menu CreateSubMenu() => new ClickOpenMenu(HoverOpenDelay, false); + + public ClickOpenMenu(double timePerAction, bool topLevel = true) : base(Direction.Vertical, topLevel) + { + HoverOpenDelay = timePerAction; + } + } + + #region Test Cases + + /// + /// Tests if the respects = true, by not alowing it to be closed + /// when a click happens outside the . + /// + [Test] + public void TestAlwaysOpen() + { + AddStep("Click outside", () => inputManager.Click(MouseButton.Left)); + AddAssert("Check AlwaysOpen = true", () => menus.GetSubMenu(0).State == MenuState.Open); + } + + /// + /// Tests if the hover state on s is valid. + /// + [Test] + public void TestHoverState() + { + AddAssert("Check submenu closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetMenuItems()[0])); + AddAssert("Check item hovered", () => menus.GetMenuItems()[0].IsHovered); + } + + /// + /// Tests if the respects = true. + /// + [Test] + public void TestTopLevelMenu() + { + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[0])); + AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); + AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); + AddStep("Click item", () => inputManager.Click(MouseButton.Left)); + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + } + + /// + /// Tests if clicking once on a menu that has opens it, and clicking a second time + /// closes it. + /// + [Test] + public void TestDoubleClick() + { + AddStep("Click item", () => clickItem(0, 0)); + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + AddStep("Click item", () => clickItem(0, 0)); + AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); + } + + /// + /// Tests whether click on s causes sub-menus to instantly appear. + /// + [Test] + public void TestInstantOpen() + { + AddStep("Click item", () => clickItem(0, 1)); + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + AddStep("Click item", () => clickItem(1, 0)); + AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); + } + + /// + /// Tests if clicking on an item that has no sub-menu causes the menu to close. + /// + [Test] + public void TestActionClick() + { + AddStep("Click item", () => clickItem(0, 0)); + AddStep("Click item", () => clickItem(1, 0)); + AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); + } + + /// + /// Tests if hovering over menu items respects the . + /// + [Test] + public void TestHoverOpen() + { + AddStep("Click item", () => clickItem(0, 1)); + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); + AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); + AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(2).GetMenuItems()[0])); + AddAssert("Check closed", () => menus.GetSubMenu(3)?.State != MenuState.Open); + AddAssert("Check open", () => menus.GetSubMenu(3).State == MenuState.Open); + } + + /// + /// Tests if hovering over a different item on the main will instantly open another menu + /// and correctly changes the sub-menu items to the new items from the hovered item. + /// + [Test] + public void TestHoverChange() + { + IReadOnlyList currentItems = null; + AddStep("Click item", () => + { + clickItem(0, 0); + }); + + AddStep("Get items", () => + { + currentItems = menus.GetSubMenu(1).Items; + }); + + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[1])); + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + + AddAssert("Check new items", () => !menus.GetSubMenu(1).Items.SequenceEqual(currentItems)); + AddAssert("Check closed", () => + { + int currentSubMenu = 3; + while (true) + { + var subMenu = menus.GetSubMenu(currentSubMenu); + if (subMenu == null) + break; + + if (subMenu.State == MenuState.Open) + return false; + currentSubMenu++; + } + + return true; + }); + } + + /// + /// Tests whether hovering over a different item on a sub-menu opens a new sub-menu in a delayed fashion + /// and correctly changes the sub-menu items to the new items from the hovered item. + /// + [Test] + public void TestDelayedHoverChange() + { + AddStep("Click item", () => clickItem(0, 2)); + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); + AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); + AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); + + AddStep("Hover item", () => + { + inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1]); + }); + + AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); + AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); + + AddAssert("Check closed", () => + { + int currentSubMenu = 3; + while (true) + { + var subMenu = menus.GetSubMenu(currentSubMenu); + if (subMenu == null) + break; + + if (subMenu.State == MenuState.Open) + return false; + currentSubMenu++; + } + + return true; + }); + } + + /// + /// Tests whether clicking on s that have opened sub-menus don't close the sub-menus. + /// Then tests hovering in reverse order to make sure only the lower level menus close. + /// + [Test] + public void TestMenuClicksDontClose() + { + AddStep("Click item", () => clickItem(0, 1)); + AddStep("Click item", () => clickItem(1, 0)); + AddStep("Click item", () => clickItem(2, 0)); + AddStep("Click item", () => clickItem(3, 0)); + + for (int i = 3; i >= 1; i--) + { + int menuIndex = i; + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[0])); + AddAssert("Check submenu open", () => menus.GetSubMenu(menuIndex + 1).State == MenuState.Open); + AddStep("Click item", () => inputManager.Click(MouseButton.Left)); + AddAssert("Check all open", () => + { + for (int j = 0; j <= menuIndex; j++) + { + int menuIndex2 = j; + if (menus.GetSubMenu(menuIndex2)?.State != MenuState.Open) + return false; + } + + return true; + }); + } + } + + /// + /// Tests whether clicking on the that has closes all sub menus. + /// + [Test] + public void TestMenuClickClosesSubMenus() + { + AddStep("Click item", () => clickItem(0, 1)); + AddStep("Click item", () => clickItem(1, 0)); + AddStep("Click item", () => clickItem(2, 0)); + AddStep("Click item", () => clickItem(3, 0)); + AddStep("Click item", () => clickItem(0, 1)); + + AddAssert("Check submenus closed", () => + { + for (int j = 1; j <= 3; j++) + { + int menuIndex2 = j; + if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) + return false; + } + + return true; + }); + } + + /// + /// Tests whether clicking on an action in a sub-menu closes all s. + /// + [Test] + public void TestActionClickClosesMenus() + { + AddStep("Click item", () => clickItem(0, 1)); + AddStep("Click item", () => clickItem(1, 0)); + AddStep("Click item", () => clickItem(2, 0)); + AddStep("Click item", () => clickItem(3, 0)); + AddStep("Click item", () => clickItem(4, 0)); + + AddAssert("Check submenus closed", () => + { + for (int j = 1; j <= 3; j++) + { + int menuIndex2 = j; + if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) + return false; + } + + return true; + }); + } + + /// + /// Tests whether clicking outside the structure closes all sub-menus. + /// + /// Whether the previous menu should first be hovered before clicking outside. + [TestCase(false)] + [TestCase(true)] + public void TestClickingOutsideClosesMenus(bool hoverPrevious) + { + for (int i = 0; i <= 3; i++) + { + int i2 = i; + + for (int j = 0; j <= i; j++) + { + int menuToOpen = j; + int itemToOpen = menuToOpen == 0 ? 1 : 0; + AddStep("Click item", () => clickItem(menuToOpen, itemToOpen)); + } + + if (hoverPrevious && i > 0) + AddStep("Hover previous", () => inputManager.MoveMouseTo(menus.GetSubStructure(i2 - 1).GetMenuItems()[i2 > 1 ? 0 : 1])); + + AddStep("Remove hover", () => inputManager.MoveMouseTo(Vector2.Zero)); + AddStep("Click outside", () => inputManager.Click(MouseButton.Left)); + AddAssert("Check submenus closed", () => + { + for (int j = 1; j <= i2 + 1; j++) + { + int menuIndex2 = j; + if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) + return false; + } + + return true; + }); + } + } + + /// + /// Opens some menus and then changes the selected item. + /// + [Test] + public void TestSelectedState() + { + AddStep("Click item", () => clickItem(0, 2)); + AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); + + AddStep("Hover item", () => inputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1])); + AddAssert("Check closed 1", () => menus.GetSubMenu(2)?.State != MenuState.Open); + AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); + AddAssert("Check selected index 1", () => menus.GetSubStructure(1).GetSelectedIndex() == 1); + + AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(0, MenuItemState.Selected)); + AddAssert("Check selected index", () => menus.GetSubStructure(1).GetSelectedIndex() == 0); + + AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(2, MenuItemState.Selected)); + AddAssert("Check selected index 2", () => menus.GetSubStructure(1).GetSelectedIndex() == 2); + + AddStep("Close menus", () => menus.GetSubMenu(0).Close()); + AddAssert("Check selected index 4", () => menus.GetSubStructure(1).GetSelectedIndex() == -1); + } + #endregion + + /// + /// Click an item in a menu. + /// + /// The level of menu our click targets. + /// The item to click in the menu. + private void clickItem(int menuIndex, int itemIndex) + { + inputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[itemIndex]); + inputManager.Click(MouseButton.Left); + } + + private MenuItem generateRandomMenuItem(string name = "Menu Item", int currDepth = 1) + { + var item = new MenuItem(name); + + if (currDepth == max_depth) + return item; + + int subCount = rng.Next(0, max_count); + var subItems = new List(); + for (int i = 0; i < subCount; i++) + subItems.Add(generateRandomMenuItem(item.Text + $" #{i + 1}", currDepth + 1)); + + item.Items = subItems; + return item; + } + + /// + /// Helper class used to retrieve various internal properties/items from a . + /// + private class MenuStructure + { + private readonly Menu menu; + + public MenuStructure(Menu menu) + { + this.menu = menu; + } + + /// + /// Retrieves the s of the represented by this . + /// + public IReadOnlyList GetMenuItems() + { + var contents = (CompositeDrawable)menu.InternalChildren[0]; + var contentContainer = (CompositeDrawable)contents.InternalChildren[1]; + return ((CompositeDrawable)((CompositeDrawable)contentContainer.InternalChildren[0]).InternalChildren[0]).InternalChildren; + } + + /// + /// Finds the index in the represented by this that + /// has set to . + /// + public int GetSelectedIndex() + { + var items = GetMenuItems(); + + for (int i = 0; i < items.Count; i++) + { + var state = (MenuItemState)(items[i]?.GetType().GetProperty("State")?.GetValue(items[i]) ?? MenuItemState.NotSelected); + if (state == MenuItemState.Selected) + return i; + } + + return -1; + } + + /// + /// Sets the at the specified index to a specified state. + /// + /// The index of the to set the state of. + /// The state to be set. + public void SetSelectedState(int index, MenuItemState state) + { + var item = GetMenuItems()[index]; + item.GetType().GetProperty("State")?.SetValue(item, state); + } + + /// + /// Retrieves the sub- at an index-offset from the current . + /// + /// The sub- index. An index of 0 is the represented by this . + public Menu GetSubMenu(int index) + { + var currentMenu = menu; + for (int i = 0; i < index; i++) + { + if (currentMenu == null) + break; + + var container = (CompositeDrawable)currentMenu.InternalChildren[1]; + currentMenu = (container.InternalChildren.Count > 0 ? container.InternalChildren[0] : null) as Menu; + } + + return currentMenu; + } + + /// + /// Generates a new for the a sub-. + /// + /// The sub- index to generate the for. An index of 0 is the represented by this . + public MenuStructure GetSubStructure(int index) => new MenuStructure(GetSubMenu(index)); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCasePadding.cs b/osu.Framework.Tests/Visual/TestCasePadding.cs index 5670bef05..ee1a6b916 100644 --- a/osu.Framework.Tests/Visual/TestCasePadding.cs +++ b/osu.Framework.Tests/Visual/TestCasePadding.cs @@ -1,248 +1,248 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCasePadding : GridTestCase - { - public TestCasePadding() : base(2, 2) - { - Cell(0).AddRange(new Drawable[] - { - new SpriteText { Text = @"Padding - 20 All Sides" }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new PaddedBox(Color4.Blue) - { - Padding = new MarginPadding(20), - Size = new Vector2(200), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new PaddedBox(Color4.DarkSeaGreen) - { - Padding = new MarginPadding(40), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - } - } - } - } - } - }); - - Cell(1).AddRange(new Drawable[] - { - new SpriteText { Text = @"Padding - 20 Top, Left" }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new PaddedBox(Color4.Blue) - { - Padding = new MarginPadding - { - Top = 20, - Left = 20, - }, - Size = new Vector2(200), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new PaddedBox(Color4.DarkSeaGreen) - { - Padding = new MarginPadding(40), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - } - } - } - } - } - }); - - Cell(2).AddRange(new Drawable[] - { - new SpriteText { Text = @"Margin - 20 All Sides" }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new PaddedBox(Color4.Blue) - { - Margin = new MarginPadding(20), - Size = new Vector2(200), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new PaddedBox(Color4.DarkSeaGreen) - { - Padding = new MarginPadding(20), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - } - } - } - } - } - }); - - Cell(3).AddRange(new Drawable[] - { - new SpriteText { Text = @"Margin - 20 Top, Left" }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new PaddedBox(Color4.Blue) - { - Margin = new MarginPadding - { - Top = 20, - Left = 20, - }, - Size = new Vector2(200), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new PaddedBox(Color4.DarkSeaGreen) - { - Padding = new MarginPadding(40), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - } - } - } - } - } - }); - } - - private class PaddedBox : Container - { - private readonly SpriteText t1; - private readonly SpriteText t2; - private readonly SpriteText t3; - private readonly SpriteText t4; - - private readonly Container content; - - protected override Container Content => content; - - public PaddedBox(Color4 colour) - { - AddRangeInternal(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colour, - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - t1 = new SpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - t2 = new SpriteText - { - Rotation = 90, - Anchor = Anchor.CentreRight, - Origin = Anchor.TopCentre - }, - t3 = new SpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre - }, - t4 = new SpriteText - { - Rotation = -90, - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre - } - }); - - Masking = true; - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - t1.Text = (Padding.Top > 0 ? $"p{Padding.Top}" : string.Empty) + (Margin.Top > 0 ? $"m{Margin.Top}" : string.Empty); - t2.Text = (Padding.Right > 0 ? $"p{Padding.Right}" : string.Empty) + (Margin.Right > 0 ? $"m{Margin.Right}" : string.Empty); - t3.Text = (Padding.Bottom > 0 ? $"p{Padding.Bottom}" : string.Empty) + (Margin.Bottom > 0 ? $"m{Margin.Bottom}" : string.Empty); - t4.Text = (Padding.Left > 0 ? $"p{Padding.Left}" : string.Empty) + (Margin.Left > 0 ? $"m{Margin.Left}" : string.Empty); - - return base.Invalidate(invalidation, source, shallPropagate); - } - - protected override bool OnDrag(InputState state) - { - Position += state.Mouse.Delta; - return true; - } - - protected override bool OnDragEnd(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCasePadding : GridTestCase + { + public TestCasePadding() : base(2, 2) + { + Cell(0).AddRange(new Drawable[] + { + new SpriteText { Text = @"Padding - 20 All Sides" }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + new PaddedBox(Color4.Blue) + { + Padding = new MarginPadding(20), + Size = new Vector2(200), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new PaddedBox(Color4.DarkSeaGreen) + { + Padding = new MarginPadding(40), + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + } + } + } + }); + + Cell(1).AddRange(new Drawable[] + { + new SpriteText { Text = @"Padding - 20 Top, Left" }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + new PaddedBox(Color4.Blue) + { + Padding = new MarginPadding + { + Top = 20, + Left = 20, + }, + Size = new Vector2(200), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new PaddedBox(Color4.DarkSeaGreen) + { + Padding = new MarginPadding(40), + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + } + } + } + }); + + Cell(2).AddRange(new Drawable[] + { + new SpriteText { Text = @"Margin - 20 All Sides" }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + new PaddedBox(Color4.Blue) + { + Margin = new MarginPadding(20), + Size = new Vector2(200), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new PaddedBox(Color4.DarkSeaGreen) + { + Padding = new MarginPadding(20), + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + } + } + } + }); + + Cell(3).AddRange(new Drawable[] + { + new SpriteText { Text = @"Margin - 20 Top, Left" }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + new PaddedBox(Color4.Blue) + { + Margin = new MarginPadding + { + Top = 20, + Left = 20, + }, + Size = new Vector2(200), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new PaddedBox(Color4.DarkSeaGreen) + { + Padding = new MarginPadding(40), + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + } + } + } + }); + } + + private class PaddedBox : Container + { + private readonly SpriteText t1; + private readonly SpriteText t2; + private readonly SpriteText t3; + private readonly SpriteText t4; + + private readonly Container content; + + protected override Container Content => content; + + public PaddedBox(Color4 colour) + { + AddRangeInternal(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour, + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, + t1 = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + t2 = new SpriteText + { + Rotation = 90, + Anchor = Anchor.CentreRight, + Origin = Anchor.TopCentre + }, + t3 = new SpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre + }, + t4 = new SpriteText + { + Rotation = -90, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopCentre + } + }); + + Masking = true; + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + t1.Text = (Padding.Top > 0 ? $"p{Padding.Top}" : string.Empty) + (Margin.Top > 0 ? $"m{Margin.Top}" : string.Empty); + t2.Text = (Padding.Right > 0 ? $"p{Padding.Right}" : string.Empty) + (Margin.Right > 0 ? $"m{Margin.Right}" : string.Empty); + t3.Text = (Padding.Bottom > 0 ? $"p{Padding.Bottom}" : string.Empty) + (Margin.Bottom > 0 ? $"m{Margin.Bottom}" : string.Empty); + t4.Text = (Padding.Left > 0 ? $"p{Padding.Left}" : string.Empty) + (Margin.Left > 0 ? $"m{Margin.Left}" : string.Empty); + + return base.Invalidate(invalidation, source, shallPropagate); + } + + protected override bool OnDrag(InputState state) + { + Position += state.Mouse.Delta; + return true; + } + + protected override bool OnDragEnd(InputState state) => true; + + protected override bool OnDragStart(InputState state) => true; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCasePathInput.cs b/osu.Framework.Tests/Visual/TestCasePathInput.cs index 128930a21..f904f7348 100644 --- a/osu.Framework.Tests/Visual/TestCasePathInput.cs +++ b/osu.Framework.Tests/Visual/TestCasePathInput.cs @@ -1,168 +1,168 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCasePathInput : TestCase - { - private const float path_width = 50; - private const float path_radius = path_width / 2; - - private readonly Path path; - private readonly TestPoint testPoint; - private readonly SpriteText text; - - public TestCasePathInput() - { - Children = new Drawable[] - { - path = new HoverablePath(), - testPoint = new TestPoint(), - text = new SpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } - }; - - testHorizontalPath(); - testDiagonalPath(); - testVShaped(); - testOverlapping(); - } - - private void testHorizontalPath() - { - addPath("Horizontal path", new Vector2(100), new Vector2(300, 100)); - // Left out - test(new Vector2(40, 100), false); - // Left in - test(new Vector2(80, 100), true); - // Cap out - test(new Vector2(60), false); - // Cap in - test(new Vector2(70), true); - //Right out - test(new Vector2(360, 100), false); - // Centre - test(new Vector2(200, 100), true); - // Top out - test(new Vector2(190, 40), false); - // Top in - test(new Vector2(190, 60), true); - } - - private void testDiagonalPath() - { - addPath("Diagonal path", new Vector2(300), new Vector2(100)); - // Top-left out - test(new Vector2(50), false); - // Top-left in - test(new Vector2(80), true); - // Left out - test(new Vector2(145, 235), false); - // Left in - test(new Vector2(170, 235), true); - // Cap out - test(new Vector2(355, 300), false); - // Cap in - test(new Vector2(340, 300), true); - } - - private void testVShaped() - { - addPath("V-shaped", new Vector2(100), new Vector2(300), new Vector2(500, 100)); - // Intersection out - test(new Vector2(300, 225), false); - // Intersection in - test(new Vector2(300, 240), true); - // Bottom cap out - test(new Vector2(300, 355), false); - // Bottom cap in - test(new Vector2(300, 340), true); - } - - private void testOverlapping() - { - addPath("Overlapping", new Vector2(100), new Vector2(600), new Vector2(800, 300), new Vector2(100, 400)); - // Left intersection out - test(new Vector2(250, 325), false); - // Left intersection in - test(new Vector2(260, 325), true); - // Top intersection out - test(new Vector2(380, 300), false); - // Top intersection in - test(new Vector2(380, 320), true); - // Triangle left intersection out - test(new Vector2(475, 400), false); - // Triangle left intersection in - test(new Vector2(460, 400), true); - // Triangle right intersection out - test(new Vector2(690, 370), false); - // Triangle right intersection in - test(new Vector2(700, 370), true); - // Triangle bottom intersection out - test(new Vector2(590, 515), false); - // Triangle bottom intersection in - test(new Vector2(590, 525), true); - // Centre intersection in - test(new Vector2(370, 360), true); - } - - protected override bool OnMouseMove(InputState state) - { - text.Text = path.ToLocalSpace(state.Mouse.NativeState.Position).ToString(); - return base.OnMouseMove(state); - } - - private void addPath(string name, params Vector2[] vertices) => AddStep(name, () => - { - path.PathWidth = path_width; - path.Positions = vertices.ToList(); - }); - - private void test(Vector2 position, bool shouldReceiveMouseInput) - { - AddAssert($"Test @ {position} = {shouldReceiveMouseInput}", () => - { - testPoint.Position = position; - return path.ReceiveMouseInputAt(path.ToScreenSpace(position)) == shouldReceiveMouseInput; - }); - } - - private class TestPoint : CircularContainer - { - public TestPoint() - { - Origin = Anchor.Centre; - - Size = new Vector2(5); - Colour = Color4.Red; - Masking = true; - - InternalChild = new Box { RelativeSizeAxes = Axes.Both }; - } - } - - private class HoverablePath : Path - { - protected override bool OnHover(InputState state) - { - Colour = Color4.Green; - return true; - } - - protected override void OnHoverLost(InputState state) - { - Colour = Color4.White; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCasePathInput : TestCase + { + private const float path_width = 50; + private const float path_radius = path_width / 2; + + private readonly Path path; + private readonly TestPoint testPoint; + private readonly SpriteText text; + + public TestCasePathInput() + { + Children = new Drawable[] + { + path = new HoverablePath(), + testPoint = new TestPoint(), + text = new SpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } + }; + + testHorizontalPath(); + testDiagonalPath(); + testVShaped(); + testOverlapping(); + } + + private void testHorizontalPath() + { + addPath("Horizontal path", new Vector2(100), new Vector2(300, 100)); + // Left out + test(new Vector2(40, 100), false); + // Left in + test(new Vector2(80, 100), true); + // Cap out + test(new Vector2(60), false); + // Cap in + test(new Vector2(70), true); + //Right out + test(new Vector2(360, 100), false); + // Centre + test(new Vector2(200, 100), true); + // Top out + test(new Vector2(190, 40), false); + // Top in + test(new Vector2(190, 60), true); + } + + private void testDiagonalPath() + { + addPath("Diagonal path", new Vector2(300), new Vector2(100)); + // Top-left out + test(new Vector2(50), false); + // Top-left in + test(new Vector2(80), true); + // Left out + test(new Vector2(145, 235), false); + // Left in + test(new Vector2(170, 235), true); + // Cap out + test(new Vector2(355, 300), false); + // Cap in + test(new Vector2(340, 300), true); + } + + private void testVShaped() + { + addPath("V-shaped", new Vector2(100), new Vector2(300), new Vector2(500, 100)); + // Intersection out + test(new Vector2(300, 225), false); + // Intersection in + test(new Vector2(300, 240), true); + // Bottom cap out + test(new Vector2(300, 355), false); + // Bottom cap in + test(new Vector2(300, 340), true); + } + + private void testOverlapping() + { + addPath("Overlapping", new Vector2(100), new Vector2(600), new Vector2(800, 300), new Vector2(100, 400)); + // Left intersection out + test(new Vector2(250, 325), false); + // Left intersection in + test(new Vector2(260, 325), true); + // Top intersection out + test(new Vector2(380, 300), false); + // Top intersection in + test(new Vector2(380, 320), true); + // Triangle left intersection out + test(new Vector2(475, 400), false); + // Triangle left intersection in + test(new Vector2(460, 400), true); + // Triangle right intersection out + test(new Vector2(690, 370), false); + // Triangle right intersection in + test(new Vector2(700, 370), true); + // Triangle bottom intersection out + test(new Vector2(590, 515), false); + // Triangle bottom intersection in + test(new Vector2(590, 525), true); + // Centre intersection in + test(new Vector2(370, 360), true); + } + + protected override bool OnMouseMove(InputState state) + { + text.Text = path.ToLocalSpace(state.Mouse.NativeState.Position).ToString(); + return base.OnMouseMove(state); + } + + private void addPath(string name, params Vector2[] vertices) => AddStep(name, () => + { + path.PathWidth = path_width; + path.Positions = vertices.ToList(); + }); + + private void test(Vector2 position, bool shouldReceiveMouseInput) + { + AddAssert($"Test @ {position} = {shouldReceiveMouseInput}", () => + { + testPoint.Position = position; + return path.ReceiveMouseInputAt(path.ToScreenSpace(position)) == shouldReceiveMouseInput; + }); + } + + private class TestPoint : CircularContainer + { + public TestPoint() + { + Origin = Anchor.Centre; + + Size = new Vector2(5); + Colour = Color4.Red; + Masking = true; + + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + } + + private class HoverablePath : Path + { + protected override bool OnHover(InputState state) + { + Colour = Color4.Green; + return true; + } + + protected override void OnHoverLost(InputState state) + { + Colour = Color4.White; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCasePropertyBoundaries.cs b/osu.Framework.Tests/Visual/TestCasePropertyBoundaries.cs index 0eb6ecec6..0e7f8373f 100644 --- a/osu.Framework.Tests/Visual/TestCasePropertyBoundaries.cs +++ b/osu.Framework.Tests/Visual/TestCasePropertyBoundaries.cs @@ -1,92 +1,92 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("ensure validity of drawables when receiving certain values")] - public class TestCasePropertyBoundaries : TestCase - { - [BackgroundDependencyLoader] - private void load() - { - testPositiveScale(); - testZeroScale(); - testNegativeScale(); - } - - private void testPositiveScale() - { - var box = new Box - { - Size = new Vector2(100), - Scale = new Vector2(2) - }; - - Add(box); - - AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); - AddAssert("Box is present", () => box.IsPresent); - AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); - } - - private void testZeroScale() - { - var box = new Box - { - Size = new Vector2(100), - Scale = new Vector2(0) - }; - - Add(box); - - AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); - AddAssert("Box is present", () => !box.IsPresent); - AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); - } - - private void testNegativeScale() - { - var box = new Box - { - Size = new Vector2(100), - Scale = new Vector2(-2) - }; - - Add(box); - - AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); - AddAssert("Box is present", () => box.IsPresent); - AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); - } - - private bool checkDrawInfo(DrawInfo drawInfo) - { - return checkFloat(drawInfo.Matrix.M11) - && checkFloat(drawInfo.Matrix.M12) - && checkFloat(drawInfo.Matrix.M13) - && checkFloat(drawInfo.Matrix.M21) - && checkFloat(drawInfo.Matrix.M22) - && checkFloat(drawInfo.Matrix.M23) - && checkFloat(drawInfo.Matrix.M31) - && checkFloat(drawInfo.Matrix.M32) - && checkFloat(drawInfo.Matrix.M33) - && checkFloat(drawInfo.MatrixInverse.M11) - && checkFloat(drawInfo.MatrixInverse.M12) - && checkFloat(drawInfo.MatrixInverse.M13) - && checkFloat(drawInfo.MatrixInverse.M21) - && checkFloat(drawInfo.MatrixInverse.M22) - && checkFloat(drawInfo.MatrixInverse.M23) - && checkFloat(drawInfo.MatrixInverse.M31) - && checkFloat(drawInfo.MatrixInverse.M32) - && checkFloat(drawInfo.MatrixInverse.M33); - } - - private bool checkFloat(float value) => !float.IsNaN(value) && !float.IsInfinity(value) && !float.IsNegativeInfinity(value); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("ensure validity of drawables when receiving certain values")] + public class TestCasePropertyBoundaries : TestCase + { + [BackgroundDependencyLoader] + private void load() + { + testPositiveScale(); + testZeroScale(); + testNegativeScale(); + } + + private void testPositiveScale() + { + var box = new Box + { + Size = new Vector2(100), + Scale = new Vector2(2) + }; + + Add(box); + + AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); + AddAssert("Box is present", () => box.IsPresent); + AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); + } + + private void testZeroScale() + { + var box = new Box + { + Size = new Vector2(100), + Scale = new Vector2(0) + }; + + Add(box); + + AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); + AddAssert("Box is present", () => !box.IsPresent); + AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); + } + + private void testNegativeScale() + { + var box = new Box + { + Size = new Vector2(100), + Scale = new Vector2(-2) + }; + + Add(box); + + AddAssert("Box is loaded", () => box.LoadState >= LoadState.Ready); + AddAssert("Box is present", () => box.IsPresent); + AddAssert("Box has valid draw matrix", () => checkDrawInfo(box.DrawInfo)); + } + + private bool checkDrawInfo(DrawInfo drawInfo) + { + return checkFloat(drawInfo.Matrix.M11) + && checkFloat(drawInfo.Matrix.M12) + && checkFloat(drawInfo.Matrix.M13) + && checkFloat(drawInfo.Matrix.M21) + && checkFloat(drawInfo.Matrix.M22) + && checkFloat(drawInfo.Matrix.M23) + && checkFloat(drawInfo.Matrix.M31) + && checkFloat(drawInfo.Matrix.M32) + && checkFloat(drawInfo.Matrix.M33) + && checkFloat(drawInfo.MatrixInverse.M11) + && checkFloat(drawInfo.MatrixInverse.M12) + && checkFloat(drawInfo.MatrixInverse.M13) + && checkFloat(drawInfo.MatrixInverse.M21) + && checkFloat(drawInfo.MatrixInverse.M22) + && checkFloat(drawInfo.MatrixInverse.M23) + && checkFloat(drawInfo.MatrixInverse.M31) + && checkFloat(drawInfo.MatrixInverse.M32) + && checkFloat(drawInfo.MatrixInverse.M33); + } + + private bool checkFloat(float value) => !float.IsNaN(value) && !float.IsInfinity(value) && !float.IsNegativeInfinity(value); + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseRigidBody.cs b/osu.Framework.Tests/Visual/TestCaseRigidBody.cs index b49481afc..08dd49d99 100644 --- a/osu.Framework.Tests/Visual/TestCaseRigidBody.cs +++ b/osu.Framework.Tests/Visual/TestCaseRigidBody.cs @@ -1,178 +1,178 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.MathUtils; -using osu.Framework.Physics; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseRigidBody : TestCase - { - private readonly TestRigidBodySimulation sim; - - private float restitutionBacking; - private float restitution - { - get { return restitutionBacking; } - set - { - restitutionBacking = value; - - if (sim == null) - return; - - foreach (var d in sim.Children) - d.Restitution = value; - sim.Restitution = value; - } - } - - private float frictionBacking; - private float friction - { - get { return frictionBacking; } - set - { - frictionBacking = value; - - if (sim == null) - return; - - foreach (var d in sim.Children) - d.FrictionCoefficient = value; - sim.FrictionCoefficient = value; - } - } - - public TestCaseRigidBody() - { - Child = sim = new TestRigidBodySimulation { RelativeSizeAxes = Axes.Both }; - - AddStep("Reset bodies", reset); - - AddSliderStep("Simulation speed", 0f, 1f, 0.5f, v => sim.SimulationSpeed = v); - AddSliderStep("Restitution", -1f, 1f, 1f, v => restitution = v); - AddSliderStep("Friction", -1f, 5f, 0f, v => friction = v); - - reset(); - } - - private bool overlapsAny(Drawable d) - { - foreach (var other in sim.Children) - if (other.ScreenSpaceDrawQuad.AABB.IntersectsWith(d.ScreenSpaceDrawQuad.AABB)) - return true; - - return false; - } - - private void generateN(int n, Func> generate) - { - for (int i = 0; i < n; i++) - { - RigidBodyContainer d; - do - { - d = generate(); - } - while (overlapsAny(d)); - - sim.Add(d); - } - } - - private void reset() - { - sim.Clear(); - - Random random = new Random(1337); - - // Add a textbox... because we can. - generateN(3, () => new RigidBodyContainer - { - Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, - Size = new Vector2(1, 0.1f + 0.2f * (float)random.NextDouble()) * (150 + 150 * (float)random.NextDouble()), - Rotation = (float)random.NextDouble() * 360, - Child = new TextBox - { - RelativeSizeAxes = Axes.Both, - PlaceholderText = "Text box fun!", - }, - }); - - // Boxes - generateN(10, () => new TestRigidBody - { - Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, - Size = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 200, - Rotation = (float)random.NextDouble() * 360, - Colour = new Color4(253, 253, 253, 255), - }); - - // Circles - generateN(5, () => - { - Vector2 size = new Vector2((float)random.NextDouble()) * 200; - return new TestRigidBody - { - Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, - Size = size, - Rotation = (float)random.NextDouble() * 360, - CornerRadius = size.X / 2, - Colour = new Color4(253, 253, 253, 255), - Masking = true, - }; - }); - - // Totally random stuff - generateN(10, () => - { - Vector2 size = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 200; - return new TestRigidBody - { - Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, - Size = size, - Rotation = (float)random.NextDouble() * 360, - Shear = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 2 - new Vector2(1), - CornerRadius = (float)random.NextDouble() * Math.Min(size.X, size.Y) / 2, - Colour = new Color4(253, 253, 253, 255), - Masking = true, - }; - }); - - // Set appropriate properties - foreach (var d in sim.Children) - { - d.Mass = Math.Max(0.01f, d.ScreenSpaceDrawQuad.Area); - d.FrictionCoefficient = friction; - d.Restitution = restitution; - } - } - - private class TestRigidBody : RigidBodyContainer - { - public TestRigidBody() - { - Child = new Box { RelativeSizeAxes = Axes.Both }; - } - } - - private class TestRigidBodySimulation : RigidBodySimulation - { - protected override void LoadComplete() - { - base.LoadComplete(); - - foreach (var d in Children) - d.ApplyImpulse(new Vector2(RNG.NextSingle() - 0.5f, RNG.NextSingle() - 0.5f) * 100, d.Centre + new Vector2(RNG.NextSingle() - 0.5f, RNG.NextSingle() - 0.5f) * 100); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.MathUtils; +using osu.Framework.Physics; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseRigidBody : TestCase + { + private readonly TestRigidBodySimulation sim; + + private float restitutionBacking; + private float restitution + { + get { return restitutionBacking; } + set + { + restitutionBacking = value; + + if (sim == null) + return; + + foreach (var d in sim.Children) + d.Restitution = value; + sim.Restitution = value; + } + } + + private float frictionBacking; + private float friction + { + get { return frictionBacking; } + set + { + frictionBacking = value; + + if (sim == null) + return; + + foreach (var d in sim.Children) + d.FrictionCoefficient = value; + sim.FrictionCoefficient = value; + } + } + + public TestCaseRigidBody() + { + Child = sim = new TestRigidBodySimulation { RelativeSizeAxes = Axes.Both }; + + AddStep("Reset bodies", reset); + + AddSliderStep("Simulation speed", 0f, 1f, 0.5f, v => sim.SimulationSpeed = v); + AddSliderStep("Restitution", -1f, 1f, 1f, v => restitution = v); + AddSliderStep("Friction", -1f, 5f, 0f, v => friction = v); + + reset(); + } + + private bool overlapsAny(Drawable d) + { + foreach (var other in sim.Children) + if (other.ScreenSpaceDrawQuad.AABB.IntersectsWith(d.ScreenSpaceDrawQuad.AABB)) + return true; + + return false; + } + + private void generateN(int n, Func> generate) + { + for (int i = 0; i < n; i++) + { + RigidBodyContainer d; + do + { + d = generate(); + } + while (overlapsAny(d)); + + sim.Add(d); + } + } + + private void reset() + { + sim.Clear(); + + Random random = new Random(1337); + + // Add a textbox... because we can. + generateN(3, () => new RigidBodyContainer + { + Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, + Size = new Vector2(1, 0.1f + 0.2f * (float)random.NextDouble()) * (150 + 150 * (float)random.NextDouble()), + Rotation = (float)random.NextDouble() * 360, + Child = new TextBox + { + RelativeSizeAxes = Axes.Both, + PlaceholderText = "Text box fun!", + }, + }); + + // Boxes + generateN(10, () => new TestRigidBody + { + Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, + Size = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 200, + Rotation = (float)random.NextDouble() * 360, + Colour = new Color4(253, 253, 253, 255), + }); + + // Circles + generateN(5, () => + { + Vector2 size = new Vector2((float)random.NextDouble()) * 200; + return new TestRigidBody + { + Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, + Size = size, + Rotation = (float)random.NextDouble() * 360, + CornerRadius = size.X / 2, + Colour = new Color4(253, 253, 253, 255), + Masking = true, + }; + }); + + // Totally random stuff + generateN(10, () => + { + Vector2 size = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 200; + return new TestRigidBody + { + Position = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 1000, + Size = size, + Rotation = (float)random.NextDouble() * 360, + Shear = new Vector2((float)random.NextDouble(), (float)random.NextDouble()) * 2 - new Vector2(1), + CornerRadius = (float)random.NextDouble() * Math.Min(size.X, size.Y) / 2, + Colour = new Color4(253, 253, 253, 255), + Masking = true, + }; + }); + + // Set appropriate properties + foreach (var d in sim.Children) + { + d.Mass = Math.Max(0.01f, d.ScreenSpaceDrawQuad.Area); + d.FrictionCoefficient = friction; + d.Restitution = restitution; + } + } + + private class TestRigidBody : RigidBodyContainer + { + public TestRigidBody() + { + Child = new Box { RelativeSizeAxes = Axes.Both }; + } + } + + private class TestRigidBodySimulation : RigidBodySimulation + { + protected override void LoadComplete() + { + base.LoadComplete(); + + foreach (var d in Children) + d.ApplyImpulse(new Vector2(RNG.NextSingle() - 0.5f, RNG.NextSingle() - 0.5f) * 100, d.Centre + new Vector2(RNG.NextSingle() - 0.5f, RNG.NextSingle() - 0.5f) * 100); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseScreen.cs b/osu.Framework.Tests/Visual/TestCaseScreen.cs index 789a40493..221d7b7d2 100644 --- a/osu.Framework.Tests/Visual/TestCaseScreen.cs +++ b/osu.Framework.Tests/Visual/TestCaseScreen.cs @@ -1,117 +1,117 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.MathUtils; -using osu.Framework.Screens; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseScreen : TestCase - { - public TestCaseScreen() - { - Add(new TestScreen()); - } - - private class TestScreen : Screen - { - public int Sequence; - private Button popButton; - - private const int transition_time = 500; - - protected override void OnEntering(Screen last) - { - if (last != null) - { - //only show the pop button if we are entered form another screen. - popButton.Alpha = 1; - } - - Content.MoveTo(new Vector2(0, -DrawSize.Y)); - Content.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); - } - - protected override bool OnExiting(Screen next) - { - Content.MoveTo(new Vector2(0, -DrawSize.Y), transition_time, Easing.OutQuint); - return base.OnExiting(next); - } - - protected override void OnSuspending(Screen next) - { - Content.MoveTo(new Vector2(0, DrawSize.Y), transition_time, Easing.OutQuint); - } - - protected override void OnResuming(Screen last) - { - Content.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = new Color4( - Math.Max(0.5f, RNG.NextSingle()), - Math.Max(0.5f, RNG.NextSingle()), - Math.Max(0.5f, RNG.NextSingle()), - 1), - }, - new SpriteText - { - Text = $@"Mode {Sequence}", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextSize = 50, - }, - popButton = new Button - { - Text = @"Pop", - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.1f), - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - BackgroundColour = Color4.Red, - Alpha = 0, - Action = Exit - }, - new Button - { - Text = @"Push", - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.1f), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - BackgroundColour = Color4.YellowGreen, - Action = delegate - { - Push(new TestScreen - { - Sequence = Sequence + 1, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - } - } - }; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.MathUtils; +using osu.Framework.Screens; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseScreen : TestCase + { + public TestCaseScreen() + { + Add(new TestScreen()); + } + + private class TestScreen : Screen + { + public int Sequence; + private Button popButton; + + private const int transition_time = 500; + + protected override void OnEntering(Screen last) + { + if (last != null) + { + //only show the pop button if we are entered form another screen. + popButton.Alpha = 1; + } + + Content.MoveTo(new Vector2(0, -DrawSize.Y)); + Content.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); + } + + protected override bool OnExiting(Screen next) + { + Content.MoveTo(new Vector2(0, -DrawSize.Y), transition_time, Easing.OutQuint); + return base.OnExiting(next); + } + + protected override void OnSuspending(Screen next) + { + Content.MoveTo(new Vector2(0, DrawSize.Y), transition_time, Easing.OutQuint); + } + + protected override void OnResuming(Screen last) + { + Content.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = new Color4( + Math.Max(0.5f, RNG.NextSingle()), + Math.Max(0.5f, RNG.NextSingle()), + Math.Max(0.5f, RNG.NextSingle()), + 1), + }, + new SpriteText + { + Text = $@"Mode {Sequence}", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextSize = 50, + }, + popButton = new Button + { + Text = @"Pop", + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.1f), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + BackgroundColour = Color4.Red, + Alpha = 0, + Action = Exit + }, + new Button + { + Text = @"Push", + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.1f), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + BackgroundColour = Color4.YellowGreen, + Action = delegate + { + Push(new TestScreen + { + Sequence = Sequence + 1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + }; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseScrollableFlow.cs b/osu.Framework.Tests/Visual/TestCaseScrollableFlow.cs index 593bd3acc..3ad5a227a 100644 --- a/osu.Framework.Tests/Visual/TestCaseScrollableFlow.cs +++ b/osu.Framework.Tests/Visual/TestCaseScrollableFlow.cs @@ -1,131 +1,131 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.MathUtils; -using osu.Framework.Testing; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseScrollableFlow : TestCase - { - private readonly ScheduledDelegate boxCreator; - - private ScrollContainer scroll; - private FillFlowContainer flow; - - private void createArea(Direction dir) - { - Axes scrollAxis = dir == Direction.Horizontal ? Axes.X : Axes.Y; - - Children = new[] - { - scroll = new ScrollContainer(dir) - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - flow = new FillFlowContainer - { - LayoutDuration = 100, - LayoutEasing = Easing.Out, - Spacing = new Vector2(1, 1), - RelativeSizeAxes = Axes.Both & ~scrollAxis, - AutoSizeAxes = scrollAxis, - Padding = new MarginPadding(5) - } - }, - }, - }; - } - - private void createAreaBoth() - { - Children = new[] - { - new ScrollContainer(Direction.Horizontal) - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 150 }, - Children = new[] - { - scroll = new ScrollContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Children = new[] - { - flow = new FillFlowContainer - { - LayoutDuration = 100, - LayoutEasing = Easing.Out, - Spacing = new Vector2(1, 1), - Size = new Vector2(1000, 0), - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(5) - } - } - } - }, - }, - }; - - scroll.ScrollContent.AutoSizeAxes = Axes.None; - scroll.ScrollContent.RelativeSizeAxes = Axes.None; - scroll.ScrollContent.AutoSizeAxes = Axes.Both; - } - - public TestCaseScrollableFlow() - { - Direction scrollDir; - - createArea(scrollDir = Direction.Vertical); - - AddStep("Vertical", delegate { createArea(scrollDir = Direction.Vertical); }); - AddStep("Horizontal", delegate { createArea(scrollDir = Direction.Horizontal); }); - AddStep("Both", createAreaBoth); - - AddStep("Dragger Anchor 1", delegate { scroll.ScrollbarAnchor = scrollDir == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft; }); - AddStep("Dragger Anchor 2", delegate { scroll.ScrollbarAnchor = Anchor.TopLeft; }); - - AddStep("Dragger Visible", delegate { scroll.ScrollbarVisible = !scroll.ScrollbarVisible; }); - AddStep("Dragger Overlap", delegate { scroll.ScrollbarOverlapsContent = !scroll.ScrollbarOverlapsContent; }); - - boxCreator?.Cancel(); - boxCreator = Scheduler.AddDelayed(delegate - { - if (Parent == null) return; - - Box box; - Container container = new Container - { - Size = new Vector2(80, 80), - Children = new[] - { - box = new Box - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) - } - } - }; - - flow.Add(container); - - container.FadeInFromZero(1000); - - double displayTime = RNG.Next(0, 20000); - box.Delay(displayTime).ScaleTo(0.5f, 4000).RotateTo((RNG.NextSingle() - 0.5f) * 90, 4000); - container.Delay(displayTime).FadeOut(4000).Expire(); - - }, 100, true); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseScrollableFlow : TestCase + { + private readonly ScheduledDelegate boxCreator; + + private ScrollContainer scroll; + private FillFlowContainer flow; + + private void createArea(Direction dir) + { + Axes scrollAxis = dir == Direction.Horizontal ? Axes.X : Axes.Y; + + Children = new[] + { + scroll = new ScrollContainer(dir) + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + flow = new FillFlowContainer + { + LayoutDuration = 100, + LayoutEasing = Easing.Out, + Spacing = new Vector2(1, 1), + RelativeSizeAxes = Axes.Both & ~scrollAxis, + AutoSizeAxes = scrollAxis, + Padding = new MarginPadding(5) + } + }, + }, + }; + } + + private void createAreaBoth() + { + Children = new[] + { + new ScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 150 }, + Children = new[] + { + scroll = new ScrollContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new[] + { + flow = new FillFlowContainer + { + LayoutDuration = 100, + LayoutEasing = Easing.Out, + Spacing = new Vector2(1, 1), + Size = new Vector2(1000, 0), + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(5) + } + } + } + }, + }, + }; + + scroll.ScrollContent.AutoSizeAxes = Axes.None; + scroll.ScrollContent.RelativeSizeAxes = Axes.None; + scroll.ScrollContent.AutoSizeAxes = Axes.Both; + } + + public TestCaseScrollableFlow() + { + Direction scrollDir; + + createArea(scrollDir = Direction.Vertical); + + AddStep("Vertical", delegate { createArea(scrollDir = Direction.Vertical); }); + AddStep("Horizontal", delegate { createArea(scrollDir = Direction.Horizontal); }); + AddStep("Both", createAreaBoth); + + AddStep("Dragger Anchor 1", delegate { scroll.ScrollbarAnchor = scrollDir == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft; }); + AddStep("Dragger Anchor 2", delegate { scroll.ScrollbarAnchor = Anchor.TopLeft; }); + + AddStep("Dragger Visible", delegate { scroll.ScrollbarVisible = !scroll.ScrollbarVisible; }); + AddStep("Dragger Overlap", delegate { scroll.ScrollbarOverlapsContent = !scroll.ScrollbarOverlapsContent; }); + + boxCreator?.Cancel(); + boxCreator = Scheduler.AddDelayed(delegate + { + if (Parent == null) return; + + Box box; + Container container = new Container + { + Size = new Vector2(80, 80), + Children = new[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) + } + } + }; + + flow.Add(container); + + container.FadeInFromZero(1000); + + double displayTime = RNG.Next(0, 20000); + box.Delay(displayTime).ScaleTo(0.5f, 4000).RotateTo((RNG.NextSingle() - 0.5f) * 90, 4000); + container.Delay(displayTime).FadeOut(4000).Expire(); + + }, 100, true); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseSearchContainer.cs b/osu.Framework.Tests/Visual/TestCaseSearchContainer.cs index d0db8a546..2b2f682d5 100644 --- a/osu.Framework.Tests/Visual/TestCaseSearchContainer.cs +++ b/osu.Framework.Tests/Visual/TestCaseSearchContainer.cs @@ -1,191 +1,191 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseSearchContainer : TestCase - { - public TestCaseSearchContainer() - { - SearchContainer search; - TextBox textBox; - - Children = new Drawable[] { - textBox = new TextBox - { - Size = new Vector2(300, 40), - }, - search = new SearchContainer - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 40 }, - Children = new[] - { - new HeaderContainer - { - AutoSizeAxes = Axes.Both, - Children = new[] - { - new HeaderContainer("Subsection 1") - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new SearchableText - { - Text = "test", - }, - new SearchableText - { - Text = "TEST", - }, - new SearchableText - { - Text = "123", - }, - new SearchableText - { - Text = "444", - }, - new FilterableFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new [] - { - new SpriteText - { - Text = "multi", - }, - new SpriteText - { - Text = "piece", - }, - new SpriteText - { - Text = "container", - }, - } - }, - new SearchableText - { - Text = "öüäéèêáàâ", - } - } - }, - new HeaderContainer("Subsection 2") - { - AutoSizeAxes = Axes.Both, - Children = new[] - { - new SearchableText - { - Text = "?!()[]{}" - }, - new SearchableText - { - Text = "@€$" - }, - }, - }, - }, - } - } - } - }; - - new Dictionary - { - { "test", 2 }, - { "sUbSeCtIoN 1", 6 }, - { "€", 1 }, - { "èê", 1 }, - { "321", 0 }, - { "mul pi", 1}, - { "header", 8 } - }.ToList().ForEach(term => - { - AddStep("Search term: " + term.Key, () => search.SearchTerm = term.Key); - AddAssert("Visible end-children: " + term.Value, () => term.Value == search.Children.SelectMany(container => container.Children.Cast()).SelectMany(container => container.Children).Count(drawable => drawable.IsPresent)); - }); - - textBox.Current.ValueChanged += newValue => search.SearchTerm = newValue; - } - - private class HeaderContainer : Container, IHasFilterableChildren - { - public IEnumerable FilterTerms => header.FilterTerms; - - public bool MatchingFilter - { - set - { - if (value) - this.FadeIn(); - else - this.FadeOut(); - } - } - - public IEnumerable FilterableChildren => Children.OfType(); - - protected override Container Content => flowContainer; - - private readonly SearchableText header; - private readonly FillFlowContainer flowContainer; - - public HeaderContainer(string headerText = "Header") - { - AddInternal(header = new SearchableText - { - Text = headerText, - }); - AddInternal(flowContainer = new FillFlowContainer - { - Margin = new MarginPadding { Top = header.TextSize, Left = 30 }, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - }); - } - } - - private class FilterableFlowContainer : FillFlowContainer, IFilterable - { - public IEnumerable FilterTerms => Children.OfType().SelectMany(d => d.FilterTerms); - - public bool MatchingFilter - { - set - { - if (value) - Show(); - else - Hide(); - } - } - } - - private class SearchableText : SpriteText, IFilterable - { - public bool MatchingFilter - { - set - { - if (value) - Show(); - else - Hide(); - } - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseSearchContainer : TestCase + { + public TestCaseSearchContainer() + { + SearchContainer search; + TextBox textBox; + + Children = new Drawable[] { + textBox = new TextBox + { + Size = new Vector2(300, 40), + }, + search = new SearchContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 40 }, + Children = new[] + { + new HeaderContainer + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + new HeaderContainer("Subsection 1") + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new SearchableText + { + Text = "test", + }, + new SearchableText + { + Text = "TEST", + }, + new SearchableText + { + Text = "123", + }, + new SearchableText + { + Text = "444", + }, + new FilterableFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new [] + { + new SpriteText + { + Text = "multi", + }, + new SpriteText + { + Text = "piece", + }, + new SpriteText + { + Text = "container", + }, + } + }, + new SearchableText + { + Text = "öüäéèêáàâ", + } + } + }, + new HeaderContainer("Subsection 2") + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + new SearchableText + { + Text = "?!()[]{}" + }, + new SearchableText + { + Text = "@€$" + }, + }, + }, + }, + } + } + } + }; + + new Dictionary + { + { "test", 2 }, + { "sUbSeCtIoN 1", 6 }, + { "€", 1 }, + { "èê", 1 }, + { "321", 0 }, + { "mul pi", 1}, + { "header", 8 } + }.ToList().ForEach(term => + { + AddStep("Search term: " + term.Key, () => search.SearchTerm = term.Key); + AddAssert("Visible end-children: " + term.Value, () => term.Value == search.Children.SelectMany(container => container.Children.Cast()).SelectMany(container => container.Children).Count(drawable => drawable.IsPresent)); + }); + + textBox.Current.ValueChanged += newValue => search.SearchTerm = newValue; + } + + private class HeaderContainer : Container, IHasFilterableChildren + { + public IEnumerable FilterTerms => header.FilterTerms; + + public bool MatchingFilter + { + set + { + if (value) + this.FadeIn(); + else + this.FadeOut(); + } + } + + public IEnumerable FilterableChildren => Children.OfType(); + + protected override Container Content => flowContainer; + + private readonly SearchableText header; + private readonly FillFlowContainer flowContainer; + + public HeaderContainer(string headerText = "Header") + { + AddInternal(header = new SearchableText + { + Text = headerText, + }); + AddInternal(flowContainer = new FillFlowContainer + { + Margin = new MarginPadding { Top = header.TextSize, Left = 30 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + }); + } + } + + private class FilterableFlowContainer : FillFlowContainer, IFilterable + { + public IEnumerable FilterTerms => Children.OfType().SelectMany(d => d.FilterTerms); + + public bool MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + } + + private class SearchableText : SpriteText, IFilterable + { + public bool MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseSizing.cs b/osu.Framework.Tests/Visual/TestCaseSizing.cs index c99f00901..c04745fac 100644 --- a/osu.Framework.Tests/Visual/TestCaseSizing.cs +++ b/osu.Framework.Tests/Visual/TestCaseSizing.cs @@ -1,1150 +1,1150 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("potentially challenging size calculations")] - public class TestCaseSizing : TestCase - { - private readonly Container testContainer; - - public TestCaseSizing() - { - Add(testContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - string[] testNames = - { - @"Multiple children", - @"Nested children", - @"AutoSize bench", - @"RelativeSize bench", - @"SpriteText 1", - @"SpriteText 2", - @"Inverted scaling", - @"RelativeSize", - @"Padding", - @"Margin", - @"Inner Margin", - @"Drawable Margin", - @"Relative Inside Autosize", - @"Negative sizing" - }; - - for (int i = 0; i < testNames.Length; i++) - { - int test = i; - AddStep(testNames[i], delegate { loadTest(test); }); - } - - loadTest(0); - addCrosshair(); - } - - private void addCrosshair() - { - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(22, 4), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(4, 22), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(20, 2), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(2, 20), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - } - - private void loadTest(int testType) - { - testContainer.Clear(); - - Container box; - - switch (testType) - { - case 0: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - addCornerMarkers(box); - - box.Add(new InfofulBox - { - //chameleon = true, - Position = new Vector2(0, 0), - Size = new Vector2(25, 25), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.Blue, - }); - - box.Add(box = new InfofulBox - { - Size = new Vector2(250, 250), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - }); - - box.OnUpdate += delegate { box.Rotation += 0.05f; }; - break; - case 1: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - addCornerMarkers(box, 5); - - - box.Add(box = new InfofulBoxAutoSize - { - Colour = Color4.DarkSeaGreen, - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - }); - - Drawable localBox = box; - box.OnUpdate += delegate { localBox.Rotation += 0.05f; }; - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(100, 100), - Position = new Vector2(50, 50), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.Blue, - }); - break; - case 2: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - addCornerMarkers(box, 10, Color4.YellowGreen); - - for (int i = 0; i < 50; i++) - { - box.Add(box = new InfofulBoxAutoSize - { - Colour = new Color4(253, 253, 253, 255), - Position = new Vector2(-3, -3), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - }); - } - - addCornerMarkers(box, 2); - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(50, 50), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - Colour = Color4.SeaGreen, - }); - break; - case 3: - testContainer.Add(box = new InfofulBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(250, 250) - }); - - addCornerMarkers(box, 10, Color4.YellowGreen); - - for (int i = 0; i < 100; i++) - { - box.Add(box = new InfofulBox - { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(253, 253, 253, 255), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - Size = new Vector2(0.99f, 0.99f) - }); - } - - addCornerMarkers(box, 2); - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(50, 50), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - Colour = Color4.SeaGreen, - }); - break; - case 4: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft - }); - - box.Add(new InfofulBox - { - Position = new Vector2(5, 0), - Size = new Vector2(300, 80), - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Colour = Color4.OrangeRed, - }); - - box.Add(new SpriteText - { - Position = new Vector2(5, -20), - Text = "Test CentreLeft line 1", - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft - }); - - box.Add(new SpriteText - { - Position = new Vector2(5, 20), - Text = "Test CentreLeft line 2", - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft - }); - break; - case 5: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft - }); - - box.Add(new InfofulBox - { - Position = new Vector2(5, 0), - Size = new Vector2(300, 80), - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Colour = Color4.OrangeRed, - }); - - box.Add(new SpriteText - { - Position = new Vector2(5, -20), - Text = "123,456,789=", - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(2f) - }); - - box.Add(new SpriteText - { - Position = new Vector2(5, 20), - Text = "123,456,789ms", - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft - }); - break; - case 6: - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - box.Add(box = new InfofulBoxAutoSize - { - Colour = Color4.OrangeRed, - Position = new Vector2(100, 100), - Origin = Anchor.Centre, - Anchor = Anchor.TopLeft - }); - - box.Add(new InfofulBox - { - Position = new Vector2(100, 100), - Size = new Vector2(100, 100), - Origin = Anchor.Centre, - Anchor = Anchor.TopLeft, - Colour = Color4.OrangeRed, - }); - break; - case 7: - Container shrinkContainer; - Container boxes; - - testContainer.Add(shrinkContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.AliceBlue, - Alpha = 0.2f - }, - boxes = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - } - } - }); - - for (int i = 0; i < 10; i++) - { - boxes.Add(new Box - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(0.9f, 40), - Colour = Color4.AliceBlue, - Alpha = 0.2f - }); - } - - shrinkContainer.ScaleTo(new Vector2(1.5f, 1), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); - break; - - case 8: - { - Container box1; - Container box2; - Container box3; - - testContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // This first guy is used for spacing. - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.125f, 1), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Padding = new MarginPadding(50), - Children = new Drawable[] - { - box1 = new InfofulBox - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Padding = new MarginPadding(50), - Children = new Drawable[] - { - box2 = new InfofulBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Padding = new MarginPadding(50), - Children = new Drawable[] - { - box3 = new InfofulBox - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - } - }); - - foreach (Container b in new[] { box1, box2, box3 }) - b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); - - break; - } - - case 9: - { - Container box1; - Container box2; - Container box3; - - testContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // This first guy is used for spacing. - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.125f, 1), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Margin = new MarginPadding(50), - Children = new Drawable[] - { - box1 = new InfofulBox - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Margin = new MarginPadding(50), - Children = new Drawable[] - { - box2 = new InfofulBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Margin = new MarginPadding(50), - Children = new Drawable[] - { - box3 = new InfofulBox - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - } - }); - - foreach (Container b in new[] { box1, box2, box3 }) - b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); - - break; - } - - case 10: - { - Container box1; - Container box2; - Container box3; - - testContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // This first guy is used for spacing. - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.125f, 1), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new Drawable[] - { - box1 = new InfofulBox - { - Margin = new MarginPadding(50), - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new Drawable[] - { - box2 = new InfofulBox - { - Margin = new MarginPadding(50), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new Drawable[] - { - box3 = new InfofulBox - { - Margin = new MarginPadding(50), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - } - }); - - foreach (Container b in new[] { box1, box2, box3 }) - b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); - - break; - } - - case 11: - { - Drawable box1; - Drawable box2; - Drawable box3; - - testContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // This first guy is used for spacing. - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.125f, 1), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new[] - { - box1 = new Box - { - Margin = new MarginPadding(50), - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new[] - { - box2 = new Box - { - Margin = new MarginPadding(50), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Children = new[] - { - new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - new Container - { - AutoSizeAxes = Axes.Both, - Depth = -1, - Children = new[] - { - box3 = new Box - { - Margin = new MarginPadding(50), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Size = new Vector2(50), - Colour = Color4.Blue, - }, - } - } - } - }, - } - }, - } - }); - - foreach (Drawable b in new[] { box1, box2, box3 }) - b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); - - break; - } - case 12: - { - // demonstrates how relativeaxes drawables act inside an autosize parent - Drawable sizedBox; - - testContainer.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - Size = new Vector2(300), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box { Colour = Color4.Gray, RelativeSizeAxes = Axes.Both }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new[] - { - // defines the size of autosize - sizedBox = new Box - { - Colour = Color4.Red, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(100f) - }, - // gets relative size based on autosize - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f) - }, - } - } - } - } - } - }); - - sizedBox.ScaleTo(new Vector2(2), 1000, Easing.Out).Then().ScaleTo(Vector2.One, 1000, Easing.In).Loop(); - break; - } - case 13: - { - testContainer.Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Children = new[] - { - new FillFlowContainer - { - Name = "Top row", - RelativeSizeAxes = Axes.X, - Height = 200, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(100, 0), - Children = new[] - { - new NegativeSizingContainer(Anchor.TopLeft, true), - new NegativeSizingContainer(Anchor.Centre, true), - new NegativeSizingContainer(Anchor.BottomRight, true) - } - }, - new FillFlowContainer - { - Name = "Bottom row", - RelativeSizeAxes = Axes.X, - Height = 200, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(100, 0), - Children = new[] - { - new NegativeSizingContainer(Anchor.TopLeft, false), - new NegativeSizingContainer(Anchor.Centre, false), - new NegativeSizingContainer(Anchor.BottomRight, false) - } - } - } - }); - - break; - } - } - } - - private void addCornerMarkers(Container box, int size = 50, Color4? colour = null) - { - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new InfofulBox - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - } - - private class NegativeSizingContainer : Container - { - private const float size = 200; - - private readonly Box box; - private readonly SpriteText text; - - private readonly bool useScale; - - public NegativeSizingContainer(Anchor anchor, bool useScale) - { - this.useScale = useScale; - - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - box = new Box - { - Anchor = anchor, - Origin = anchor, - Size = new Vector2(size) - }, - text = new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Red, - BypassAutoSizeAxes = Axes.Both - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (useScale) - box.ScaleTo(new Vector2(-1), 3000, Easing.InSine).Then().ScaleTo(new Vector2(1), 3000, Easing.InSine).Loop(); - else - box.ResizeTo(new Vector2(-size), 3000, Easing.InSine).Then().ResizeTo(new Vector2(size), 3000, Easing.InSine).Loop(); - } - - protected override void Update() - { - base.Update(); - - text.Text = useScale ? $"Scale: {box.Scale}" : $"Size: {box.Size}"; - } - } - } - - internal class InfofulBoxAutoSize : Container - { - protected override Container Content => content; - - private readonly Container content; - - public InfofulBoxAutoSize() - { - AutoSizeAxes = Axes.Both; - - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue, - }, - content = new Container - { - AutoSizeAxes = Axes.Both, - } - }; - } - - public bool AllowDrag = true; - - protected override bool OnDrag(InputState state) - { - if (!AllowDrag) return false; - - Position += state.Mouse.Delta; - return true; - } - - protected override bool OnDragEnd(InputState state) - { - return true; - } - - protected override bool OnDragStart(InputState state) => AllowDrag; - } - - internal class InfofulBox : Container - { - public bool Chameleon = false; - public bool AllowDrag = true; - - protected override bool OnDrag(InputState state) - { - if (!AllowDrag) return false; - - Position += state.Mouse.Delta; - return true; - } - - protected override bool OnDragEnd(InputState state) - { - return true; - } - - protected override bool OnDragStart(InputState state) => AllowDrag; - - public InfofulBox() - { - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue, - }); - } - - private int lastSwitch; - - protected override void Update() - { - if (Chameleon && (int)Time.Current / 1000 != lastSwitch) - { - lastSwitch = (int)Time.Current / 1000; - switch (lastSwitch % 6) - { - case 0: - Anchor = (Anchor)((int)Anchor + 1); - Origin = (Anchor)((int)Origin + 1); - break; - case 1: - this.MoveTo(new Vector2(0, 0), 800, Easing.Out); - break; - case 2: - this.MoveTo(new Vector2(200, 0), 800, Easing.Out); - break; - case 3: - this.MoveTo(new Vector2(200, 200), 800, Easing.Out); - break; - case 4: - this.MoveTo(new Vector2(0, 200), 800, Easing.Out); - break; - case 5: - this.MoveTo(new Vector2(0, 0), 800, Easing.Out); - break; - } - } - - base.Update(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("potentially challenging size calculations")] + public class TestCaseSizing : TestCase + { + private readonly Container testContainer; + + public TestCaseSizing() + { + Add(testContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + string[] testNames = + { + @"Multiple children", + @"Nested children", + @"AutoSize bench", + @"RelativeSize bench", + @"SpriteText 1", + @"SpriteText 2", + @"Inverted scaling", + @"RelativeSize", + @"Padding", + @"Margin", + @"Inner Margin", + @"Drawable Margin", + @"Relative Inside Autosize", + @"Negative sizing" + }; + + for (int i = 0; i < testNames.Length; i++) + { + int test = i; + AddStep(testNames[i], delegate { loadTest(test); }); + } + + loadTest(0); + addCrosshair(); + } + + private void addCrosshair() + { + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(22, 4), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(4, 22), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(20, 2), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(2, 20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + private void loadTest(int testType) + { + testContainer.Clear(); + + Container box; + + switch (testType) + { + case 0: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + addCornerMarkers(box); + + box.Add(new InfofulBox + { + //chameleon = true, + Position = new Vector2(0, 0), + Size = new Vector2(25, 25), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Blue, + }); + + box.Add(box = new InfofulBox + { + Size = new Vector2(250, 250), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + }); + + box.OnUpdate += delegate { box.Rotation += 0.05f; }; + break; + case 1: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + addCornerMarkers(box, 5); + + + box.Add(box = new InfofulBoxAutoSize + { + Colour = Color4.DarkSeaGreen, + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + }); + + Drawable localBox = box; + box.OnUpdate += delegate { localBox.Rotation += 0.05f; }; + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(100, 100), + Position = new Vector2(50, 50), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Blue, + }); + break; + case 2: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + addCornerMarkers(box, 10, Color4.YellowGreen); + + for (int i = 0; i < 50; i++) + { + box.Add(box = new InfofulBoxAutoSize + { + Colour = new Color4(253, 253, 253, 255), + Position = new Vector2(-3, -3), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + }); + } + + addCornerMarkers(box, 2); + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(50, 50), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Colour = Color4.SeaGreen, + }); + break; + case 3: + testContainer.Add(box = new InfofulBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(250, 250) + }); + + addCornerMarkers(box, 10, Color4.YellowGreen); + + for (int i = 0; i < 100; i++) + { + box.Add(box = new InfofulBox + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(253, 253, 253, 255), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Size = new Vector2(0.99f, 0.99f) + }); + } + + addCornerMarkers(box, 2); + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(50, 50), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Colour = Color4.SeaGreen, + }); + break; + case 4: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft + }); + + box.Add(new InfofulBox + { + Position = new Vector2(5, 0), + Size = new Vector2(300, 80), + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Colour = Color4.OrangeRed, + }); + + box.Add(new SpriteText + { + Position = new Vector2(5, -20), + Text = "Test CentreLeft line 1", + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft + }); + + box.Add(new SpriteText + { + Position = new Vector2(5, 20), + Text = "Test CentreLeft line 2", + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft + }); + break; + case 5: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft + }); + + box.Add(new InfofulBox + { + Position = new Vector2(5, 0), + Size = new Vector2(300, 80), + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Colour = Color4.OrangeRed, + }); + + box.Add(new SpriteText + { + Position = new Vector2(5, -20), + Text = "123,456,789=", + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(2f) + }); + + box.Add(new SpriteText + { + Position = new Vector2(5, 20), + Text = "123,456,789ms", + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft + }); + break; + case 6: + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + box.Add(box = new InfofulBoxAutoSize + { + Colour = Color4.OrangeRed, + Position = new Vector2(100, 100), + Origin = Anchor.Centre, + Anchor = Anchor.TopLeft + }); + + box.Add(new InfofulBox + { + Position = new Vector2(100, 100), + Size = new Vector2(100, 100), + Origin = Anchor.Centre, + Anchor = Anchor.TopLeft, + Colour = Color4.OrangeRed, + }); + break; + case 7: + Container shrinkContainer; + Container boxes; + + testContainer.Add(shrinkContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.AliceBlue, + Alpha = 0.2f + }, + boxes = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + } + } + }); + + for (int i = 0; i < 10; i++) + { + boxes.Add(new Box + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(0.9f, 40), + Colour = Color4.AliceBlue, + Alpha = 0.2f + }); + } + + shrinkContainer.ScaleTo(new Vector2(1.5f, 1), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); + break; + + case 8: + { + Container box1; + Container box2; + Container box3; + + testContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // This first guy is used for spacing. + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.125f, 1), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Padding = new MarginPadding(50), + Children = new Drawable[] + { + box1 = new InfofulBox + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Padding = new MarginPadding(50), + Children = new Drawable[] + { + box2 = new InfofulBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Padding = new MarginPadding(50), + Children = new Drawable[] + { + box3 = new InfofulBox + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + } + }); + + foreach (Container b in new[] { box1, box2, box3 }) + b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); + + break; + } + + case 9: + { + Container box1; + Container box2; + Container box3; + + testContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // This first guy is used for spacing. + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.125f, 1), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Margin = new MarginPadding(50), + Children = new Drawable[] + { + box1 = new InfofulBox + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Margin = new MarginPadding(50), + Children = new Drawable[] + { + box2 = new InfofulBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Margin = new MarginPadding(50), + Children = new Drawable[] + { + box3 = new InfofulBox + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + } + }); + + foreach (Container b in new[] { box1, box2, box3 }) + b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); + + break; + } + + case 10: + { + Container box1; + Container box2; + Container box3; + + testContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // This first guy is used for spacing. + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.125f, 1), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new Drawable[] + { + box1 = new InfofulBox + { + Margin = new MarginPadding(50), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new Drawable[] + { + box2 = new InfofulBox + { + Margin = new MarginPadding(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new Drawable[] + { + box3 = new InfofulBox + { + Margin = new MarginPadding(50), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + } + }); + + foreach (Container b in new[] { box1, box2, box3 }) + b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); + + break; + } + + case 11: + { + Drawable box1; + Drawable box2; + Drawable box3; + + testContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // This first guy is used for spacing. + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.125f, 1), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new[] + { + box1 = new Box + { + Margin = new MarginPadding(50), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new[] + { + box2 = new Box + { + Margin = new MarginPadding(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Children = new[] + { + new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Depth = -1, + Children = new[] + { + box3 = new Box + { + Margin = new MarginPadding(50), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Size = new Vector2(50), + Colour = Color4.Blue, + }, + } + } + } + }, + } + }, + } + }); + + foreach (Drawable b in new[] { box1, box2, box3 }) + b.ScaleTo(new Vector2(2), 1000).Then().ScaleTo(Vector2.One, 1000).Loop(); + + break; + } + case 12: + { + // demonstrates how relativeaxes drawables act inside an autosize parent + Drawable sizedBox; + + testContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Size = new Vector2(300), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box { Colour = Color4.Gray, RelativeSizeAxes = Axes.Both }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + // defines the size of autosize + sizedBox = new Box + { + Colour = Color4.Red, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(100f) + }, + // gets relative size based on autosize + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f) + }, + } + } + } + } + } + }); + + sizedBox.ScaleTo(new Vector2(2), 1000, Easing.Out).Then().ScaleTo(Vector2.One, 1000, Easing.In).Loop(); + break; + } + case 13: + { + testContainer.Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new[] + { + new FillFlowContainer + { + Name = "Top row", + RelativeSizeAxes = Axes.X, + Height = 200, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(100, 0), + Children = new[] + { + new NegativeSizingContainer(Anchor.TopLeft, true), + new NegativeSizingContainer(Anchor.Centre, true), + new NegativeSizingContainer(Anchor.BottomRight, true) + } + }, + new FillFlowContainer + { + Name = "Bottom row", + RelativeSizeAxes = Axes.X, + Height = 200, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(100, 0), + Children = new[] + { + new NegativeSizingContainer(Anchor.TopLeft, false), + new NegativeSizingContainer(Anchor.Centre, false), + new NegativeSizingContainer(Anchor.BottomRight, false) + } + } + } + }); + + break; + } + } + } + + private void addCornerMarkers(Container box, int size = 50, Color4? colour = null) + { + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new InfofulBox + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + } + + private class NegativeSizingContainer : Container + { + private const float size = 200; + + private readonly Box box; + private readonly SpriteText text; + + private readonly bool useScale; + + public NegativeSizingContainer(Anchor anchor, bool useScale) + { + this.useScale = useScale; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + box = new Box + { + Anchor = anchor, + Origin = anchor, + Size = new Vector2(size) + }, + text = new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Red, + BypassAutoSizeAxes = Axes.Both + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (useScale) + box.ScaleTo(new Vector2(-1), 3000, Easing.InSine).Then().ScaleTo(new Vector2(1), 3000, Easing.InSine).Loop(); + else + box.ResizeTo(new Vector2(-size), 3000, Easing.InSine).Then().ResizeTo(new Vector2(size), 3000, Easing.InSine).Loop(); + } + + protected override void Update() + { + base.Update(); + + text.Text = useScale ? $"Scale: {box.Scale}" : $"Size: {box.Size}"; + } + } + } + + internal class InfofulBoxAutoSize : Container + { + protected override Container Content => content; + + private readonly Container content; + + public InfofulBoxAutoSize() + { + AutoSizeAxes = Axes.Both; + + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }, + content = new Container + { + AutoSizeAxes = Axes.Both, + } + }; + } + + public bool AllowDrag = true; + + protected override bool OnDrag(InputState state) + { + if (!AllowDrag) return false; + + Position += state.Mouse.Delta; + return true; + } + + protected override bool OnDragEnd(InputState state) + { + return true; + } + + protected override bool OnDragStart(InputState state) => AllowDrag; + } + + internal class InfofulBox : Container + { + public bool Chameleon = false; + public bool AllowDrag = true; + + protected override bool OnDrag(InputState state) + { + if (!AllowDrag) return false; + + Position += state.Mouse.Delta; + return true; + } + + protected override bool OnDragEnd(InputState state) + { + return true; + } + + protected override bool OnDragStart(InputState state) => AllowDrag; + + public InfofulBox() + { + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }); + } + + private int lastSwitch; + + protected override void Update() + { + if (Chameleon && (int)Time.Current / 1000 != lastSwitch) + { + lastSwitch = (int)Time.Current / 1000; + switch (lastSwitch % 6) + { + case 0: + Anchor = (Anchor)((int)Anchor + 1); + Origin = (Anchor)((int)Origin + 1); + break; + case 1: + this.MoveTo(new Vector2(0, 0), 800, Easing.Out); + break; + case 2: + this.MoveTo(new Vector2(200, 0), 800, Easing.Out); + break; + case 3: + this.MoveTo(new Vector2(200, 200), 800, Easing.Out); + break; + case 4: + this.MoveTo(new Vector2(0, 200), 800, Easing.Out); + break; + case 5: + this.MoveTo(new Vector2(0, 0), 800, Easing.Out); + break; + } + } + + base.Update(); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseSliderbar.cs b/osu.Framework.Tests/Visual/TestCaseSliderbar.cs index 50e6d7da3..51e81b4b7 100644 --- a/osu.Framework.Tests/Visual/TestCaseSliderbar.cs +++ b/osu.Framework.Tests/Visual/TestCaseSliderbar.cs @@ -1,68 +1,68 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseSliderbar : TestCase - { - // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable - private readonly BindableDouble sliderBarValue; //keep a reference to avoid GC of the bindable - private readonly SpriteText sliderbarText; - - public TestCaseSliderbar() - { - sliderBarValue = new BindableDouble(8) - { - MinValue = -10, - MaxValue = 10 - }; - sliderBarValue.ValueChanged += sliderBarValueChanged; - - sliderbarText = new SpriteText - { - Text = $"Selected value: {sliderBarValue.Value}", - Position = new Vector2(25, 0) - }; - - SliderBar sliderBar = new BasicSliderBar - { - Size = new Vector2(200, 10), - Position = new Vector2(25, 25), - Color = Color4.White, - SelectionColor = Color4.Pink, - KeyboardStep = 1 - }; - - sliderBar.Current.BindTo(sliderBarValue); - - Add(sliderBar); - Add(sliderbarText); - - Add(sliderBar = new BasicSliderBar - { - Size = new Vector2(200, 10), - RangePadding = 20, - Position = new Vector2(25, 45), - Color = Color4.White, - SelectionColor = Color4.Pink, - KeyboardStep = 1, - }); - - sliderBar.Current.BindTo(sliderBarValue); - - AddSliderStep("Value", -10.0, 10.0, 0.0, v => sliderBarValue.Value = v); - } - - private void sliderBarValueChanged(double newValue) - { - sliderbarText.Text = $"Selected value: {newValue:N}"; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseSliderbar : TestCase + { + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly BindableDouble sliderBarValue; //keep a reference to avoid GC of the bindable + private readonly SpriteText sliderbarText; + + public TestCaseSliderbar() + { + sliderBarValue = new BindableDouble(8) + { + MinValue = -10, + MaxValue = 10 + }; + sliderBarValue.ValueChanged += sliderBarValueChanged; + + sliderbarText = new SpriteText + { + Text = $"Selected value: {sliderBarValue.Value}", + Position = new Vector2(25, 0) + }; + + SliderBar sliderBar = new BasicSliderBar + { + Size = new Vector2(200, 10), + Position = new Vector2(25, 25), + Color = Color4.White, + SelectionColor = Color4.Pink, + KeyboardStep = 1 + }; + + sliderBar.Current.BindTo(sliderBarValue); + + Add(sliderBar); + Add(sliderbarText); + + Add(sliderBar = new BasicSliderBar + { + Size = new Vector2(200, 10), + RangePadding = 20, + Position = new Vector2(25, 45), + Color = Color4.White, + SelectionColor = Color4.Pink, + KeyboardStep = 1, + }); + + sliderBar.Current.BindTo(sliderBarValue); + + AddSliderStep("Value", -10.0, 10.0, 0.0, v => sliderBarValue.Value = v); + } + + private void sliderBarValueChanged(double newValue) + { + sliderbarText.Text = $"Selected value: {newValue:N}"; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseSmoothedEdges.cs b/osu.Framework.Tests/Visual/TestCaseSmoothedEdges.cs index 05eede777..a013cfcbe 100644 --- a/osu.Framework.Tests/Visual/TestCaseSmoothedEdges.cs +++ b/osu.Framework.Tests/Visual/TestCaseSmoothedEdges.cs @@ -1,57 +1,57 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseSmoothedEdges : GridTestCase - { - public TestCaseSmoothedEdges() : base(2, 2) - { - Vector2[] smoothnesses = - { - new Vector2(0, 0), - new Vector2(0, 2), - new Vector2(1, 1), - new Vector2(2, 2), - }; - - for (int i = 0; i < Rows * Cols; ++i) - { - Cell(i).AddRange(new Drawable[] - { - new SpriteText - { - Text = $"{nameof(Sprite.EdgeSmoothness)}={smoothnesses[i]}", - TextSize = 20, - }, - boxes[i] = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.5f), - EdgeSmoothness = smoothnesses[i], - }, - }); - } - } - - private readonly Box[] boxes = new Box[4]; - - protected override void Update() - { - base.Update(); - - foreach (Box box in boxes) - box.Rotation += 0.01f; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseSmoothedEdges : GridTestCase + { + public TestCaseSmoothedEdges() : base(2, 2) + { + Vector2[] smoothnesses = + { + new Vector2(0, 0), + new Vector2(0, 2), + new Vector2(1, 1), + new Vector2(2, 2), + }; + + for (int i = 0; i < Rows * Cols; ++i) + { + Cell(i).AddRange(new Drawable[] + { + new SpriteText + { + Text = $"{nameof(Sprite.EdgeSmoothness)}={smoothnesses[i]}", + TextSize = 20, + }, + boxes[i] = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f), + EdgeSmoothness = smoothnesses[i], + }, + }); + } + } + + private readonly Box[] boxes = new Box[4]; + + protected override void Update() + { + base.Update(); + + foreach (Box box in boxes) + box.Rotation += 0.01f; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseSpriteText.cs b/osu.Framework.Tests/Visual/TestCaseSpriteText.cs index 7701b8a9e..b56ef7beb 100644 --- a/osu.Framework.Tests/Visual/TestCaseSpriteText.cs +++ b/osu.Framework.Tests/Visual/TestCaseSpriteText.cs @@ -1,63 +1,63 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseSpriteText : TestCase - { - public TestCaseSpriteText() - { - FillFlowContainer flow; - - Children = new Drawable[] - { - new ScrollContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - flow = new FillFlowContainer - { - Anchor = Anchor.TopLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - } - }; - - flow.Add(new SpriteText - { - Text = @"the quick red fox jumps over the lazy brown dog" - }); - flow.Add(new SpriteText - { - Text = @"THE QUICK RED FOX JUMPS OVER THE LAZY BROWN DOG" - }); - flow.Add(new SpriteText - { - Text = @"0123456789!@#$%^&*()_-+-[]{}.,<>;'\" - }); - - for (int i = 1; i <= 200; i++) - { - SpriteText text = new SpriteText - { - Text = $@"Font testy at size {i}", - AllowMultiline = true, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - TextSize = i - }; - - flow.Add(text); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseSpriteText : TestCase + { + public TestCaseSpriteText() + { + FillFlowContainer flow; + + Children = new Drawable[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + flow = new FillFlowContainer + { + Anchor = Anchor.TopLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + } + } + } + }; + + flow.Add(new SpriteText + { + Text = @"the quick red fox jumps over the lazy brown dog" + }); + flow.Add(new SpriteText + { + Text = @"THE QUICK RED FOX JUMPS OVER THE LAZY BROWN DOG" + }); + flow.Add(new SpriteText + { + Text = @"0123456789!@#$%^&*()_-+-[]{}.,<>;'\" + }); + + for (int i = 1; i <= 200; i++) + { + SpriteText text = new SpriteText + { + Text = $@"Font testy at size {i}", + AllowMultiline = true, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + TextSize = i + }; + + flow.Add(text); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTabControl.cs b/osu.Framework.Tests/Visual/TestCaseTabControl.cs index c41bf7fd4..eb00608d4 100644 --- a/osu.Framework.Tests/Visual/TestCaseTabControl.cs +++ b/osu.Framework.Tests/Visual/TestCaseTabControl.cs @@ -1,179 +1,179 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTabControl : TestCase - { - public TestCaseTabControl() - { - List> items = new List>(); - foreach (var val in (TestEnum[])Enum.GetValues(typeof(TestEnum))) - items.Add(new KeyValuePair(val.GetDescription(), val)); - - StyledTabControl simpleTabcontrol = new StyledTabControl - { - Position = new Vector2(200, 50), - Size = new Vector2(200, 30), - }; - items.AsEnumerable().ForEach(item => simpleTabcontrol.AddItem(item.Value)); - - StyledTabControl pinnedAndAutoSort = new StyledTabControl - { - Position = new Vector2(500, 50), - Size = new Vector2(200, 30), - AutoSort = true - }; - items.GetRange(0, 7).AsEnumerable().ForEach(item => pinnedAndAutoSort.AddItem(item.Value)); - pinnedAndAutoSort.PinItem(TestEnum.Test5); - - Add(simpleTabcontrol); - Add(pinnedAndAutoSort); - - var nextTest = new Func(() => items.AsEnumerable() - .Select(item => item.Value) - .FirstOrDefault(test => !pinnedAndAutoSort.Items.Contains(test))); - - Stack pinned = new Stack(); - - AddStep("AddItem", () => - { - var item = nextTest.Invoke(); - if (!pinnedAndAutoSort.Items.Contains(item)) - pinnedAndAutoSort.AddItem(item); - }); - - AddStep("RemoveItem", () => - { - if (pinnedAndAutoSort.Items.Any()) - { - pinnedAndAutoSort.RemoveItem(pinnedAndAutoSort.Items.First()); - } - }); - - AddStep("PinItem", () => - { - var item = nextTest.Invoke(); - - if (!pinnedAndAutoSort.Items.Contains(item)) - { - pinned.Push(item); - pinnedAndAutoSort.AddItem(item); - pinnedAndAutoSort.PinItem(item); - } - }); - - AddStep("UnpinItem", () => - { - if (pinned.Count > 0) pinnedAndAutoSort.UnpinItem(pinned.Pop()); - }); - } - - private class StyledTabControl : TabControl - { - protected override Dropdown CreateDropdown() => new StyledDropdown(); - - protected override TabItem CreateTabItem(TestEnum value) => new StyledTabItem(value); - } - - private class StyledTabItem : TabItem - { - private readonly SpriteText text; - - public override bool IsRemovable => true; - - public StyledTabItem(TestEnum value) : base(value) - { - AutoSizeAxes = Axes.Both; - Children = new Drawable[] - { - text = new SpriteText - { - Margin = new MarginPadding(2), - Text = value.ToString(), - TextSize = 18 - } - }; - } - - protected override void OnActivated() => text.Colour = Color4.MediumPurple; - - protected override void OnDeactivated() => text.Colour = Color4.White; - } - - private class StyledDropdown : Dropdown - { - protected override DropdownMenu CreateMenu() => new StyledDropdownMenu(); - - protected override DropdownHeader CreateHeader() => new StyledDropdownHeader(); - - public StyledDropdown() - { - Menu.Anchor = Anchor.TopRight; - Menu.Origin = Anchor.TopRight; - Header.Anchor = Anchor.TopRight; - Header.Origin = Anchor.TopRight; - } - - private class StyledDropdownMenu : DropdownMenu - { - public StyledDropdownMenu() - { - ScrollbarVisible = false; - CornerRadius = 4; - } - } - } - - private class StyledDropdownHeader : DropdownHeader - { - protected internal override string Label { get; set; } - - public StyledDropdownHeader() - { - Background.Hide(); // don't need a background - - RelativeSizeAxes = Axes.None; - AutoSizeAxes = Axes.X; - - Foreground.RelativeSizeAxes = Axes.None; - Foreground.AutoSizeAxes = Axes.Both; - - Foreground.Children = new[] - { - new Box { Width = 20, Height = 20 } - }; - } - } - - private enum TestEnum - { - Test0, - Test1, - Test2, - Test3, - Test4, - Test5, - Test6, - Test7, - Test8, - Test9, - Test10, - Test11, - Test12 - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTabControl : TestCase + { + public TestCaseTabControl() + { + List> items = new List>(); + foreach (var val in (TestEnum[])Enum.GetValues(typeof(TestEnum))) + items.Add(new KeyValuePair(val.GetDescription(), val)); + + StyledTabControl simpleTabcontrol = new StyledTabControl + { + Position = new Vector2(200, 50), + Size = new Vector2(200, 30), + }; + items.AsEnumerable().ForEach(item => simpleTabcontrol.AddItem(item.Value)); + + StyledTabControl pinnedAndAutoSort = new StyledTabControl + { + Position = new Vector2(500, 50), + Size = new Vector2(200, 30), + AutoSort = true + }; + items.GetRange(0, 7).AsEnumerable().ForEach(item => pinnedAndAutoSort.AddItem(item.Value)); + pinnedAndAutoSort.PinItem(TestEnum.Test5); + + Add(simpleTabcontrol); + Add(pinnedAndAutoSort); + + var nextTest = new Func(() => items.AsEnumerable() + .Select(item => item.Value) + .FirstOrDefault(test => !pinnedAndAutoSort.Items.Contains(test))); + + Stack pinned = new Stack(); + + AddStep("AddItem", () => + { + var item = nextTest.Invoke(); + if (!pinnedAndAutoSort.Items.Contains(item)) + pinnedAndAutoSort.AddItem(item); + }); + + AddStep("RemoveItem", () => + { + if (pinnedAndAutoSort.Items.Any()) + { + pinnedAndAutoSort.RemoveItem(pinnedAndAutoSort.Items.First()); + } + }); + + AddStep("PinItem", () => + { + var item = nextTest.Invoke(); + + if (!pinnedAndAutoSort.Items.Contains(item)) + { + pinned.Push(item); + pinnedAndAutoSort.AddItem(item); + pinnedAndAutoSort.PinItem(item); + } + }); + + AddStep("UnpinItem", () => + { + if (pinned.Count > 0) pinnedAndAutoSort.UnpinItem(pinned.Pop()); + }); + } + + private class StyledTabControl : TabControl + { + protected override Dropdown CreateDropdown() => new StyledDropdown(); + + protected override TabItem CreateTabItem(TestEnum value) => new StyledTabItem(value); + } + + private class StyledTabItem : TabItem + { + private readonly SpriteText text; + + public override bool IsRemovable => true; + + public StyledTabItem(TestEnum value) : base(value) + { + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + text = new SpriteText + { + Margin = new MarginPadding(2), + Text = value.ToString(), + TextSize = 18 + } + }; + } + + protected override void OnActivated() => text.Colour = Color4.MediumPurple; + + protected override void OnDeactivated() => text.Colour = Color4.White; + } + + private class StyledDropdown : Dropdown + { + protected override DropdownMenu CreateMenu() => new StyledDropdownMenu(); + + protected override DropdownHeader CreateHeader() => new StyledDropdownHeader(); + + public StyledDropdown() + { + Menu.Anchor = Anchor.TopRight; + Menu.Origin = Anchor.TopRight; + Header.Anchor = Anchor.TopRight; + Header.Origin = Anchor.TopRight; + } + + private class StyledDropdownMenu : DropdownMenu + { + public StyledDropdownMenu() + { + ScrollbarVisible = false; + CornerRadius = 4; + } + } + } + + private class StyledDropdownHeader : DropdownHeader + { + protected internal override string Label { get; set; } + + public StyledDropdownHeader() + { + Background.Hide(); // don't need a background + + RelativeSizeAxes = Axes.None; + AutoSizeAxes = Axes.X; + + Foreground.RelativeSizeAxes = Axes.None; + Foreground.AutoSizeAxes = Axes.Both; + + Foreground.Children = new[] + { + new Box { Width = 20, Height = 20 } + }; + } + } + + private enum TestEnum + { + Test0, + Test1, + Test2, + Test3, + Test4, + Test5, + Test6, + Test7, + Test8, + Test9, + Test10, + Test11, + Test12 + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTextBox.cs b/osu.Framework.Tests/Visual/TestCaseTextBox.cs index 1f87224a8..2e25f90c5 100644 --- a/osu.Framework.Tests/Visual/TestCaseTextBox.cs +++ b/osu.Framework.Tests/Visual/TestCaseTextBox.cs @@ -1,143 +1,143 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTextBox : TestCase - { - public TestCaseTextBox() - { - FillFlowContainer textBoxes = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 50), - Padding = new MarginPadding - { - Top = 50, - }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.9f, 1) - }; - - Add(textBoxes); - - textBoxes.Add(new TextBox - { - Size = new Vector2(100, 16), - TabbableContentContainer = textBoxes - }); - - textBoxes.Add(new TextBox - { - Text = @"Limited length", - Size = new Vector2(200, 20), - LengthLimit = 20, - TabbableContentContainer = textBoxes - }); - - textBoxes.Add(new TextBox - { - Text = @"Box with some more text", - Size = new Vector2(500, 30), - TabbableContentContainer = textBoxes - }); - - textBoxes.Add(new TextBox - { - PlaceholderText = @"Placeholder text", - Size = new Vector2(500, 30), - TabbableContentContainer = textBoxes - }); - - textBoxes.Add(new TextBox - { - Text = @"prefilled placeholder", - PlaceholderText = @"Placeholder text", - Size = new Vector2(500, 30), - TabbableContentContainer = textBoxes - }); - - textBoxes.Add(new TextBox - { - Text = "Readonly textbox", - Size = new Vector2(500, 30), - ReadOnly = true, - TabbableContentContainer = textBoxes - }); - - FillFlowContainer otherTextBoxes = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 50), - Padding = new MarginPadding - { - Top = 50, - Left = 500 - }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f, 1) - }; - - otherTextBoxes.Add(new TextBox - { - PlaceholderText = @"Textbox in separate container", - Size = new Vector2(500, 30), - TabbableContentContainer = otherTextBoxes - }); - - otherTextBoxes.Add(new PasswordTextBox - { - PlaceholderText = @"Password textbox", - Text = "Secret ;)", - Size = new Vector2(500, 30), - TabbableContentContainer = otherTextBoxes - }); - - FillFlowContainer nestedTextBoxes = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 50), - Margin = new MarginPadding { Left = 50 }, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f, 1) - }; - - nestedTextBoxes.Add(new TextBox - { - PlaceholderText = @"Nested textbox 1", - Size = new Vector2(457, 30), - TabbableContentContainer = otherTextBoxes - }); - - nestedTextBoxes.Add(new TextBox - { - PlaceholderText = @"Nested textbox 2", - Size = new Vector2(457, 30), - TabbableContentContainer = otherTextBoxes - }); - - nestedTextBoxes.Add(new TextBox - { - PlaceholderText = @"Nested textbox 3", - Size = new Vector2(457, 30), - TabbableContentContainer = otherTextBoxes - }); - - otherTextBoxes.Add(nestedTextBoxes); - - Add(otherTextBoxes); - - //textBoxes.Add(tb = new PasswordTextBox(@"", 14, Vector2.Zero, 300)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTextBox : TestCase + { + public TestCaseTextBox() + { + FillFlowContainer textBoxes = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 50), + Padding = new MarginPadding + { + Top = 50, + }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.9f, 1) + }; + + Add(textBoxes); + + textBoxes.Add(new TextBox + { + Size = new Vector2(100, 16), + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new TextBox + { + Text = @"Limited length", + Size = new Vector2(200, 20), + LengthLimit = 20, + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new TextBox + { + Text = @"Box with some more text", + Size = new Vector2(500, 30), + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new TextBox + { + PlaceholderText = @"Placeholder text", + Size = new Vector2(500, 30), + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new TextBox + { + Text = @"prefilled placeholder", + PlaceholderText = @"Placeholder text", + Size = new Vector2(500, 30), + TabbableContentContainer = textBoxes + }); + + textBoxes.Add(new TextBox + { + Text = "Readonly textbox", + Size = new Vector2(500, 30), + ReadOnly = true, + TabbableContentContainer = textBoxes + }); + + FillFlowContainer otherTextBoxes = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 50), + Padding = new MarginPadding + { + Top = 50, + Left = 500 + }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f, 1) + }; + + otherTextBoxes.Add(new TextBox + { + PlaceholderText = @"Textbox in separate container", + Size = new Vector2(500, 30), + TabbableContentContainer = otherTextBoxes + }); + + otherTextBoxes.Add(new PasswordTextBox + { + PlaceholderText = @"Password textbox", + Text = "Secret ;)", + Size = new Vector2(500, 30), + TabbableContentContainer = otherTextBoxes + }); + + FillFlowContainer nestedTextBoxes = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 50), + Margin = new MarginPadding { Left = 50 }, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f, 1) + }; + + nestedTextBoxes.Add(new TextBox + { + PlaceholderText = @"Nested textbox 1", + Size = new Vector2(457, 30), + TabbableContentContainer = otherTextBoxes + }); + + nestedTextBoxes.Add(new TextBox + { + PlaceholderText = @"Nested textbox 2", + Size = new Vector2(457, 30), + TabbableContentContainer = otherTextBoxes + }); + + nestedTextBoxes.Add(new TextBox + { + PlaceholderText = @"Nested textbox 3", + Size = new Vector2(457, 30), + TabbableContentContainer = otherTextBoxes + }); + + otherTextBoxes.Add(nestedTextBoxes); + + Add(otherTextBoxes); + + //textBoxes.Add(tb = new PasswordTextBox(@"", 14, Vector2.Zero, 300)); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTextFlow.cs b/osu.Framework.Tests/Visual/TestCaseTextFlow.cs index 5c56d34db..3bd1cc0cf 100644 --- a/osu.Framework.Tests/Visual/TestCaseTextFlow.cs +++ b/osu.Framework.Tests/Visual/TestCaseTextFlow.cs @@ -1,212 +1,212 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - [System.ComponentModel.Description("word-wrap and paragraphs")] - public class TestCaseTextFlow : TestCase - { - public TestCaseTextFlow() - { - FillFlowContainer flow; - - Children = new Drawable[] - { - new ScrollContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - flow = new FillFlowContainer - { - Anchor = Anchor.TopLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - } - }; - - FillFlowContainer paragraphContainer; - TextFlowContainer textFlowContainer; - flow.Add(paragraphContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0.5f, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - textFlowContainer = new TextFlowContainer - { - FirstLineIndent = 5, - ContentIndent = 10, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - } - }); - - textFlowContainer.AddText("the considerably swift vermilion reynard bounds above the slothful mahogany hound.", t => t.Colour = Color4.Yellow); - textFlowContainer.AddText("\nTHE ", t => t.Colour = Color4.Red); - textFlowContainer.AddText("CONSIDERABLY", t => t.Colour = Color4.Pink); - textFlowContainer.AddText(" SWIFT VERMILION REYNARD BOUNDS ABOVE THE SLOTHFUL MAHOGANY HOUND!!", t => t.Colour = Color4.Red); - textFlowContainer.AddText("\n\n0123456789!@#$%^&*()_-+-[]{}.,<>;'\\\\", t => t.Colour = Color4.Blue); - var textSize = 48f; - textFlowContainer.AddParagraph("Multiple Text Sizes", t => - { - t.TextSize = textSize; - textSize -= 12f; - }); - textFlowContainer.AddText("\nI'm a paragraph\nnewlines are cool", t => t.Colour = Color4.Beige); - textFlowContainer.AddText(" (and so are inline styles!)", t => t.Colour = Color4.Yellow); - textFlowContainer.AddParagraph("There's 2 line breaks\n\ninside this paragraph!", t => t.Colour = Color4.GreenYellow); - textFlowContainer.AddParagraph("Make\nTextFlowContainer\ngreat\nagain!", t => t.Colour = Color4.Red); - - paragraphContainer.Add(new TextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = -@"osu! is a freeware rhythm game developed by Dean ""peppy"" Herbert, originally for Microsoft Windows. The game has also been ported to macOS, iOS, Android, and Windows Phone.[1] Its game play is based on commercial titles including Osu! Tatakae! Ouendan, Elite Beat Agents, Taiko no Tatsujin, beatmania IIDX, O2Jam, and DJMax. - -osu! is written in C# on the .NET Framework. On August 28, 2016, osu!'s source code was open-sourced under the MIT License. [2] [3] Dubbed as ""Lazer"", the project aims to make osu! available to more platforms and transparent. [4] The community includes over 9 million registered users, with a total of 6 billion ranked plays.[5]" - }); - - paragraphContainer.Add(new TestCaseCustomText - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Placeholders = new Drawable[] - { - new LineBaseBox - { - Colour = Color4.Purple, - LineBaseHeight = 25f, - Size = new Vector2(25, 25) - }.WithEffect(new OutlineEffect - { - Strength = 20f, - PadExtent = true, - BlurSigma = new Vector2(5f), - Colour = Color4.White - }) - }, - Text = "Test icons [RedBox] interleaved\n[GreenBox] with other [0] text, also [[0]] escaping stuff is possible." - }); - - paragraphContainer.Add(new Container - { - Size = new Vector2(300), - Children = new Drawable[] - { - new Box - { - Name = "Background", - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.TopLeft, - Text = "TopLeft" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.TopCentre, - Text = "TopCentre" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.TopRight, - Text = "TopRight" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.BottomLeft, - Text = "BottomLeft" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.BottomCentre, - Text = "BottomCentre" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.BottomRight, - Text = "BottomRight" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.CentreLeft, - Text = "CentreLeft" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.Centre, - Text = "Centre" - }, - new TextFlowContainer - { - RelativeSizeAxes = Axes.Both, - TextAnchor = Anchor.CentreRight, - Text = "CentreRight" - } - } - }); - - AddStep(@"resize paragraph 1", () => { paragraphContainer.Width = 1f; }); - AddStep(@"resize paragraph 2", () => { paragraphContainer.Width = 0.6f; }); - AddStep(@"header inset", () => { textFlowContainer.FirstLineIndent += 2; }); - AddStep(@"body inset", () => { textFlowContainer.ContentIndent += 4; }); - AddToggleStep(@"Zero paragraph spacing", state => textFlowContainer.ParagraphSpacing = state ? 0 : 0.5f); - AddToggleStep(@"Non-zero line spacing", state => textFlowContainer.LineSpacing = state ? 1 : 0); - } - - private class LineBaseBox : Box, IHasLineBaseHeight - { - public float LineBaseHeight { get; set; } - } - - private class TestCaseCustomText : CustomizableTextContainer - { - public TestCaseCustomText() - { - AddIconFactory("RedBox", makeRedBox); - AddIconFactory("GreenBox", makeGreenBox); - } - - private Drawable makeGreenBox() => new LineBaseBox - { - Colour = Color4.Green, - LineBaseHeight = 25f, - Size = new Vector2(25, 20) - }; - - private Drawable makeRedBox() => new LineBaseBox - { - Colour = Color4.Red, - LineBaseHeight = 10f, - Size = new Vector2(25, 25) - }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + [System.ComponentModel.Description("word-wrap and paragraphs")] + public class TestCaseTextFlow : TestCase + { + public TestCaseTextFlow() + { + FillFlowContainer flow; + + Children = new Drawable[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + flow = new FillFlowContainer + { + Anchor = Anchor.TopLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + } + } + } + }; + + FillFlowContainer paragraphContainer; + TextFlowContainer textFlowContainer; + flow.Add(paragraphContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textFlowContainer = new TextFlowContainer + { + FirstLineIndent = 5, + ContentIndent = 10, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }); + + textFlowContainer.AddText("the considerably swift vermilion reynard bounds above the slothful mahogany hound.", t => t.Colour = Color4.Yellow); + textFlowContainer.AddText("\nTHE ", t => t.Colour = Color4.Red); + textFlowContainer.AddText("CONSIDERABLY", t => t.Colour = Color4.Pink); + textFlowContainer.AddText(" SWIFT VERMILION REYNARD BOUNDS ABOVE THE SLOTHFUL MAHOGANY HOUND!!", t => t.Colour = Color4.Red); + textFlowContainer.AddText("\n\n0123456789!@#$%^&*()_-+-[]{}.,<>;'\\\\", t => t.Colour = Color4.Blue); + var textSize = 48f; + textFlowContainer.AddParagraph("Multiple Text Sizes", t => + { + t.TextSize = textSize; + textSize -= 12f; + }); + textFlowContainer.AddText("\nI'm a paragraph\nnewlines are cool", t => t.Colour = Color4.Beige); + textFlowContainer.AddText(" (and so are inline styles!)", t => t.Colour = Color4.Yellow); + textFlowContainer.AddParagraph("There's 2 line breaks\n\ninside this paragraph!", t => t.Colour = Color4.GreenYellow); + textFlowContainer.AddParagraph("Make\nTextFlowContainer\ngreat\nagain!", t => t.Colour = Color4.Red); + + paragraphContainer.Add(new TextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = +@"osu! is a freeware rhythm game developed by Dean ""peppy"" Herbert, originally for Microsoft Windows. The game has also been ported to macOS, iOS, Android, and Windows Phone.[1] Its game play is based on commercial titles including Osu! Tatakae! Ouendan, Elite Beat Agents, Taiko no Tatsujin, beatmania IIDX, O2Jam, and DJMax. + +osu! is written in C# on the .NET Framework. On August 28, 2016, osu!'s source code was open-sourced under the MIT License. [2] [3] Dubbed as ""Lazer"", the project aims to make osu! available to more platforms and transparent. [4] The community includes over 9 million registered users, with a total of 6 billion ranked plays.[5]" + }); + + paragraphContainer.Add(new TestCaseCustomText + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Placeholders = new Drawable[] + { + new LineBaseBox + { + Colour = Color4.Purple, + LineBaseHeight = 25f, + Size = new Vector2(25, 25) + }.WithEffect(new OutlineEffect + { + Strength = 20f, + PadExtent = true, + BlurSigma = new Vector2(5f), + Colour = Color4.White + }) + }, + Text = "Test icons [RedBox] interleaved\n[GreenBox] with other [0] text, also [[0]] escaping stuff is possible." + }); + + paragraphContainer.Add(new Container + { + Size = new Vector2(300), + Children = new Drawable[] + { + new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.TopLeft, + Text = "TopLeft" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.TopCentre, + Text = "TopCentre" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.TopRight, + Text = "TopRight" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.BottomLeft, + Text = "BottomLeft" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.BottomCentre, + Text = "BottomCentre" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.BottomRight, + Text = "BottomRight" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.CentreLeft, + Text = "CentreLeft" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.Centre, + Text = "Centre" + }, + new TextFlowContainer + { + RelativeSizeAxes = Axes.Both, + TextAnchor = Anchor.CentreRight, + Text = "CentreRight" + } + } + }); + + AddStep(@"resize paragraph 1", () => { paragraphContainer.Width = 1f; }); + AddStep(@"resize paragraph 2", () => { paragraphContainer.Width = 0.6f; }); + AddStep(@"header inset", () => { textFlowContainer.FirstLineIndent += 2; }); + AddStep(@"body inset", () => { textFlowContainer.ContentIndent += 4; }); + AddToggleStep(@"Zero paragraph spacing", state => textFlowContainer.ParagraphSpacing = state ? 0 : 0.5f); + AddToggleStep(@"Non-zero line spacing", state => textFlowContainer.LineSpacing = state ? 1 : 0); + } + + private class LineBaseBox : Box, IHasLineBaseHeight + { + public float LineBaseHeight { get; set; } + } + + private class TestCaseCustomText : CustomizableTextContainer + { + public TestCaseCustomText() + { + AddIconFactory("RedBox", makeRedBox); + AddIconFactory("GreenBox", makeGreenBox); + } + + private Drawable makeGreenBox() => new LineBaseBox + { + Colour = Color4.Green, + LineBaseHeight = 25f, + Size = new Vector2(25, 20) + }; + + private Drawable makeRedBox() => new LineBaseBox + { + Colour = Color4.Red, + LineBaseHeight = 10f, + Size = new Vector2(25, 25) + }; + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTooltip.cs b/osu.Framework.Tests/Visual/TestCaseTooltip.cs index 13340f722..ca67cd374 100644 --- a/osu.Framework.Tests/Visual/TestCaseTooltip.cs +++ b/osu.Framework.Tests/Visual/TestCaseTooltip.cs @@ -1,212 +1,212 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTooltip : TestCase - { - private readonly Container testContainer; - - public TestCaseTooltip() - { - Add(testContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - AddToggleStep("Cursor-less tooltip", generateTest); - generateTest(false); - } - - private TooltipBox makeBox(Anchor anchor) - { - return new TooltipBox - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.2f), - Anchor = anchor, - Origin = anchor, - Colour = Color4.Blue, - TooltipText = $"{anchor}", - }; - } - - private void generateTest(bool cursorlessTooltip) - { - testContainer.Clear(); - - CursorContainer cursor = null; - if (!cursorlessTooltip) - { - cursor = new RectangleCursorContainer(); - testContainer.Add(cursor); - } - - TooltipContainer ttc; - testContainer.Add(ttc = new TooltipContainer(cursor) - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new[] - { - new TooltipBox - { - TooltipText = "Outer Tooltip", - Colour = Color4.CornflowerBlue, - Size = new Vector2(300, 300), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new TooltipBox - { - TooltipText = "Inner Tooltip", - Size = new Vector2(150, 150), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - } - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new TooltipSpriteText("this text has a tooltip!"), - new TooltipSpriteText("this one too!"), - new CustomTooltipSpriteText("this text has an empty tooltip!", string.Empty), - new CustomTooltipSpriteText("this text has a nulled tooltip!", null), - new TooltipTextbox - { - Text = "with real time updates!", - Size = new Vector2(400, 30), - }, - new TooltipContainer - { - AutoSizeAxes = Axes.Both, - Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases!"), - }, - new TooltipTooltipContainer("This tooltip container has a tooltip itself!") - { - AutoSizeAxes = Axes.Both, - Child = new Container - { - AutoSizeAxes = Axes.Both, - Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases; parent TooltipContainer has a tooltip"), - } - }, - new Container - { - Child = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 8), - Children = new[] - { - new Container - { - Child = new Container - { - Child = new TooltipSpriteText("Tooltip within containers with zero size; i.e. parent is never hovered."), - } - }, - new Container - { - Child = new TooltipSpriteText("Other tooltip within containers with zero size; different nesting; overlap."), - } - } - } - } - }, - } - } - }); - - ttc.Add(makeBox(Anchor.BottomLeft)); - ttc.Add(makeBox(Anchor.TopRight)); - ttc.Add(makeBox(Anchor.BottomRight)); - } - - private class CustomTooltipSpriteText : Container, IHasTooltip - { - private readonly string tooltipText; - - public string TooltipText => tooltipText; - - public CustomTooltipSpriteText(string displayedText, string tooltipText) - { - this.tooltipText = tooltipText; - - AutoSizeAxes = Axes.Both; - Children = new[] - { - new SpriteText - { - Text = displayedText, - } - }; - } - } - - private class TooltipSpriteText : CustomTooltipSpriteText - { - public TooltipSpriteText(string tooltipText) - : base(tooltipText, tooltipText) - { - } - } - - private class TooltipTooltipContainer : TooltipContainer, IHasTooltip - { - public string TooltipText { get; set; } - - public TooltipTooltipContainer(string tooltipText) - { - TooltipText = tooltipText; - } - } - - private class TooltipTextbox : TextBox, IHasTooltip - { - public string TooltipText => Text; - } - - private class TooltipBox : Box, IHasTooltip - { - public string TooltipText { get; set; } - - public override bool HandleKeyboardInput => true; - public override bool HandleMouseInput => true; - } - - private class RectangleCursorContainer : CursorContainer - { - protected override Drawable CreateCursor() => new RectangleCursor(); - - private class RectangleCursor : Box - { - public RectangleCursor() - { - Size = new Vector2(20, 40); - } - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTooltip : TestCase + { + private readonly Container testContainer; + + public TestCaseTooltip() + { + Add(testContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + AddToggleStep("Cursor-less tooltip", generateTest); + generateTest(false); + } + + private TooltipBox makeBox(Anchor anchor) + { + return new TooltipBox + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.2f), + Anchor = anchor, + Origin = anchor, + Colour = Color4.Blue, + TooltipText = $"{anchor}", + }; + } + + private void generateTest(bool cursorlessTooltip) + { + testContainer.Clear(); + + CursorContainer cursor = null; + if (!cursorlessTooltip) + { + cursor = new RectangleCursorContainer(); + testContainer.Add(cursor); + } + + TooltipContainer ttc; + testContainer.Add(ttc = new TooltipContainer(cursor) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new[] + { + new TooltipBox + { + TooltipText = "Outer Tooltip", + Colour = Color4.CornflowerBlue, + Size = new Vector2(300, 300), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new TooltipBox + { + TooltipText = "Inner Tooltip", + Size = new Vector2(150, 150), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new TooltipSpriteText("this text has a tooltip!"), + new TooltipSpriteText("this one too!"), + new CustomTooltipSpriteText("this text has an empty tooltip!", string.Empty), + new CustomTooltipSpriteText("this text has a nulled tooltip!", null), + new TooltipTextbox + { + Text = "with real time updates!", + Size = new Vector2(400, 30), + }, + new TooltipContainer + { + AutoSizeAxes = Axes.Both, + Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases!"), + }, + new TooltipTooltipContainer("This tooltip container has a tooltip itself!") + { + AutoSizeAxes = Axes.Both, + Child = new Container + { + AutoSizeAxes = Axes.Both, + Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases; parent TooltipContainer has a tooltip"), + } + }, + new Container + { + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 8), + Children = new[] + { + new Container + { + Child = new Container + { + Child = new TooltipSpriteText("Tooltip within containers with zero size; i.e. parent is never hovered."), + } + }, + new Container + { + Child = new TooltipSpriteText("Other tooltip within containers with zero size; different nesting; overlap."), + } + } + } + } + }, + } + } + }); + + ttc.Add(makeBox(Anchor.BottomLeft)); + ttc.Add(makeBox(Anchor.TopRight)); + ttc.Add(makeBox(Anchor.BottomRight)); + } + + private class CustomTooltipSpriteText : Container, IHasTooltip + { + private readonly string tooltipText; + + public string TooltipText => tooltipText; + + public CustomTooltipSpriteText(string displayedText, string tooltipText) + { + this.tooltipText = tooltipText; + + AutoSizeAxes = Axes.Both; + Children = new[] + { + new SpriteText + { + Text = displayedText, + } + }; + } + } + + private class TooltipSpriteText : CustomTooltipSpriteText + { + public TooltipSpriteText(string tooltipText) + : base(tooltipText, tooltipText) + { + } + } + + private class TooltipTooltipContainer : TooltipContainer, IHasTooltip + { + public string TooltipText { get; set; } + + public TooltipTooltipContainer(string tooltipText) + { + TooltipText = tooltipText; + } + } + + private class TooltipTextbox : TextBox, IHasTooltip + { + public string TooltipText => Text; + } + + private class TooltipBox : Box, IHasTooltip + { + public string TooltipText { get; set; } + + public override bool HandleKeyboardInput => true; + public override bool HandleMouseInput => true; + } + + private class RectangleCursorContainer : CursorContainer + { + protected override Drawable CreateCursor() => new RectangleCursor(); + + private class RectangleCursor : Box + { + public RectangleCursor() + { + Size = new Vector2(20, 40); + } + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTransformRewinding.cs b/osu.Framework.Tests/Visual/TestCaseTransformRewinding.cs index 1d6d44d58..039927889 100644 --- a/osu.Framework.Tests/Visual/TestCaseTransformRewinding.cs +++ b/osu.Framework.Tests/Visual/TestCaseTransformRewinding.cs @@ -1,385 +1,385 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Testing; -using osu.Framework.Timing; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTransformRewinding : TestCase - { - private const double interval = 250; - private const int interval_count = 4; - - private static double intervalAt(int sequence) => interval * sequence; - - protected override void LoadComplete() - { - base.LoadComplete(); - - AddStep("Basic scale", () => boxTest(box => - { - box.Scale = Vector2.One; - box.ScaleTo(0, interval * 4); - })); - - AddStep("Scale sequence", () => boxTest(box => - { - box.Scale = Vector2.One; - - box.ScaleTo(0.75f, interval).Then() - .ScaleTo(0.5f, interval).Then() - .ScaleTo(0.25f, interval).Then() - .ScaleTo(0, interval); - })); - - AddStep("Basic movement", () => boxTest(box => - { - box.Scale = new Vector2(0.25f); - box.Anchor = Anchor.TopLeft; - box.Origin = Anchor.TopLeft; - - box.MoveTo(new Vector2(0.75f, 0), interval).Then() - .MoveTo(new Vector2(0.75f, 0.75f), interval).Then() - .MoveTo(new Vector2(0, 0.75f), interval).Then() - .MoveTo(new Vector2(0), interval); - })); - - AddStep("Move sequence", () => boxTest(box => - { - box.Scale = new Vector2(0.25f); - box.Anchor = Anchor.TopLeft; - box.Origin = Anchor.TopLeft; - - box.ScaleTo(0.5f, interval).MoveTo(new Vector2(0.5f), interval) - .Then() - .ScaleTo(0.1f, interval).MoveTo(new Vector2(0, 0.75f), interval) - .Then() - .ScaleTo(1f, interval).MoveTo(new Vector2(0, 0), interval) - .Then() - .FadeTo(0, interval); - })); - - AddStep("Same type in type", () => boxTest(box => - { - box.ScaleTo(0.5f, interval * 4); - box.Delay(interval * 2).ScaleTo(1, interval); - })); - - AddStep("Same type partial overlap", () => boxTest(box => - { - box.ScaleTo(0.5f, interval * 2); - box.Delay(interval).ScaleTo(1, interval * 2); - })); - - AddStep("Start in middle of sequence", () => boxTest(box => - { - box.Alpha = 0; - box.Delay(interval * 2).FadeInFromZero(interval); - box.ScaleTo(0.9f, interval * 4); - }, 750)); - - AddStep("Loop sequence", () => boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); })); - - AddStep("Start in middle of loop sequence", () => boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); }, 750)); - } - - private Box box; - - private void boxTest(Action action, int startTime = 0) - { - Clear(); - Add(new AnimationContainer(startTime) - { - Child = box = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Scale = new Vector2(0.25f), - }, - ExaminableDrawable = box, - }); - - action(box); - } - - private class AnimationContainer : Container - { - public override bool RemoveCompletedTransforms => false; - - protected override Container Content => content; - private readonly Container content; - - private readonly SpriteText minTimeText; - private readonly SpriteText currentTimeText; - private readonly SpriteText maxTimeText; - - private readonly Tick seekingTick; - private readonly WrappingTimeContainer wrapping; - - public Box ExaminableDrawable; - - private readonly FlowContainer transforms; - - public AnimationContainer(int startTime = 0) - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - RelativeSizeAxes = Axes.Both; - - InternalChild = wrapping = new WrappingTimeContainer(startTime) - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - FillMode = FillMode.Fit, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.6f), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.DarkGray, - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - }, - } - }, - transforms = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Spacing = Vector2.One, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = 0.2f, - }, - new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f, 0.1f), - Children = new Drawable[] - { - minTimeText = new SpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopLeft, - }, - currentTimeText = new SpriteText - { - RelativePositionAxes = Axes.X, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomCentre, - Y = -10, - }, - maxTimeText = new SpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.TopRight, - }, - seekingTick = new Tick(0, false), - new Tick(0), - new Tick(1), - new Tick(2), - new Tick(3), - new Tick(4), - } - } - } - }; - } - - private int displayedTransformsCount; - - protected override void Update() - { - base.Update(); - - double time = wrapping.Time.Current; - - minTimeText.Text = wrapping.MinTime.ToString("n0"); - currentTimeText.Text = time.ToString("n0"); - seekingTick.X = currentTimeText.X = (float)(time / (wrapping.MaxTime - wrapping.MinTime)); - maxTimeText.Text = wrapping.MaxTime.ToString("n0"); - - maxTimeText.Colour = time > wrapping.MaxTime ? Color4.Gray : (wrapping.Time.Elapsed > 0 ? Color4.Blue : Color4.Red); - minTimeText.Colour = time < wrapping.MinTime ? Color4.Gray : (content.Time.Elapsed > 0 ? Color4.Blue : Color4.Red); - - if (ExaminableDrawable.Transforms.Count != displayedTransformsCount) - { - transforms.Clear(); - foreach (var t in ExaminableDrawable.Transforms) - transforms.Add(new DrawableTransform(t)); - displayedTransformsCount = ExaminableDrawable.Transforms.Count; - } - } - - private class DrawableTransform : CompositeDrawable - { - private readonly Transform transform; - private readonly Box applied; - private readonly Box appliedToEnd; - private readonly SpriteText text; - - private const float height = 15; - - public DrawableTransform(Transform transform) - { - this.transform = transform; - - RelativeSizeAxes = Axes.X; - Height = height; - - InternalChildren = new Drawable[] - { - applied = new Box { Size = new Vector2(height) }, - appliedToEnd = new Box { X = height + 2, Size = new Vector2(height) }, - text = new SpriteText { X = (height + 2) * 2, TextSize = height }, - }; - } - - protected override void Update() - { - base.Update(); - - applied.Colour = transform.Applied ? Color4.Green : Color4.Red; - appliedToEnd.Colour = transform.AppliedToEnd ? Color4.Green : Color4.Red; - text.Text = transform.ToString(); - } - } - - private class Tick : Box - { - private readonly int tick; - private readonly bool colouring; - - public Tick(int tick, bool colouring = true) - { - this.tick = tick; - this.colouring = colouring; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomCentre; - - Size = new Vector2(1, 10); - Colour = Color4.White; - - RelativePositionAxes = Axes.X; - X = (float)tick / interval_count; - } - - protected override void Update() - { - base.Update(); - - if (colouring) - Colour = Time.Current > tick * interval ? Color4.Yellow : Color4.White; - } - } - } - - private class WrappingTimeContainer : Container - { - // Padding, in milliseconds, at each end of maxima of the clock time - private const double time_padding = 50; - - public double MinTime => clock.MinTime + time_padding; - public double MaxTime => clock.MaxTime - time_padding; - - private readonly ReversibleClock clock; - - public WrappingTimeContainer(double startTime) - { - clock = new ReversibleClock(startTime); - } - - [BackgroundDependencyLoader] - private void load() - { - // Replace the game clock, but keep it as a reference - clock.SetSource(Clock); - Clock = clock; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - clock.MinTime = -time_padding; - clock.MaxTime = intervalAt(interval_count) + time_padding; - } - - private class ReversibleClock : IFrameBasedClock - { - private readonly double startTime; - public double MinTime; - public double MaxTime = 1000; - - private IFrameBasedClock trackingClock; - - private bool reversed; - - public ReversibleClock(double startTime) - { - this.startTime = startTime; - } - - public void SetSource(IFrameBasedClock trackingClock) - { - this.trackingClock = new FramedOffsetClock(trackingClock) { Offset = -trackingClock.CurrentTime + startTime }; - } - - public double CurrentTime { get; private set; } - - public double Rate => trackingClock.Rate; - - public bool IsRunning => trackingClock.IsRunning; - - public double ElapsedFrameTime => (reversed ? -1 : 1) * trackingClock.ElapsedFrameTime; - - public double AverageFrameTime => trackingClock.AverageFrameTime; - - public double FramesPerSecond => trackingClock.FramesPerSecond; - - public FrameTimeInfo TimeInfo => new FrameTimeInfo { Current = CurrentTime, Elapsed = ElapsedFrameTime }; - - public void ProcessFrame() - { - trackingClock.ProcessFrame(); - - // There are two iterations, when iteration % 2 == 0 : not reversed - int iteration = (int)(trackingClock.CurrentTime / (MaxTime - MinTime)); - reversed = iteration % 2 == 1; - - double iterationTime = trackingClock.CurrentTime % (MaxTime - MinTime); - - if (reversed) - CurrentTime = MaxTime - iterationTime; - else - CurrentTime = MinTime + iterationTime; - } - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Testing; +using osu.Framework.Timing; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTransformRewinding : TestCase + { + private const double interval = 250; + private const int interval_count = 4; + + private static double intervalAt(int sequence) => interval * sequence; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("Basic scale", () => boxTest(box => + { + box.Scale = Vector2.One; + box.ScaleTo(0, interval * 4); + })); + + AddStep("Scale sequence", () => boxTest(box => + { + box.Scale = Vector2.One; + + box.ScaleTo(0.75f, interval).Then() + .ScaleTo(0.5f, interval).Then() + .ScaleTo(0.25f, interval).Then() + .ScaleTo(0, interval); + })); + + AddStep("Basic movement", () => boxTest(box => + { + box.Scale = new Vector2(0.25f); + box.Anchor = Anchor.TopLeft; + box.Origin = Anchor.TopLeft; + + box.MoveTo(new Vector2(0.75f, 0), interval).Then() + .MoveTo(new Vector2(0.75f, 0.75f), interval).Then() + .MoveTo(new Vector2(0, 0.75f), interval).Then() + .MoveTo(new Vector2(0), interval); + })); + + AddStep("Move sequence", () => boxTest(box => + { + box.Scale = new Vector2(0.25f); + box.Anchor = Anchor.TopLeft; + box.Origin = Anchor.TopLeft; + + box.ScaleTo(0.5f, interval).MoveTo(new Vector2(0.5f), interval) + .Then() + .ScaleTo(0.1f, interval).MoveTo(new Vector2(0, 0.75f), interval) + .Then() + .ScaleTo(1f, interval).MoveTo(new Vector2(0, 0), interval) + .Then() + .FadeTo(0, interval); + })); + + AddStep("Same type in type", () => boxTest(box => + { + box.ScaleTo(0.5f, interval * 4); + box.Delay(interval * 2).ScaleTo(1, interval); + })); + + AddStep("Same type partial overlap", () => boxTest(box => + { + box.ScaleTo(0.5f, interval * 2); + box.Delay(interval).ScaleTo(1, interval * 2); + })); + + AddStep("Start in middle of sequence", () => boxTest(box => + { + box.Alpha = 0; + box.Delay(interval * 2).FadeInFromZero(interval); + box.ScaleTo(0.9f, interval * 4); + }, 750)); + + AddStep("Loop sequence", () => boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); })); + + AddStep("Start in middle of loop sequence", () => boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); }, 750)); + } + + private Box box; + + private void boxTest(Action action, int startTime = 0) + { + Clear(); + Add(new AnimationContainer(startTime) + { + Child = box = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Scale = new Vector2(0.25f), + }, + ExaminableDrawable = box, + }); + + action(box); + } + + private class AnimationContainer : Container + { + public override bool RemoveCompletedTransforms => false; + + protected override Container Content => content; + private readonly Container content; + + private readonly SpriteText minTimeText; + private readonly SpriteText currentTimeText; + private readonly SpriteText maxTimeText; + + private readonly Tick seekingTick; + private readonly WrappingTimeContainer wrapping; + + public Box ExaminableDrawable; + + private readonly FlowContainer transforms; + + public AnimationContainer(int startTime = 0) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + + InternalChild = wrapping = new WrappingTimeContainer(startTime) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.6f), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkGray, + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + } + }, + transforms = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = Vector2.One, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.2f, + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f, 0.1f), + Children = new Drawable[] + { + minTimeText = new SpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + }, + currentTimeText = new SpriteText + { + RelativePositionAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + Y = -10, + }, + maxTimeText = new SpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.TopRight, + }, + seekingTick = new Tick(0, false), + new Tick(0), + new Tick(1), + new Tick(2), + new Tick(3), + new Tick(4), + } + } + } + }; + } + + private int displayedTransformsCount; + + protected override void Update() + { + base.Update(); + + double time = wrapping.Time.Current; + + minTimeText.Text = wrapping.MinTime.ToString("n0"); + currentTimeText.Text = time.ToString("n0"); + seekingTick.X = currentTimeText.X = (float)(time / (wrapping.MaxTime - wrapping.MinTime)); + maxTimeText.Text = wrapping.MaxTime.ToString("n0"); + + maxTimeText.Colour = time > wrapping.MaxTime ? Color4.Gray : (wrapping.Time.Elapsed > 0 ? Color4.Blue : Color4.Red); + minTimeText.Colour = time < wrapping.MinTime ? Color4.Gray : (content.Time.Elapsed > 0 ? Color4.Blue : Color4.Red); + + if (ExaminableDrawable.Transforms.Count != displayedTransformsCount) + { + transforms.Clear(); + foreach (var t in ExaminableDrawable.Transforms) + transforms.Add(new DrawableTransform(t)); + displayedTransformsCount = ExaminableDrawable.Transforms.Count; + } + } + + private class DrawableTransform : CompositeDrawable + { + private readonly Transform transform; + private readonly Box applied; + private readonly Box appliedToEnd; + private readonly SpriteText text; + + private const float height = 15; + + public DrawableTransform(Transform transform) + { + this.transform = transform; + + RelativeSizeAxes = Axes.X; + Height = height; + + InternalChildren = new Drawable[] + { + applied = new Box { Size = new Vector2(height) }, + appliedToEnd = new Box { X = height + 2, Size = new Vector2(height) }, + text = new SpriteText { X = (height + 2) * 2, TextSize = height }, + }; + } + + protected override void Update() + { + base.Update(); + + applied.Colour = transform.Applied ? Color4.Green : Color4.Red; + appliedToEnd.Colour = transform.AppliedToEnd ? Color4.Green : Color4.Red; + text.Text = transform.ToString(); + } + } + + private class Tick : Box + { + private readonly int tick; + private readonly bool colouring; + + public Tick(int tick, bool colouring = true) + { + this.tick = tick; + this.colouring = colouring; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomCentre; + + Size = new Vector2(1, 10); + Colour = Color4.White; + + RelativePositionAxes = Axes.X; + X = (float)tick / interval_count; + } + + protected override void Update() + { + base.Update(); + + if (colouring) + Colour = Time.Current > tick * interval ? Color4.Yellow : Color4.White; + } + } + } + + private class WrappingTimeContainer : Container + { + // Padding, in milliseconds, at each end of maxima of the clock time + private const double time_padding = 50; + + public double MinTime => clock.MinTime + time_padding; + public double MaxTime => clock.MaxTime - time_padding; + + private readonly ReversibleClock clock; + + public WrappingTimeContainer(double startTime) + { + clock = new ReversibleClock(startTime); + } + + [BackgroundDependencyLoader] + private void load() + { + // Replace the game clock, but keep it as a reference + clock.SetSource(Clock); + Clock = clock; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + clock.MinTime = -time_padding; + clock.MaxTime = intervalAt(interval_count) + time_padding; + } + + private class ReversibleClock : IFrameBasedClock + { + private readonly double startTime; + public double MinTime; + public double MaxTime = 1000; + + private IFrameBasedClock trackingClock; + + private bool reversed; + + public ReversibleClock(double startTime) + { + this.startTime = startTime; + } + + public void SetSource(IFrameBasedClock trackingClock) + { + this.trackingClock = new FramedOffsetClock(trackingClock) { Offset = -trackingClock.CurrentTime + startTime }; + } + + public double CurrentTime { get; private set; } + + public double Rate => trackingClock.Rate; + + public bool IsRunning => trackingClock.IsRunning; + + public double ElapsedFrameTime => (reversed ? -1 : 1) * trackingClock.ElapsedFrameTime; + + public double AverageFrameTime => trackingClock.AverageFrameTime; + + public double FramesPerSecond => trackingClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => new FrameTimeInfo { Current = CurrentTime, Elapsed = ElapsedFrameTime }; + + public void ProcessFrame() + { + trackingClock.ProcessFrame(); + + // There are two iterations, when iteration % 2 == 0 : not reversed + int iteration = (int)(trackingClock.CurrentTime / (MaxTime - MinTime)); + reversed = iteration % 2 == 1; + + double iterationTime = trackingClock.CurrentTime % (MaxTime - MinTime); + + if (reversed) + CurrentTime = MaxTime - iterationTime; + else + CurrentTime = MinTime + iterationTime; + } + } + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTransformSequence.cs b/osu.Framework.Tests/Visual/TestCaseTransformSequence.cs index 66d718a60..30efce179 100644 --- a/osu.Framework.Tests/Visual/TestCaseTransformSequence.cs +++ b/osu.Framework.Tests/Visual/TestCaseTransformSequence.cs @@ -1,178 +1,178 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTransformSequence : GridTestCase - { - private readonly Container[] boxes; - - public TestCaseTransformSequence() - : base(3, 3) - { - boxes = new Container[Rows * Cols]; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - testFinish(); - testClear(); - } - - private void testFinish() - { - AddStep("Animate", delegate - { - setup(); - animate(); - }); - - AddStep($"{nameof(FinishTransforms)}", delegate - { - foreach (var box in boxes) - box.FinishTransforms(); - }); - - AddAssert("finalize triggered", () => finalizeTriggered); - } - - private void testClear() - { - AddStep("Animate", delegate - { - setup(); - animate(); - }); - - AddStep($"{nameof(ClearTransforms)}", delegate - { - foreach (var box in boxes) - box.ClearTransforms(); - }); - - AddAssert("finalize triggered", () => finalizeTriggered); - } - - private void setup() - { - finalizeTriggered = false; - - string[] labels = - { - "Spin after 2 seconds", - "Loop(1 sec pause; 1 sec rotate)", - "Complex transform 1 (should end in sync with CT2)", - "Complex transform 2 (should end in sync with CT1)", - $"Red on {nameof(TransformSequence)}.{nameof(TransformSequence.OnAbort)}", - $"Red on {nameof(TransformSequence)}.{nameof(TransformSequence.Finally)}", - "Red after instant transform", - "Red after instant transform 1 sec in the past", - "Red after 1 sec transform 1 sec in the past", - }; - - for (int i = 0; i < Rows * Cols; ++i) - { - Cell(i).Children = new Drawable[] - { - new SpriteText - { - Text = labels[i], - TextSize = 20, - }, - boxes[i] = new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 20, - Colour = Color4.Blue, - }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - } - } - }; - } - } - - private bool finalizeTriggered; - - private void animate() - { - boxes[0].Delay(500).Then(500).Then(500).Then( - b => b.Delay(500).Spin(1000, RotationDirection.CounterClockwise) - ); - - boxes[1].Delay(1000).Loop(1000, 10, b => b.RotateTo(0).RotateTo(340, 1000)); - - boxes[2].RotateTo(0).ScaleTo(1).RotateTo(360, 1000) - .Then(1000, - b => b.RotateTo(0, 1000), - b => b.ScaleTo(2, 500) - ) - .Then().RotateTo(360, 1000).ScaleTo(0.5f, 1000) - .Then().FadeEdgeEffectTo(Color4.Red, 1000).ScaleTo(2, 500); - - boxes[3].RotateTo(0).ScaleTo(1).RotateTo(360, 500) - .Then(1000, - b => b.RotateTo(0), - b => b.ScaleTo(2) - ) - .Then( - b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)).Delay(500).ScaleTo(0.5f, 500) - ) - .Then().FadeEdgeEffectTo(Color4.Red, 1000).ScaleTo(2, 500) - .Finally(_ => finalizeTriggered = true); - - - boxes[4].RotateTo(0).ScaleTo(1).RotateTo(360, 500) - .Then(1000, - b => b.RotateTo(0), - b => b.ScaleTo(2) - ) - .Then( - b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)), - b => b.ScaleTo(0.5f, 500) - ) - .OnAbort(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); - - - boxes[5].RotateTo(0).ScaleTo(1).RotateTo(360, 500) - .Then(1000, - b => b.RotateTo(0), - b => b.ScaleTo(2) - ) - .Then( - b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)), - b => b.ScaleTo(0.5f, 500) - ) - .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); - - boxes[6].RotateTo(200) - .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); - - boxes[7].Delay(-1000).RotateTo(200) - .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); - - boxes[8].Delay(-1000).RotateTo(200, 1000) - .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTransformSequence : GridTestCase + { + private readonly Container[] boxes; + + public TestCaseTransformSequence() + : base(3, 3) + { + boxes = new Container[Rows * Cols]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + testFinish(); + testClear(); + } + + private void testFinish() + { + AddStep("Animate", delegate + { + setup(); + animate(); + }); + + AddStep($"{nameof(FinishTransforms)}", delegate + { + foreach (var box in boxes) + box.FinishTransforms(); + }); + + AddAssert("finalize triggered", () => finalizeTriggered); + } + + private void testClear() + { + AddStep("Animate", delegate + { + setup(); + animate(); + }); + + AddStep($"{nameof(ClearTransforms)}", delegate + { + foreach (var box in boxes) + box.ClearTransforms(); + }); + + AddAssert("finalize triggered", () => finalizeTriggered); + } + + private void setup() + { + finalizeTriggered = false; + + string[] labels = + { + "Spin after 2 seconds", + "Loop(1 sec pause; 1 sec rotate)", + "Complex transform 1 (should end in sync with CT2)", + "Complex transform 2 (should end in sync with CT1)", + $"Red on {nameof(TransformSequence)}.{nameof(TransformSequence.OnAbort)}", + $"Red on {nameof(TransformSequence)}.{nameof(TransformSequence.Finally)}", + "Red after instant transform", + "Red after instant transform 1 sec in the past", + "Red after 1 sec transform 1 sec in the past", + }; + + for (int i = 0; i < Rows * Cols; ++i) + { + Cell(i).Children = new Drawable[] + { + new SpriteText + { + Text = labels[i], + TextSize = 20, + }, + boxes[i] = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 20, + Colour = Color4.Blue, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }; + } + } + + private bool finalizeTriggered; + + private void animate() + { + boxes[0].Delay(500).Then(500).Then(500).Then( + b => b.Delay(500).Spin(1000, RotationDirection.CounterClockwise) + ); + + boxes[1].Delay(1000).Loop(1000, 10, b => b.RotateTo(0).RotateTo(340, 1000)); + + boxes[2].RotateTo(0).ScaleTo(1).RotateTo(360, 1000) + .Then(1000, + b => b.RotateTo(0, 1000), + b => b.ScaleTo(2, 500) + ) + .Then().RotateTo(360, 1000).ScaleTo(0.5f, 1000) + .Then().FadeEdgeEffectTo(Color4.Red, 1000).ScaleTo(2, 500); + + boxes[3].RotateTo(0).ScaleTo(1).RotateTo(360, 500) + .Then(1000, + b => b.RotateTo(0), + b => b.ScaleTo(2) + ) + .Then( + b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)).Delay(500).ScaleTo(0.5f, 500) + ) + .Then().FadeEdgeEffectTo(Color4.Red, 1000).ScaleTo(2, 500) + .Finally(_ => finalizeTriggered = true); + + + boxes[4].RotateTo(0).ScaleTo(1).RotateTo(360, 500) + .Then(1000, + b => b.RotateTo(0), + b => b.ScaleTo(2) + ) + .Then( + b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)), + b => b.ScaleTo(0.5f, 500) + ) + .OnAbort(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); + + + boxes[5].RotateTo(0).ScaleTo(1).RotateTo(360, 500) + .Then(1000, + b => b.RotateTo(0), + b => b.ScaleTo(2) + ) + .Then( + b => b.Loop(500, 2, d => d.RotateTo(0).RotateTo(360, 1000)), + b => b.ScaleTo(0.5f, 500) + ) + .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); + + boxes[6].RotateTo(200) + .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); + + boxes[7].Delay(-1000).RotateTo(200) + .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); + + boxes[8].Delay(-1000).RotateTo(200, 1000) + .Finally(b => b.FadeEdgeEffectTo(Color4.Red, 1000)); + } + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseTriangles.cs b/osu.Framework.Tests/Visual/TestCaseTriangles.cs index 81c40b16a..9f87b8904 100644 --- a/osu.Framework.Tests/Visual/TestCaseTriangles.cs +++ b/osu.Framework.Tests/Visual/TestCaseTriangles.cs @@ -1,189 +1,189 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Framework.Testing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseTriangles : TestCase - { - private readonly Container testContainer; - - public TestCaseTriangles() - { - Add(testContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - string[] testNames = { @"Bounding box / input" }; - - for (int i = 0; i < testNames.Length; i++) - { - int test = i; - AddStep(testNames[i], delegate { loadTest(test); }); - } - - loadTest(0); - addCrosshair(); - } - - private void addCrosshair() - { - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(22, 4), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.Black, - Size = new Vector2(4, 22), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(20, 2), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - Add(new Box - { - Colour = Color4.WhiteSmoke, - Size = new Vector2(2, 20), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - } - - private void loadTest(int testType) - { - testContainer.Clear(); - - Triangle triangle; - - switch (testType) - { - case 0: - Container box; - - testContainer.Add(box = new InfofulBoxAutoSize - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - addCornerMarkers(box); - - box.AddRange(new[] - { - new DraggableTriangle - { - //chameleon = true, - Position = new Vector2(0, 0), - Size = new Vector2(25, 25), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.Blue, - }, - triangle = new DraggableTriangle - { - Size = new Vector2(250, 250), - Alpha = 0.5f, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = Color4.DarkSeaGreen, - } - }); - - triangle.OnUpdate += delegate { triangle.Rotation += 0.05f; }; - break; - } - -#if DEBUG - //if (toggleDebugAutosize.State) - // testContainer.Children.FindAll(c => c.HasAutosizeChildren).ForEach(c => c.AutoSizeDebug = true); -#endif - } - - private void addCornerMarkers(Container box, int size = 50, Color4? colour = null) - { - box.Add(new DraggableTriangle - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new DraggableTriangle - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new DraggableTriangle - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - - box.Add(new DraggableTriangle - { - //chameleon = true, - Size = new Vector2(size, size), - Origin = Anchor.BottomRight, - Anchor = Anchor.BottomRight, - AllowDrag = false, - Depth = -2, - Colour = colour ?? Color4.Red, - }); - } - } - - internal class DraggableTriangle : Triangle - { - public bool AllowDrag = true; - - protected override bool OnDrag(InputState state) - { - if (!AllowDrag) return false; - - Position += state.Mouse.Delta; - return true; - } - - protected override bool OnDragEnd(InputState state) - { - return true; - } - - protected override bool OnDragStart(InputState state) => AllowDrag; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Testing; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseTriangles : TestCase + { + private readonly Container testContainer; + + public TestCaseTriangles() + { + Add(testContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + string[] testNames = { @"Bounding box / input" }; + + for (int i = 0; i < testNames.Length; i++) + { + int test = i; + AddStep(testNames[i], delegate { loadTest(test); }); + } + + loadTest(0); + addCrosshair(); + } + + private void addCrosshair() + { + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(22, 4), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.Black, + Size = new Vector2(4, 22), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(20, 2), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + Add(new Box + { + Colour = Color4.WhiteSmoke, + Size = new Vector2(2, 20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + private void loadTest(int testType) + { + testContainer.Clear(); + + Triangle triangle; + + switch (testType) + { + case 0: + Container box; + + testContainer.Add(box = new InfofulBoxAutoSize + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + addCornerMarkers(box); + + box.AddRange(new[] + { + new DraggableTriangle + { + //chameleon = true, + Position = new Vector2(0, 0), + Size = new Vector2(25, 25), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Blue, + }, + triangle = new DraggableTriangle + { + Size = new Vector2(250, 250), + Alpha = 0.5f, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.DarkSeaGreen, + } + }); + + triangle.OnUpdate += delegate { triangle.Rotation += 0.05f; }; + break; + } + +#if DEBUG + //if (toggleDebugAutosize.State) + // testContainer.Children.FindAll(c => c.HasAutosizeChildren).ForEach(c => c.AutoSizeDebug = true); +#endif + } + + private void addCornerMarkers(Container box, int size = 50, Color4? colour = null) + { + box.Add(new DraggableTriangle + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new DraggableTriangle + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new DraggableTriangle + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + + box.Add(new DraggableTriangle + { + //chameleon = true, + Size = new Vector2(size, size), + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + AllowDrag = false, + Depth = -2, + Colour = colour ?? Color4.Red, + }); + } + } + + internal class DraggableTriangle : Triangle + { + public bool AllowDrag = true; + + protected override bool OnDrag(InputState state) + { + if (!AllowDrag) return false; + + Position += state.Mouse.Delta; + return true; + } + + protected override bool OnDragEnd(InputState state) + { + return true; + } + + protected override bool OnDragStart(InputState state) => AllowDrag; + } +} diff --git a/osu.Framework.Tests/Visual/TestCaseWaveform.cs b/osu.Framework.Tests/Visual/TestCaseWaveform.cs index cd011c0e9..9011f84c6 100644 --- a/osu.Framework.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Framework.Tests/Visual/TestCaseWaveform.cs @@ -1,90 +1,90 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using OpenTK; -using OpenTK.Graphics; -using System; - -namespace osu.Framework.Tests.Visual -{ - public class TestCaseWaveform : FrameworkTestCase - { - private readonly List waveforms = new List(); - - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Waveform), - typeof(WaveformGraph), - typeof(DataStreamFileProcedures) - }; - - public TestCaseWaveform() - { - FillFlowContainer flow; - Add(flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10) - }); - - for (int i = 1; i <= 16; i *= 2) - { - var newDisplay = new WaveformGraph - { - RelativeSizeAxes = Axes.Both, - Resolution = 1f / i - }; - - waveforms.Add(newDisplay); - - flow.Add(new Container - { - RelativeSizeAxes = Axes.X, - Height = 100, - Children = new Drawable[] - { - newDisplay, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.75f - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(4), - Text = $"Resolution: {1f / i:0.00}" - } - } - } - } - }); - } - } - - [BackgroundDependencyLoader] - private void load(Game game) - { - var waveform = new Waveform(game.Resources.GetStream("Tracks/sample-track.mp3")); - waveforms.ForEach(w => w.Waveform = waveform); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using OpenTK; +using OpenTK.Graphics; +using System; + +namespace osu.Framework.Tests.Visual +{ + public class TestCaseWaveform : FrameworkTestCase + { + private readonly List waveforms = new List(); + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Waveform), + typeof(WaveformGraph), + typeof(DataStreamFileProcedures) + }; + + public TestCaseWaveform() + { + FillFlowContainer flow; + Add(flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10) + }); + + for (int i = 1; i <= 16; i *= 2) + { + var newDisplay = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Resolution = 1f / i + }; + + waveforms.Add(newDisplay); + + flow.Add(new Container + { + RelativeSizeAxes = Axes.X, + Height = 100, + Children = new Drawable[] + { + newDisplay, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.75f + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(4), + Text = $"Resolution: {1f / i:0.00}" + } + } + } + } + }); + } + } + + [BackgroundDependencyLoader] + private void load(Game game) + { + var waveform = new Waveform(game.Resources.GetStream("Tracks/sample-track.mp3")); + waveforms.ForEach(w => w.Waveform = waveform); + } + } +} diff --git a/osu.Framework.Tests/VisualTestGame.cs b/osu.Framework.Tests/VisualTestGame.cs index e4db7e039..0b720c96e 100644 --- a/osu.Framework.Tests/VisualTestGame.cs +++ b/osu.Framework.Tests/VisualTestGame.cs @@ -1,34 +1,34 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Containers; -using osu.Framework.Platform; -using osu.Framework.Testing; - -namespace osu.Framework.Tests -{ - internal class VisualTestGame : TestGame - { - [BackgroundDependencyLoader] - private void load() - { - Child = new DrawSizePreservingFillContainer - { - Children = new Drawable[] - { - new TestBrowser(), - new CursorContainer(), - }, - }; - } - - public override void SetHost(GameHost host) - { - base.SetHost(host); - host.Window.CursorState |= CursorState.Hidden; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; + +namespace osu.Framework.Tests +{ + internal class VisualTestGame : TestGame + { + [BackgroundDependencyLoader] + private void load() + { + Child = new DrawSizePreservingFillContainer + { + Children = new Drawable[] + { + new TestBrowser(), + new CursorContainer(), + }, + }; + } + + public override void SetHost(GameHost host) + { + base.SetHost(host); + host.Window.CursorState |= CursorState.Hidden; + } + } +} diff --git a/osu.Framework/Allocation/BackgroundDependencyLoader.cs b/osu.Framework/Allocation/BackgroundDependencyLoader.cs index 30df8a00e..2f505be6d 100644 --- a/osu.Framework/Allocation/BackgroundDependencyLoader.cs +++ b/osu.Framework/Allocation/BackgroundDependencyLoader.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Allocation -{ - /// - /// Marks a method as the loader-Method of a , allowing for automatic injection of dependencies via the parameters of the method. - /// - [AttributeUsage(AttributeTargets.Method)] - public class BackgroundDependencyLoader : Attribute - { - /// - /// True if nulls are allowed to be passed to the method marked with this attribute. - /// - public bool PermitNulls { get; private set; } - - /// - /// Marks this method as the initializer for a class in the context of dependency injection. - /// - /// If true, the initializer may be passed null for the dependencies we can't fulfill. - public BackgroundDependencyLoader(bool permitNulls = false) - { - PermitNulls = permitNulls; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Allocation +{ + /// + /// Marks a method as the loader-Method of a , allowing for automatic injection of dependencies via the parameters of the method. + /// + [AttributeUsage(AttributeTargets.Method)] + public class BackgroundDependencyLoader : Attribute + { + /// + /// True if nulls are allowed to be passed to the method marked with this attribute. + /// + public bool PermitNulls { get; private set; } + + /// + /// Marks this method as the initializer for a class in the context of dependency injection. + /// + /// If true, the initializer may be passed null for the dependencies we can't fulfill. + public BackgroundDependencyLoader(bool permitNulls = false) + { + PermitNulls = permitNulls; + } + } +} diff --git a/osu.Framework/Allocation/BufferStack.cs b/osu.Framework/Allocation/BufferStack.cs index c91131e50..8993a7689 100644 --- a/osu.Framework/Allocation/BufferStack.cs +++ b/osu.Framework/Allocation/BufferStack.cs @@ -1,76 +1,76 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.Allocation -{ - /// - /// A stack of buffers (arrays with elements of type ) which allows bypassing the - /// garbage collector and expensive allocations when buffers can be frequently re-used. - /// The stack nature ensures that the most recently used buffers remain hot in memory, while - /// at the same time guaranteeing a certain degree of order preservation. - /// - public class BufferStack - { - private readonly int maxAmountBuffers; - private readonly Stack freeDataBuffers = new Stack(); - private readonly HashSet usedDataBuffers = new HashSet(); - - /// - /// Creates a new buffer stack containing a given maximum amount of buffers. - /// - /// The maximum amount of buffers to be contained within the buffer stack. - public BufferStack(int maxAmountBuffers) - { - this.maxAmountBuffers = maxAmountBuffers; - } - - private T[] findFreeBuffer(int minimumLength) - { - T[] buffer = null; - - if (freeDataBuffers.Count > 0) - buffer = freeDataBuffers.Pop(); - - if (buffer == null || buffer.Length < minimumLength) - buffer = new T[minimumLength]; - - if (usedDataBuffers.Count < maxAmountBuffers) - usedDataBuffers.Add(buffer); - - return buffer; - } - - private void returnFreeBuffer(T[] buffer) - { - if (usedDataBuffers.Remove(buffer)) - // We are here if the element was successfully found and removed - freeDataBuffers.Push(buffer); - } - - /// - /// Reserve a buffer from the buffer stack. If no free buffers are available, a new one is allocated. - /// - /// The minimum length required of the reserved buffer. - /// The reserved buffer. - public T[] ReserveBuffer(int minimumLength) - { - T[] buffer; - lock (freeDataBuffers) - buffer = findFreeBuffer(minimumLength); - - return buffer; - } - - /// - /// Frees a previously reserved buffer for future reservations. - /// - /// The buffer to be freed. If the buffer has not previously been reserved then this method does nothing. - public void FreeBuffer(T[] buffer) - { - lock (freeDataBuffers) - returnFreeBuffer(buffer); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.Allocation +{ + /// + /// A stack of buffers (arrays with elements of type ) which allows bypassing the + /// garbage collector and expensive allocations when buffers can be frequently re-used. + /// The stack nature ensures that the most recently used buffers remain hot in memory, while + /// at the same time guaranteeing a certain degree of order preservation. + /// + public class BufferStack + { + private readonly int maxAmountBuffers; + private readonly Stack freeDataBuffers = new Stack(); + private readonly HashSet usedDataBuffers = new HashSet(); + + /// + /// Creates a new buffer stack containing a given maximum amount of buffers. + /// + /// The maximum amount of buffers to be contained within the buffer stack. + public BufferStack(int maxAmountBuffers) + { + this.maxAmountBuffers = maxAmountBuffers; + } + + private T[] findFreeBuffer(int minimumLength) + { + T[] buffer = null; + + if (freeDataBuffers.Count > 0) + buffer = freeDataBuffers.Pop(); + + if (buffer == null || buffer.Length < minimumLength) + buffer = new T[minimumLength]; + + if (usedDataBuffers.Count < maxAmountBuffers) + usedDataBuffers.Add(buffer); + + return buffer; + } + + private void returnFreeBuffer(T[] buffer) + { + if (usedDataBuffers.Remove(buffer)) + // We are here if the element was successfully found and removed + freeDataBuffers.Push(buffer); + } + + /// + /// Reserve a buffer from the buffer stack. If no free buffers are available, a new one is allocated. + /// + /// The minimum length required of the reserved buffer. + /// The reserved buffer. + public T[] ReserveBuffer(int minimumLength) + { + T[] buffer; + lock (freeDataBuffers) + buffer = findFreeBuffer(minimumLength); + + return buffer; + } + + /// + /// Frees a previously reserved buffer for future reservations. + /// + /// The buffer to be freed. If the buffer has not previously been reserved then this method does nothing. + public void FreeBuffer(T[] buffer) + { + lock (freeDataBuffers) + returnFreeBuffer(buffer); + } + } +} diff --git a/osu.Framework/Allocation/DependencyContainer.cs b/osu.Framework/Allocation/DependencyContainer.cs index 06072a22e..b59343c0c 100644 --- a/osu.Framework/Allocation/DependencyContainer.cs +++ b/osu.Framework/Allocation/DependencyContainer.cs @@ -1,196 +1,196 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions.TypeExtensions; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using osu.Framework.Extensions.ExceptionExtensions; - -namespace osu.Framework.Allocation -{ - /// - /// Hierarchically caches dependencies and can inject those automatically into types registered for dependency injection. - /// - public class DependencyContainer : IReadOnlyDependencyContainer - { - private delegate object ObjectActivator(DependencyContainer dc, object instance); - - private readonly ConcurrentDictionary activators = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); - - private readonly IReadOnlyDependencyContainer parentContainer; - - /// - /// Create a new DependencyContainer instance. - /// - /// An optional parent container which we should use as a fallback for cache lookups. - public DependencyContainer(IReadOnlyDependencyContainer parent = null) - { - parentContainer = parent; - } - - private MethodInfo getLoaderMethod(Type type) - { - var loaderMethods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where( - mi => mi.GetCustomAttribute() != null).ToArray(); - if (loaderMethods.Length == 0) - return null; - if (loaderMethods.Length == 1) - return loaderMethods[0]; - throw new InvalidOperationException($"The type {type.ReadableName()} has more than one method marked with the {nameof(BackgroundDependencyLoader)}-Attribute. Any given type can only have one such method."); - } - - private void register(Type type, bool lazy) - { - if (activators.ContainsKey(type)) - throw new InvalidOperationException($@"Type {type.FullName} can not be registered twice"); - - var initialize = getLoaderMethod(type); - var constructor = type.GetConstructor(new Type[] { }); - - var initializerMethods = new List(); - - for (Type parent = type.BaseType; parent != typeof(object); parent = parent?.BaseType) - { - var init = getLoaderMethod(parent); - if (init != null) - initializerMethods.Insert(0, init); - } - if (initialize != null) - initializerMethods.Add(initialize); - - var initializers = initializerMethods.Select(initializer => - { - var permitNull = initializer.GetCustomAttribute().PermitNulls; - var parameters = initializer.GetParameters().Select(p => p.ParameterType) - .Select(t => new Func(() => - { - var val = Get(t); - if (val == null && !permitNull) - { - throw new InvalidOperationException( - $@"Type {t.FullName} is not registered, and is a dependency of {type.FullName}"); - } - return val; - })).ToList(); - // Test that we already have all the dependencies registered - if (!lazy) - parameters.ForEach(p => p()); - return new Action(instance => - { - var p = parameters.Select(pa => pa()).ToArray(); - - try - { - initializer.Invoke(instance, p); - } - catch (TargetInvocationException e) - { - new RecursiveLoadException(e.GetLastInvocation(), initializer).Rethrow(); - } - }); - }).ToList(); - - activators[type] = (container, instance) => - { - if (instance == null) - { - if (constructor == null) - throw new InvalidOperationException($@"Type {type.FullName} must have a parameterless constructor to initialize one from scratch."); - instance = Activator.CreateInstance(type); - } - initializers.ForEach(init => init(instance)); - return instance; - }; - } - - /// - /// Registers a type and configures a default allocator for it that injects its - /// dependencies. - /// - public void Register(bool lazy = false) where T : class => register(typeof(T), lazy); - - /// - /// Registers a type that allocates with a custom allocator. - /// - public void Register(Func activator) where T : class - { - var type = typeof(T); - if (activators.ContainsKey(type)) - throw new InvalidOperationException($@"Type {typeof(T).FullName} is already registered"); - activators[type] = (d, i) => i ?? activator(d); - } - - /// - /// Caches an instance of a type as its most derived type. This instance will be returned each time you . - /// - /// The instance to cache. - public void Cache(T instance) - where T : class - { - if (instance == null) throw new ArgumentNullException(nameof(instance)); - - cache[instance.GetType()] = instance; - } - - /// - /// Caches an instance of a type as a type of . This instance will be returned each time you . - /// - /// The instance to cache. Must be or derive from . - public void CacheAs(T instance) - where T : class - { - cache[typeof(T)] = instance ?? throw new ArgumentNullException(nameof(instance)); - } - - /// - /// Retrieves a cached dependency of if it exists. If not, then the parent - /// is recursively queried. If no parent contains - /// , then null is returned. - /// - /// The dependency type to query for. - /// The requested dependency, or null if not found. - public object Get(Type type) - { - if (cache.TryGetValue(type, out object ret)) - return ret; - - return parentContainer?.Get(type); - - //we don't ever want to instantiate for now, as this breaks expectations when using permitNull. - //need to revisit this when/if it is required. - //if (!activators.ContainsKey(type)) - // return null; // Or an exception? - //object instance = activators[type](this, null); - //if (cacheable.Contains(type)) - // cache[type] = instance; - //return instance; - } - - /// - /// Injects dependencies into the given instance. - /// - /// The type of the instance to inject dependencies into. - /// The instance to inject dependencies into. - /// True if the instance should be automatically registered as injectable if it isn't already. - /// True if the dependencies should be initialized lazily. - public void Inject(T instance, bool autoRegister = true, bool lazy = false) where T : class - { - var type = instance.GetType(); - - // TODO: consider using parentContainer for activator lookups as a potential performance improvement. - - lock (activators) - if (autoRegister && !activators.ContainsKey(type)) - register(type, lazy); - - if (!activators.TryGetValue(type, out ObjectActivator activator)) - throw new InvalidOperationException("DI Initialisation failed badly."); - - activator(this, instance); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions.TypeExtensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using osu.Framework.Extensions.ExceptionExtensions; + +namespace osu.Framework.Allocation +{ + /// + /// Hierarchically caches dependencies and can inject those automatically into types registered for dependency injection. + /// + public class DependencyContainer : IReadOnlyDependencyContainer + { + private delegate object ObjectActivator(DependencyContainer dc, object instance); + + private readonly ConcurrentDictionary activators = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + + private readonly IReadOnlyDependencyContainer parentContainer; + + /// + /// Create a new DependencyContainer instance. + /// + /// An optional parent container which we should use as a fallback for cache lookups. + public DependencyContainer(IReadOnlyDependencyContainer parent = null) + { + parentContainer = parent; + } + + private MethodInfo getLoaderMethod(Type type) + { + var loaderMethods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where( + mi => mi.GetCustomAttribute() != null).ToArray(); + if (loaderMethods.Length == 0) + return null; + if (loaderMethods.Length == 1) + return loaderMethods[0]; + throw new InvalidOperationException($"The type {type.ReadableName()} has more than one method marked with the {nameof(BackgroundDependencyLoader)}-Attribute. Any given type can only have one such method."); + } + + private void register(Type type, bool lazy) + { + if (activators.ContainsKey(type)) + throw new InvalidOperationException($@"Type {type.FullName} can not be registered twice"); + + var initialize = getLoaderMethod(type); + var constructor = type.GetConstructor(new Type[] { }); + + var initializerMethods = new List(); + + for (Type parent = type.BaseType; parent != typeof(object); parent = parent?.BaseType) + { + var init = getLoaderMethod(parent); + if (init != null) + initializerMethods.Insert(0, init); + } + if (initialize != null) + initializerMethods.Add(initialize); + + var initializers = initializerMethods.Select(initializer => + { + var permitNull = initializer.GetCustomAttribute().PermitNulls; + var parameters = initializer.GetParameters().Select(p => p.ParameterType) + .Select(t => new Func(() => + { + var val = Get(t); + if (val == null && !permitNull) + { + throw new InvalidOperationException( + $@"Type {t.FullName} is not registered, and is a dependency of {type.FullName}"); + } + return val; + })).ToList(); + // Test that we already have all the dependencies registered + if (!lazy) + parameters.ForEach(p => p()); + return new Action(instance => + { + var p = parameters.Select(pa => pa()).ToArray(); + + try + { + initializer.Invoke(instance, p); + } + catch (TargetInvocationException e) + { + new RecursiveLoadException(e.GetLastInvocation(), initializer).Rethrow(); + } + }); + }).ToList(); + + activators[type] = (container, instance) => + { + if (instance == null) + { + if (constructor == null) + throw new InvalidOperationException($@"Type {type.FullName} must have a parameterless constructor to initialize one from scratch."); + instance = Activator.CreateInstance(type); + } + initializers.ForEach(init => init(instance)); + return instance; + }; + } + + /// + /// Registers a type and configures a default allocator for it that injects its + /// dependencies. + /// + public void Register(bool lazy = false) where T : class => register(typeof(T), lazy); + + /// + /// Registers a type that allocates with a custom allocator. + /// + public void Register(Func activator) where T : class + { + var type = typeof(T); + if (activators.ContainsKey(type)) + throw new InvalidOperationException($@"Type {typeof(T).FullName} is already registered"); + activators[type] = (d, i) => i ?? activator(d); + } + + /// + /// Caches an instance of a type as its most derived type. This instance will be returned each time you . + /// + /// The instance to cache. + public void Cache(T instance) + where T : class + { + if (instance == null) throw new ArgumentNullException(nameof(instance)); + + cache[instance.GetType()] = instance; + } + + /// + /// Caches an instance of a type as a type of . This instance will be returned each time you . + /// + /// The instance to cache. Must be or derive from . + public void CacheAs(T instance) + where T : class + { + cache[typeof(T)] = instance ?? throw new ArgumentNullException(nameof(instance)); + } + + /// + /// Retrieves a cached dependency of if it exists. If not, then the parent + /// is recursively queried. If no parent contains + /// , then null is returned. + /// + /// The dependency type to query for. + /// The requested dependency, or null if not found. + public object Get(Type type) + { + if (cache.TryGetValue(type, out object ret)) + return ret; + + return parentContainer?.Get(type); + + //we don't ever want to instantiate for now, as this breaks expectations when using permitNull. + //need to revisit this when/if it is required. + //if (!activators.ContainsKey(type)) + // return null; // Or an exception? + //object instance = activators[type](this, null); + //if (cacheable.Contains(type)) + // cache[type] = instance; + //return instance; + } + + /// + /// Injects dependencies into the given instance. + /// + /// The type of the instance to inject dependencies into. + /// The instance to inject dependencies into. + /// True if the instance should be automatically registered as injectable if it isn't already. + /// True if the dependencies should be initialized lazily. + public void Inject(T instance, bool autoRegister = true, bool lazy = false) where T : class + { + var type = instance.GetType(); + + // TODO: consider using parentContainer for activator lookups as a potential performance improvement. + + lock (activators) + if (autoRegister && !activators.ContainsKey(type)) + register(type, lazy); + + if (!activators.TryGetValue(type, out ObjectActivator activator)) + throw new InvalidOperationException("DI Initialisation failed badly."); + + activator(this, instance); + } + } +} diff --git a/osu.Framework/Allocation/IReadOnlyDependencyContainer.cs b/osu.Framework/Allocation/IReadOnlyDependencyContainer.cs index 8ab96a0f1..cde8fdc00 100644 --- a/osu.Framework/Allocation/IReadOnlyDependencyContainer.cs +++ b/osu.Framework/Allocation/IReadOnlyDependencyContainer.cs @@ -1,42 +1,42 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Allocation -{ - /// - /// Read-only interface into a dependency container capable of injective and retrieving dependencies based - /// on types. - /// - public interface IReadOnlyDependencyContainer - { - /// - /// Retrieves a cached dependency of if it exists and null otherwise. - /// - /// The dependency type to query for. - /// The requested dependency, or null if not found. - object Get(Type type); - - /// - /// Injects dependencies into the given instance. - /// - /// The type of the instance to inject dependencies into. - /// The instance to inject dependencies into. - /// True if the instance should be automatically registered as injectable if it isn't already. - /// True if the dependencies should be initialized lazily. - void Inject(T instance, bool autoRegister = true, bool lazy = false) where T : class; - } - - public static class ReadOnlyDependencyContainerExtensions - { - /// - /// Retrieves a cached dependency of type if it exists and null otherwise. - /// - /// The dependency type to query for. - /// The to query. - /// The requested dependency, or null if not found. - public static T Get(this IReadOnlyDependencyContainer container) where T : class => - (T)container.Get(typeof(T)); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Allocation +{ + /// + /// Read-only interface into a dependency container capable of injective and retrieving dependencies based + /// on types. + /// + public interface IReadOnlyDependencyContainer + { + /// + /// Retrieves a cached dependency of if it exists and null otherwise. + /// + /// The dependency type to query for. + /// The requested dependency, or null if not found. + object Get(Type type); + + /// + /// Injects dependencies into the given instance. + /// + /// The type of the instance to inject dependencies into. + /// The instance to inject dependencies into. + /// True if the instance should be automatically registered as injectable if it isn't already. + /// True if the dependencies should be initialized lazily. + void Inject(T instance, bool autoRegister = true, bool lazy = false) where T : class; + } + + public static class ReadOnlyDependencyContainerExtensions + { + /// + /// Retrieves a cached dependency of type if it exists and null otherwise. + /// + /// The dependency type to query for. + /// The to query. + /// The requested dependency, or null if not found. + public static T Get(this IReadOnlyDependencyContainer container) where T : class => + (T)container.Get(typeof(T)); + } +} diff --git a/osu.Framework/Allocation/InvokeOnDisposal.cs b/osu.Framework/Allocation/InvokeOnDisposal.cs index 4c4f01578..4fab5fd5b 100644 --- a/osu.Framework/Allocation/InvokeOnDisposal.cs +++ b/osu.Framework/Allocation/InvokeOnDisposal.cs @@ -1,39 +1,39 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Allocation -{ - /// - /// Instances of this class capture an action for later cleanup. When a method returns an instance of this class, the appropriate usage is: - /// using (SomeMethod()) - /// { - /// // ... - /// } - /// The using block will automatically dispose the returned instance, doing the necessary cleanup work. - /// - public class InvokeOnDisposal : IDisposable - { - private readonly Action action; - - /// - /// Constructs a new instance, capturing the given action to be run during disposal. - /// - /// The action to invoke during disposal. - public InvokeOnDisposal(Action action) => this.action = action ?? throw new ArgumentNullException(nameof(action)); - - #region IDisposable Support - - /// - /// Disposes this instance, calling the initially captured action. - /// - public void Dispose() - { - //no isDisposed check here so we can reuse these instances multiple times to save on allocations. - action(); - } - - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Allocation +{ + /// + /// Instances of this class capture an action for later cleanup. When a method returns an instance of this class, the appropriate usage is: + /// using (SomeMethod()) + /// { + /// // ... + /// } + /// The using block will automatically dispose the returned instance, doing the necessary cleanup work. + /// + public class InvokeOnDisposal : IDisposable + { + private readonly Action action; + + /// + /// Constructs a new instance, capturing the given action to be run during disposal. + /// + /// The action to invoke during disposal. + public InvokeOnDisposal(Action action) => this.action = action ?? throw new ArgumentNullException(nameof(action)); + + #region IDisposable Support + + /// + /// Disposes this instance, calling the initially captured action. + /// + public void Dispose() + { + //no isDisposed check here so we can reuse these instances multiple times to save on allocations. + action(); + } + + #endregion + } +} diff --git a/osu.Framework/Allocation/ObjectStack.cs b/osu.Framework/Allocation/ObjectStack.cs index 1ff9e4036..93d2df16e 100644 --- a/osu.Framework/Allocation/ObjectStack.cs +++ b/osu.Framework/Allocation/ObjectStack.cs @@ -1,59 +1,59 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.Allocation -{ - public class ObjectStack where T : new() - { - private readonly int maxAmountObjects; - private readonly Stack freeObjects = new Stack(); - private int usedObjects; - - public ObjectStack(int maxAmountObjects = -1) - { - this.maxAmountObjects = maxAmountObjects; - } - - private T findFreeObject() - { - T o = freeObjects.Count > 0 ? freeObjects.Pop() : new T(); - - if (maxAmountObjects == -1 || usedObjects < maxAmountObjects) - usedObjects++; - - return o; - } - - private void returnFreeObject(T o) - { - if (usedObjects-- > 0) - // We are here if the element was successfully found and removed - freeObjects.Push(o); - } - - /// - /// Reserve an object from the pool. This is used to avoid excessive amounts of heap allocations. - /// - /// The reserved object. - public T ReserveObject() - { - T o; - lock (freeObjects) - o = findFreeObject(); - - return o; - } - - /// - /// Frees a previously reserved object for future reservations. - /// - /// The object to be freed. If the object has not previously been reserved then this method does nothing. - public void FreeObject(T o) - { - lock (freeObjects) - returnFreeObject(o); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.Allocation +{ + public class ObjectStack where T : new() + { + private readonly int maxAmountObjects; + private readonly Stack freeObjects = new Stack(); + private int usedObjects; + + public ObjectStack(int maxAmountObjects = -1) + { + this.maxAmountObjects = maxAmountObjects; + } + + private T findFreeObject() + { + T o = freeObjects.Count > 0 ? freeObjects.Pop() : new T(); + + if (maxAmountObjects == -1 || usedObjects < maxAmountObjects) + usedObjects++; + + return o; + } + + private void returnFreeObject(T o) + { + if (usedObjects-- > 0) + // We are here if the element was successfully found and removed + freeObjects.Push(o); + } + + /// + /// Reserve an object from the pool. This is used to avoid excessive amounts of heap allocations. + /// + /// The reserved object. + public T ReserveObject() + { + T o; + lock (freeObjects) + o = findFreeObject(); + + return o; + } + + /// + /// Frees a previously reserved object for future reservations. + /// + /// The object to be freed. If the object has not previously been reserved then this method does nothing. + public void FreeObject(T o) + { + lock (freeObjects) + returnFreeObject(o); + } + } +} diff --git a/osu.Framework/Allocation/ObjectUsage.cs b/osu.Framework/Allocation/ObjectUsage.cs index 32a319842..aee34bdd1 100644 --- a/osu.Framework/Allocation/ObjectUsage.cs +++ b/osu.Framework/Allocation/ObjectUsage.cs @@ -1,31 +1,31 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Allocation -{ - public class ObjectUsage : IDisposable - { - public T Object; - public int Index; - - public long FrameId; - - internal Action, UsageType> Finish; - - public UsageType Usage; - - public void Dispose() - { - Finish?.Invoke(this, Usage); - } - } - - public enum UsageType - { - None, - Read, - Write - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Allocation +{ + public class ObjectUsage : IDisposable + { + public T Object; + public int Index; + + public long FrameId; + + internal Action, UsageType> Finish; + + public UsageType Usage; + + public void Dispose() + { + Finish?.Invoke(this, Usage); + } + } + + public enum UsageType + { + None, + Read, + Write + } +} diff --git a/osu.Framework/Allocation/RecursiveLoadException.cs b/osu.Framework/Allocation/RecursiveLoadException.cs index 1e29d48a8..bb205612f 100644 --- a/osu.Framework/Allocation/RecursiveLoadException.cs +++ b/osu.Framework/Allocation/RecursiveLoadException.cs @@ -1,74 +1,74 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using System.Reflection; -using System.Text; -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Allocation -{ - /// - /// The exception that is re-thrown by when a loader invocation fails. - /// This exception type builds a readablestacktrace message since loader invocations tend to be long recursive reflection calls. - /// - public class RecursiveLoadException : Exception - { - /// - /// Types that are ignored for the custom stack traces. The initializers for these typically invoke - /// initializers in user code where the problem actually lies. - /// - private static readonly Type[] blacklist = - { - typeof(Container), - typeof(Container<>), - typeof(CompositeDrawable) - }; - - private readonly StringBuilder traceBuilder; - private readonly Exception original; - - public RecursiveLoadException(Exception original, MethodInfo loaderMethod) - { - var recursive = original as RecursiveLoadException; - - this.original = recursive?.original ?? original; - traceBuilder = recursive?.traceBuilder ?? new StringBuilder(); - - // Find the location of the load method - var loaderLocation = $"{loaderMethod.DeclaringType}.{loaderMethod.Name}"; - - if (recursive == null) - { - var lines = original.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - - // Write all lines from the original exception until the loader method is hit - foreach (var o in lines) - { - traceBuilder.AppendLine(o); - if (o.Contains(loaderLocation)) - break; - } - } - else if (!blacklist.Contains(loaderMethod.DeclaringType)) - traceBuilder.AppendLine($" at {loaderLocation} ()"); - - stackTrace = traceBuilder.ToString(); - } - - public override string Message => original.Message; - - private readonly string stackTrace; - public override string StackTrace => stackTrace; - - public override string ToString() - { - var builder = new StringBuilder(); - builder.AppendLine($"{original.GetType()}: {original.Message}"); - builder.AppendLine(stackTrace); - - return builder.ToString(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Allocation +{ + /// + /// The exception that is re-thrown by when a loader invocation fails. + /// This exception type builds a readablestacktrace message since loader invocations tend to be long recursive reflection calls. + /// + public class RecursiveLoadException : Exception + { + /// + /// Types that are ignored for the custom stack traces. The initializers for these typically invoke + /// initializers in user code where the problem actually lies. + /// + private static readonly Type[] blacklist = + { + typeof(Container), + typeof(Container<>), + typeof(CompositeDrawable) + }; + + private readonly StringBuilder traceBuilder; + private readonly Exception original; + + public RecursiveLoadException(Exception original, MethodInfo loaderMethod) + { + var recursive = original as RecursiveLoadException; + + this.original = recursive?.original ?? original; + traceBuilder = recursive?.traceBuilder ?? new StringBuilder(); + + // Find the location of the load method + var loaderLocation = $"{loaderMethod.DeclaringType}.{loaderMethod.Name}"; + + if (recursive == null) + { + var lines = original.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + + // Write all lines from the original exception until the loader method is hit + foreach (var o in lines) + { + traceBuilder.AppendLine(o); + if (o.Contains(loaderLocation)) + break; + } + } + else if (!blacklist.Contains(loaderMethod.DeclaringType)) + traceBuilder.AppendLine($" at {loaderLocation} ()"); + + stackTrace = traceBuilder.ToString(); + } + + public override string Message => original.Message; + + private readonly string stackTrace; + public override string StackTrace => stackTrace; + + public override string ToString() + { + var builder = new StringBuilder(); + builder.AppendLine($"{original.GetType()}: {original.Message}"); + builder.AppendLine(stackTrace); + + return builder.ToString(); + } + } +} diff --git a/osu.Framework/Allocation/TimedExpiryCache.cs b/osu.Framework/Allocation/TimedExpiryCache.cs index cff79fc07..6f1026c2b 100644 --- a/osu.Framework/Allocation/TimedExpiryCache.cs +++ b/osu.Framework/Allocation/TimedExpiryCache.cs @@ -1,115 +1,115 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace osu.Framework.Allocation -{ - /// - /// A key-value store which supports cleaning up items after a specified expiry time. - /// - public class TimedExpiryCache : IDisposable - { - private readonly ConcurrentDictionary> dictionary = new ConcurrentDictionary>(); - - /// - /// Time in milliseconds after last access after which items will be cleaned up. - /// - public int ExpiryTime = 5000; - - /// - /// The interval in milliseconds between checks for expiry. - /// - public readonly int CheckPeriod = 5000; - - private readonly Timer timer; - - public TimedExpiryCache() - { - timer = new Timer(checkExpiry, null, 0, CheckPeriod); - } - - internal void Add(TKey key, TValue value) - { - dictionary.TryAdd(key, new TimedObject(value)); - } - - private void checkExpiry(object state) - { - var now = DateTimeOffset.Now; - - foreach (var v in dictionary) - { - if ((now - v.Value.LastAccessTime).TotalMilliseconds > ExpiryTime) - dictionary.TryRemove(v.Key, out _); - } - } - - public bool TryGetValue(TKey key, out TValue value) - { - if (!dictionary.TryGetValue(key, out TimedObject timed)) - { - value = default(TValue); - return false; - } - - value = timed.Value; - return true; - } - - #region IDisposable Support - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (!isDisposed) - { - isDisposed = true; - timer.Dispose(); - } - } - - ~TimedExpiryCache() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - private class TimedObject - { - public DateTimeOffset LastAccessTime; - - private readonly T value; - - public T Value - { - get - { - updateAccessTime(); - return value; - } - } - - public TimedObject(T value) - { - this.value = value; - updateAccessTime(); - } - - private void updateAccessTime() - { - LastAccessTime = DateTimeOffset.Now; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace osu.Framework.Allocation +{ + /// + /// A key-value store which supports cleaning up items after a specified expiry time. + /// + public class TimedExpiryCache : IDisposable + { + private readonly ConcurrentDictionary> dictionary = new ConcurrentDictionary>(); + + /// + /// Time in milliseconds after last access after which items will be cleaned up. + /// + public int ExpiryTime = 5000; + + /// + /// The interval in milliseconds between checks for expiry. + /// + public readonly int CheckPeriod = 5000; + + private readonly Timer timer; + + public TimedExpiryCache() + { + timer = new Timer(checkExpiry, null, 0, CheckPeriod); + } + + internal void Add(TKey key, TValue value) + { + dictionary.TryAdd(key, new TimedObject(value)); + } + + private void checkExpiry(object state) + { + var now = DateTimeOffset.Now; + + foreach (var v in dictionary) + { + if ((now - v.Value.LastAccessTime).TotalMilliseconds > ExpiryTime) + dictionary.TryRemove(v.Key, out _); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + if (!dictionary.TryGetValue(key, out TimedObject timed)) + { + value = default(TValue); + return false; + } + + value = timed.Value; + return true; + } + + #region IDisposable Support + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + isDisposed = true; + timer.Dispose(); + } + } + + ~TimedExpiryCache() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + private class TimedObject + { + public DateTimeOffset LastAccessTime; + + private readonly T value; + + public T Value + { + get + { + updateAccessTime(); + return value; + } + } + + public TimedObject(T value) + { + this.value = value; + updateAccessTime(); + } + + private void updateAccessTime() + { + LastAccessTime = DateTimeOffset.Now; + } + } + } +} diff --git a/osu.Framework/Allocation/TripleBuffer.cs b/osu.Framework/Allocation/TripleBuffer.cs index 1f56d28d5..7adf81f07 100644 --- a/osu.Framework/Allocation/TripleBuffer.cs +++ b/osu.Framework/Allocation/TripleBuffer.cs @@ -1,91 +1,91 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading; - -namespace osu.Framework.Allocation -{ - /// - /// Handles triple-buffering of any object type. - /// Thread safety assumes at most one writer and one reader. - /// - public class TripleBuffer - { - private readonly ObjectUsage[] buffers = new ObjectUsage[3]; - - private int read; - private int write; - private int lastWrite = -1; - - private long currentFrame; - - private readonly Action, UsageType> finishDelegate; - - public TripleBuffer() - { - //caching the delegate means we only have to allocate it once, rather than once per created buffer. - finishDelegate = finish; - } - - public ObjectUsage Get(UsageType usage) - { - switch (usage) - { - case UsageType.Write: - lock (buffers) - { - while (buffers[write]?.Usage == UsageType.Read || write == lastWrite) - write = (write + 1) % 3; - } - - if (buffers[write] == null) - { - buffers[write] = new ObjectUsage - { - Finish = finishDelegate, - Usage = UsageType.Write, - Index = write, - }; - } - else - { - buffers[write].Usage = UsageType.Write; - } - - buffers[write].FrameId = Interlocked.Increment(ref currentFrame); - return buffers[write]; - case UsageType.Read: - if (lastWrite < 0) return null; - - lock (buffers) - { - read = lastWrite; - buffers[read].Usage = UsageType.Read; - } - - return buffers[read]; - } - - return null; - } - - private void finish(ObjectUsage obj, UsageType type) - { - switch (type) - { - case UsageType.Read: - lock (buffers) - buffers[read].Usage = UsageType.None; - break; - case UsageType.Write: - lock (buffers) - { - buffers[write].Usage = UsageType.None; - lastWrite = write; - } - break; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading; + +namespace osu.Framework.Allocation +{ + /// + /// Handles triple-buffering of any object type. + /// Thread safety assumes at most one writer and one reader. + /// + public class TripleBuffer + { + private readonly ObjectUsage[] buffers = new ObjectUsage[3]; + + private int read; + private int write; + private int lastWrite = -1; + + private long currentFrame; + + private readonly Action, UsageType> finishDelegate; + + public TripleBuffer() + { + //caching the delegate means we only have to allocate it once, rather than once per created buffer. + finishDelegate = finish; + } + + public ObjectUsage Get(UsageType usage) + { + switch (usage) + { + case UsageType.Write: + lock (buffers) + { + while (buffers[write]?.Usage == UsageType.Read || write == lastWrite) + write = (write + 1) % 3; + } + + if (buffers[write] == null) + { + buffers[write] = new ObjectUsage + { + Finish = finishDelegate, + Usage = UsageType.Write, + Index = write, + }; + } + else + { + buffers[write].Usage = UsageType.Write; + } + + buffers[write].FrameId = Interlocked.Increment(ref currentFrame); + return buffers[write]; + case UsageType.Read: + if (lastWrite < 0) return null; + + lock (buffers) + { + read = lastWrite; + buffers[read].Usage = UsageType.Read; + } + + return buffers[read]; + } + + return null; + } + + private void finish(ObjectUsage obj, UsageType type) + { + switch (type) + { + case UsageType.Read: + lock (buffers) + buffers[read].Usage = UsageType.None; + break; + case UsageType.Write: + lock (buffers) + { + buffers[write].Usage = UsageType.None; + lastWrite = write; + } + break; + } + } + } +} diff --git a/osu.Framework/Audio/AdjustableAudioComponent.cs b/osu.Framework/Audio/AdjustableAudioComponent.cs index 88cea41e1..56beab58a 100644 --- a/osu.Framework/Audio/AdjustableAudioComponent.cs +++ b/osu.Framework/Audio/AdjustableAudioComponent.cs @@ -1,148 +1,148 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Configuration; - -namespace osu.Framework.Audio -{ - public class AdjustableAudioComponent : AudioComponent - { - private readonly HashSet volumeAdjustments = new HashSet(); - private readonly HashSet balanceAdjustments = new HashSet(); - private readonly HashSet frequencyAdjustments = new HashSet(); - - /// - /// Global volume of this component. - /// - public readonly BindableDouble Volume = new BindableDouble(1) - { - MinValue = 0, - MaxValue = 1 - }; - - protected readonly BindableDouble VolumeCalculated = new BindableDouble(1) - { - MinValue = 0, - MaxValue = 1 - }; - - /// - /// Playback balance of this sample (-1 .. 1 where 0 is centered) - /// - public readonly BindableDouble Balance = new BindableDouble - { - MinValue = -1, - MaxValue = 1 - }; - - protected readonly BindableDouble BalanceCalculated = new BindableDouble - { - MinValue = -1, - MaxValue = 1 - }; - - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// - public readonly BindableDouble Frequency = new BindableDouble(1); - - protected readonly BindableDouble FrequencyCalculated = new BindableDouble(1); - - protected AdjustableAudioComponent() - { - Volume.ValueChanged += InvalidateState; - Balance.ValueChanged += InvalidateState; - Frequency.ValueChanged += InvalidateState; - } - - internal void InvalidateState(double newValue = 0) - { - EnqueueAction(OnStateChanged); - } - - internal virtual void OnStateChanged() - { - VolumeCalculated.Value = volumeAdjustments.Aggregate(Volume.Value, (current, adj) => current * adj); - BalanceCalculated.Value = balanceAdjustments.Aggregate(Balance.Value, (current, adj) => current + adj); - FrequencyCalculated.Value = frequencyAdjustments.Aggregate(Frequency.Value, (current, adj) => current * adj); - } - - public void AddAdjustmentDependency(AdjustableAudioComponent component) - { - AddAdjustment(AdjustableProperty.Balance, component.BalanceCalculated); - AddAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated); - AddAdjustment(AdjustableProperty.Volume, component.VolumeCalculated); - } - - public void RemoveAdjustmentDependency(AdjustableAudioComponent component) - { - RemoveAdjustment(AdjustableProperty.Balance, component.BalanceCalculated); - RemoveAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated); - RemoveAdjustment(AdjustableProperty.Volume, component.VolumeCalculated); - } - - public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) - { - switch (type) - { - case AdjustableProperty.Balance: - if (balanceAdjustments.Contains(adjustBindable)) - throw new ArgumentException("An adjustable binding may only be registered once."); - - balanceAdjustments.Add(adjustBindable); - break; - case AdjustableProperty.Frequency: - if (frequencyAdjustments.Contains(adjustBindable)) - throw new ArgumentException("An adjustable binding may only be registered once."); - - frequencyAdjustments.Add(adjustBindable); - break; - case AdjustableProperty.Volume: - if (volumeAdjustments.Contains(adjustBindable)) - throw new ArgumentException("An adjustable binding may only be registered once."); - - volumeAdjustments.Add(adjustBindable); - break; - } - - InvalidateState(); - } - - public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) - { - switch (type) - { - case AdjustableProperty.Balance: - balanceAdjustments.Remove(adjustBindable); - break; - case AdjustableProperty.Frequency: - frequencyAdjustments.Remove(adjustBindable); - break; - case AdjustableProperty.Volume: - volumeAdjustments.Remove(adjustBindable); - break; - } - - InvalidateState(); - } - - protected override void Dispose(bool disposing) - { - volumeAdjustments.Clear(); - balanceAdjustments.Clear(); - frequencyAdjustments.Clear(); - - base.Dispose(disposing); - } - } - - public enum AdjustableProperty - { - Volume, - Balance, - Frequency - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; + +namespace osu.Framework.Audio +{ + public class AdjustableAudioComponent : AudioComponent + { + private readonly HashSet volumeAdjustments = new HashSet(); + private readonly HashSet balanceAdjustments = new HashSet(); + private readonly HashSet frequencyAdjustments = new HashSet(); + + /// + /// Global volume of this component. + /// + public readonly BindableDouble Volume = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; + + protected readonly BindableDouble VolumeCalculated = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; + + /// + /// Playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public readonly BindableDouble Balance = new BindableDouble + { + MinValue = -1, + MaxValue = 1 + }; + + protected readonly BindableDouble BalanceCalculated = new BindableDouble + { + MinValue = -1, + MaxValue = 1 + }; + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public readonly BindableDouble Frequency = new BindableDouble(1); + + protected readonly BindableDouble FrequencyCalculated = new BindableDouble(1); + + protected AdjustableAudioComponent() + { + Volume.ValueChanged += InvalidateState; + Balance.ValueChanged += InvalidateState; + Frequency.ValueChanged += InvalidateState; + } + + internal void InvalidateState(double newValue = 0) + { + EnqueueAction(OnStateChanged); + } + + internal virtual void OnStateChanged() + { + VolumeCalculated.Value = volumeAdjustments.Aggregate(Volume.Value, (current, adj) => current * adj); + BalanceCalculated.Value = balanceAdjustments.Aggregate(Balance.Value, (current, adj) => current + adj); + FrequencyCalculated.Value = frequencyAdjustments.Aggregate(Frequency.Value, (current, adj) => current * adj); + } + + public void AddAdjustmentDependency(AdjustableAudioComponent component) + { + AddAdjustment(AdjustableProperty.Balance, component.BalanceCalculated); + AddAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated); + AddAdjustment(AdjustableProperty.Volume, component.VolumeCalculated); + } + + public void RemoveAdjustmentDependency(AdjustableAudioComponent component) + { + RemoveAdjustment(AdjustableProperty.Balance, component.BalanceCalculated); + RemoveAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated); + RemoveAdjustment(AdjustableProperty.Volume, component.VolumeCalculated); + } + + public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) + { + switch (type) + { + case AdjustableProperty.Balance: + if (balanceAdjustments.Contains(adjustBindable)) + throw new ArgumentException("An adjustable binding may only be registered once."); + + balanceAdjustments.Add(adjustBindable); + break; + case AdjustableProperty.Frequency: + if (frequencyAdjustments.Contains(adjustBindable)) + throw new ArgumentException("An adjustable binding may only be registered once."); + + frequencyAdjustments.Add(adjustBindable); + break; + case AdjustableProperty.Volume: + if (volumeAdjustments.Contains(adjustBindable)) + throw new ArgumentException("An adjustable binding may only be registered once."); + + volumeAdjustments.Add(adjustBindable); + break; + } + + InvalidateState(); + } + + public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) + { + switch (type) + { + case AdjustableProperty.Balance: + balanceAdjustments.Remove(adjustBindable); + break; + case AdjustableProperty.Frequency: + frequencyAdjustments.Remove(adjustBindable); + break; + case AdjustableProperty.Volume: + volumeAdjustments.Remove(adjustBindable); + break; + } + + InvalidateState(); + } + + protected override void Dispose(bool disposing) + { + volumeAdjustments.Clear(); + balanceAdjustments.Clear(); + frequencyAdjustments.Clear(); + + base.Dispose(disposing); + } + } + + public enum AdjustableProperty + { + Volume, + Balance, + Frequency + } +} diff --git a/osu.Framework/Audio/AudioCollectionManager.cs b/osu.Framework/Audio/AudioCollectionManager.cs index 8dc706509..602df454b 100644 --- a/osu.Framework/Audio/AudioCollectionManager.cs +++ b/osu.Framework/Audio/AudioCollectionManager.cs @@ -1,82 +1,82 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; - -namespace osu.Framework.Audio -{ - /// - /// A collection of audio components which need central property control. - /// - public class AudioCollectionManager : AdjustableAudioComponent - where T : AdjustableAudioComponent - { - protected List Items = new List(); - - public void AddItem(T item) - { - RegisterItem(item); - AddItemToList(item); - } - - public void AddItemToList(T item) - { - EnqueueAction(delegate - { - if (Items.Contains(item)) return; - Items.Add(item); - }); - } - - public void RegisterItem(T item) - { - EnqueueAction(() => item.AddAdjustmentDependency(this)); - } - - public void UnregisterItem(T item) - { - EnqueueAction(() => item.RemoveAdjustmentDependency(this)); - } - - internal override void OnStateChanged() - { - base.OnStateChanged(); - foreach (var item in Items) - item.OnStateChanged(); - } - - public virtual void UpdateDevice(int deviceIndex) - { - foreach (var item in Items.OfType()) - item.UpdateDevice(deviceIndex); - } - - protected override void UpdateChildren() - { - base.UpdateChildren(); - - for (int i = 0; i < Items.Count; i++) - { - var item = Items[i]; - - if (!item.IsAlive) - { - Items.RemoveAt(i--); - continue; - } - - item.Update(); - } - } - - public override void Dispose() - { - // we need to queue disposal of our Items before enqueueing the main dispose. - foreach (var i in Items) - i.Dispose(); - - base.Dispose(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Audio +{ + /// + /// A collection of audio components which need central property control. + /// + public class AudioCollectionManager : AdjustableAudioComponent + where T : AdjustableAudioComponent + { + protected List Items = new List(); + + public void AddItem(T item) + { + RegisterItem(item); + AddItemToList(item); + } + + public void AddItemToList(T item) + { + EnqueueAction(delegate + { + if (Items.Contains(item)) return; + Items.Add(item); + }); + } + + public void RegisterItem(T item) + { + EnqueueAction(() => item.AddAdjustmentDependency(this)); + } + + public void UnregisterItem(T item) + { + EnqueueAction(() => item.RemoveAdjustmentDependency(this)); + } + + internal override void OnStateChanged() + { + base.OnStateChanged(); + foreach (var item in Items) + item.OnStateChanged(); + } + + public virtual void UpdateDevice(int deviceIndex) + { + foreach (var item in Items.OfType()) + item.UpdateDevice(deviceIndex); + } + + protected override void UpdateChildren() + { + base.UpdateChildren(); + + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + if (!item.IsAlive) + { + Items.RemoveAt(i--); + continue; + } + + item.Update(); + } + } + + public override void Dispose() + { + // we need to queue disposal of our Items before enqueueing the main dispose. + foreach (var i in Items) + i.Dispose(); + + base.Dispose(); + } + } +} diff --git a/osu.Framework/Audio/AudioComponent.cs b/osu.Framework/Audio/AudioComponent.cs index 83681674c..e5f711c15 100644 --- a/osu.Framework/Audio/AudioComponent.cs +++ b/osu.Framework/Audio/AudioComponent.cs @@ -1,105 +1,105 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using osu.Framework.Development; -using osu.Framework.Statistics; - -namespace osu.Framework.Audio -{ - public class AudioComponent : IDisposable, IUpdateable - { - /// - /// Audio operations will be run on a separate dedicated thread, so we need to schedule any audio API calls using this queue. - /// - protected ConcurrentQueue PendingActions = new ConcurrentQueue(); - - protected Task EnqueueAction(Action action) - { - var task = new Task(action); - - if (ThreadSafety.IsAudioThread) - { - task.RunSynchronously(); - return task; - } - - if (!acceptingActions) - // we don't want consumers to block on operations after we are disposed. - return Task.CompletedTask; - - PendingActions.Enqueue(task); - return task; - } - - private bool acceptingActions = true; - - ~AudioComponent() - { - Dispose(false); - } - - /// - /// Run each loop of the audio thread after queued actions to allow components to update anything they need to. - /// - protected virtual void UpdateState() - { - } - - protected virtual void UpdateChildren() - { - } - - /// - /// Updates this audio component. Always runs on the audio thread. - /// - public void Update() - { - ThreadSafety.EnsureNotUpdateThread(); - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not update disposed audio components."); - - FrameStatistics.Add(StatisticsCounterType.TasksRun, PendingActions.Count); - FrameStatistics.Increment(StatisticsCounterType.Components); - - while (!IsDisposed && PendingActions.TryDequeue(out Task task)) - task.RunSynchronously(); - - if (!IsDisposed) - UpdateState(); - - UpdateChildren(); - } - - /// - /// This component has completed playback and is now in a stopped state. - /// - public virtual bool HasCompleted => !IsAlive; - - /// - /// This component has completed all processing and is ready to be removed from its parent. - /// - public virtual bool IsAlive => !IsDisposed; - - public virtual bool IsLoaded => true; - - #region IDisposable Support - - protected volatile bool IsDisposed; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - IsDisposed = true; - } - - public virtual void Dispose() - { - acceptingActions = false; - PendingActions.Enqueue(new Task(() => Dispose(true))); - } - - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using osu.Framework.Development; +using osu.Framework.Statistics; + +namespace osu.Framework.Audio +{ + public class AudioComponent : IDisposable, IUpdateable + { + /// + /// Audio operations will be run on a separate dedicated thread, so we need to schedule any audio API calls using this queue. + /// + protected ConcurrentQueue PendingActions = new ConcurrentQueue(); + + protected Task EnqueueAction(Action action) + { + var task = new Task(action); + + if (ThreadSafety.IsAudioThread) + { + task.RunSynchronously(); + return task; + } + + if (!acceptingActions) + // we don't want consumers to block on operations after we are disposed. + return Task.CompletedTask; + + PendingActions.Enqueue(task); + return task; + } + + private bool acceptingActions = true; + + ~AudioComponent() + { + Dispose(false); + } + + /// + /// Run each loop of the audio thread after queued actions to allow components to update anything they need to. + /// + protected virtual void UpdateState() + { + } + + protected virtual void UpdateChildren() + { + } + + /// + /// Updates this audio component. Always runs on the audio thread. + /// + public void Update() + { + ThreadSafety.EnsureNotUpdateThread(); + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not update disposed audio components."); + + FrameStatistics.Add(StatisticsCounterType.TasksRun, PendingActions.Count); + FrameStatistics.Increment(StatisticsCounterType.Components); + + while (!IsDisposed && PendingActions.TryDequeue(out Task task)) + task.RunSynchronously(); + + if (!IsDisposed) + UpdateState(); + + UpdateChildren(); + } + + /// + /// This component has completed playback and is now in a stopped state. + /// + public virtual bool HasCompleted => !IsAlive; + + /// + /// This component has completed all processing and is ready to be removed from its parent. + /// + public virtual bool IsAlive => !IsDisposed; + + public virtual bool IsLoaded => true; + + #region IDisposable Support + + protected volatile bool IsDisposed; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + IsDisposed = true; + } + + public virtual void Dispose() + { + acceptingActions = false; + PendingActions.Enqueue(new Task(() => Dispose(true))); + } + + #endregion + } +} diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index ad1df7434..c1894764c 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -1,378 +1,378 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using ManagedBass; -using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; -using osu.Framework.Configuration; -using osu.Framework.IO.Stores; -using osu.Framework.Threading; -using System.Linq; -using System.Diagnostics; -using osu.Framework.Extensions.TypeExtensions; - -namespace osu.Framework.Audio -{ - public class AudioManager : AudioCollectionManager - { - /// - /// The manager component responsible for audio tracks (e.g. songs). - /// - public TrackManager Track => GetTrackManager(); - - /// - /// The manager component responsible for audio samples (e.g. sound effects). - /// - public SampleManager Sample => GetSampleManager(); - - /// - /// The thread audio operations (mainly Bass calls) are ran on. - /// - internal readonly AudioThread Thread; - - private List audioDevices = new List(); - private List audioDeviceNames = new List(); - - /// - /// The names of all available audio devices. - /// - public IEnumerable AudioDeviceNames => audioDeviceNames; - - /// - /// Is fired whenever a new audio device is discovered and provides its name. - /// - public event Action OnNewDevice; - - /// - /// Is fired whenever an audio device is lost and provides its name. - /// - public event Action OnLostDevice; - - /// - /// The preferred audio device we should use. A value of - /// denotes the OS default. - /// - public readonly Bindable AudioDevice = new Bindable(); - - private string currentAudioDevice; - - /// - /// Volume of all samples played game-wide. - /// - public readonly BindableDouble VolumeSample = new BindableDouble(1) - { - MinValue = 0, - MaxValue = 1 - }; - - /// - /// Volume of all tracks played game-wide. - /// - public readonly BindableDouble VolumeTrack = new BindableDouble(1) - { - MinValue = 0, - MaxValue = 1 - }; - - private Scheduler scheduler => Thread.Scheduler; - - private Scheduler eventScheduler => EventScheduler ?? scheduler; - - /// - /// The scheduler used for invoking publicly exposed delegate events. - /// - public Scheduler EventScheduler; - - private readonly Lazy globalTrackManager; - private readonly Lazy globalSampleManager; - - /// - /// Constructs an AudioManager given a track resource store, and a sample resource store. - /// - /// The resource store containing all audio tracks to be used in the future. - /// The sample store containing all audio samples to be used in the future. - public AudioManager(ResourceStore trackStore, ResourceStore sampleStore) - { - AudioDevice.ValueChanged += onDeviceChanged; - - trackStore.AddExtension(@"mp3"); - - sampleStore.AddExtension(@"wav"); - sampleStore.AddExtension(@"mp3"); - - Thread = new AudioThread(Update); - Thread.Start(); - - globalTrackManager = new Lazy(() => GetTrackManager(trackStore)); - globalSampleManager = new Lazy(() => GetSampleManager(sampleStore)); - - scheduler.Add(() => - { - try - { - setAudioDevice(); - } - catch - { - } - }); - - scheduler.AddDelayed(delegate - { - updateAvailableAudioDevices(); - checkAudioDeviceChanged(); - }, 1000, true); - } - - protected override void Dispose(bool disposing) - { - OnNewDevice = null; - OnLostDevice = null; - - base.Dispose(disposing); - } - - private void onDeviceChanged(string newDevice) - { - scheduler.Add(() => setAudioDevice(string.IsNullOrEmpty(newDevice) ? null : newDevice)); - } - - /// - /// Returns a list of the names of recognized audio devices. - /// - /// - /// The No Sound device that is in the list of Audio Devices that are stored internally is not returned. - /// Regarding the .Skip(1) as implementation for removing "No Sound", see http://bass.radio42.com/help/html/e5a666b4-1bdd-d1cb-555e-ce041997d52f.htm. - /// - /// A list of the names of recognized audio devices. - private IEnumerable getDeviceNames(List devices) => devices.Skip(1).Select(d => d.Name); - - /// - /// Obtains the corresponding to a given resource store. - /// Returns the global if no resource store is passed. - /// - /// The of which to retrieve the . - public TrackManager GetTrackManager(ResourceStore store = null) - { - if (store == null) return globalTrackManager.Value; - - TrackManager tm = new TrackManager(store); - AddItem(tm); - tm.AddAdjustment(AdjustableProperty.Volume, VolumeTrack); - VolumeTrack.ValueChanged += tm.InvalidateState; - - return tm; - } - - /// - /// Obtains the corresponding to a given resource store. - /// Returns the global if no resource store is passed. - /// - /// The of which to retrieve the . - public SampleManager GetSampleManager(IResourceStore store = null) - { - if (store == null) return globalSampleManager.Value; - - SampleManager sm = new SampleManager(store); - AddItem(sm); - sm.AddAdjustment(AdjustableProperty.Volume, VolumeSample); - VolumeSample.ValueChanged += sm.InvalidateState; - - return sm; - } - - private List getAllDevices() - { - int deviceCount = Bass.DeviceCount; - List info = new List(); - for (int i = 0; i < deviceCount; i++) - info.Add(Bass.GetDeviceInfo(i)); - - return info; - } - - private bool setAudioDevice(string preferredDevice = null) - { - updateAvailableAudioDevices(); - - string oldDevice = currentAudioDevice; - string newDevice = preferredDevice; - - if (string.IsNullOrEmpty(newDevice)) - newDevice = audioDevices.Find(df => df.IsDefault).Name; - - bool oldDeviceValid = Bass.CurrentDevice >= 0; - if (oldDeviceValid) - { - DeviceInfo oldDeviceInfo = Bass.GetDeviceInfo(Bass.CurrentDevice); - oldDeviceValid &= oldDeviceInfo.IsEnabled && oldDeviceInfo.IsInitialized; - } - - if (newDevice == oldDevice) - { - //check the old device is still valid - if (oldDeviceValid) - return true; - } - - if (string.IsNullOrEmpty(newDevice)) - return false; - - int newDeviceIndex = audioDevices.FindIndex(df => df.Name == newDevice); - - DeviceInfo newDeviceInfo = new DeviceInfo(); - - try - { - if (newDeviceIndex >= 0) - newDeviceInfo = Bass.GetDeviceInfo(newDeviceIndex); - //we may have previously initialised this device. - } - catch - { - } - - if (oldDeviceValid && (newDeviceInfo.Driver == null || !newDeviceInfo.IsEnabled)) - { - //handles the case we are trying to load a user setting which is currently unavailable, - //and we have already fallen back to a sane default. - return true; - } - - if (!Bass.Init(newDeviceIndex) && Bass.LastError != Errors.Already) - { - //the new device didn't go as planned. we need another option. - - if (preferredDevice == null) - { - //we're fucked. the default device won't initialise. - currentAudioDevice = null; - return false; - } - - //let's try again using the default device. - return setAudioDevice(); - } - - if (Bass.LastError == Errors.Already) - { - // We check if the initialization error is that we already initialized the device - // If it is, it means we can just tell Bass to use the already initialized device without much - // other fuzz. - Bass.CurrentDevice = newDeviceIndex; - Bass.Free(); - Bass.Init(newDeviceIndex); - } - - Trace.Assert(Bass.LastError == Errors.OK); - - //we have successfully initialised a new device. - currentAudioDevice = newDevice; - - UpdateDevice(newDeviceIndex); - - Bass.PlaybackBufferLength = 100; - Bass.UpdatePeriod = 5; - - return true; - } - - public override void UpdateDevice(int deviceIndex) - { - Sample.UpdateDevice(deviceIndex); - Track.UpdateDevice(deviceIndex); - } - - private void updateAvailableAudioDevices() - { - var currentDeviceList = getAllDevices().Where(d => d.IsEnabled).ToList(); - var currentDeviceNames = getDeviceNames(currentDeviceList).ToList(); - - var newDevices = currentDeviceNames.Except(audioDeviceNames).ToList(); - var lostDevices = audioDeviceNames.Except(currentDeviceNames).ToList(); - - if (newDevices.Count > 0 || lostDevices.Count > 0) - { - eventScheduler.Add(delegate - { - foreach (var d in newDevices) - OnNewDevice?.Invoke(d); - foreach (var d in lostDevices) - OnLostDevice?.Invoke(d); - }); - } - - audioDevices = currentDeviceList; - audioDeviceNames = currentDeviceNames; - } - - private void checkAudioDeviceChanged() - { - try - { - if (AudioDevice.Value == string.Empty) - { - // use default device - var device = Bass.GetDeviceInfo(Bass.CurrentDevice); - if (!device.IsDefault && !setAudioDevice()) - { - if (!device.IsEnabled || !setAudioDevice(device.Name)) - { - foreach (var d in getAllDevices()) - { - if (d.Name == device.Name || !d.IsEnabled) - continue; - - if (setAudioDevice(d.Name)) - break; - } - } - } - } - else - { - // use whatever is the preferred device - var device = Bass.GetDeviceInfo(Bass.CurrentDevice); - if (device.Name == AudioDevice.Value) - { - if (!device.IsEnabled && !setAudioDevice()) - { - foreach (var d in getAllDevices()) - { - if (d.Name == device.Name || !d.IsEnabled) - continue; - - if (setAudioDevice(d.Name)) - break; - } - } - } - else - { - var preferredDevice = getAllDevices().SingleOrDefault(d => d.Name == AudioDevice.Value); - if (preferredDevice.Name == AudioDevice.Value && preferredDevice.IsEnabled) - setAudioDevice(preferredDevice.Name); - else if (!device.IsEnabled && !setAudioDevice()) - { - foreach (var d in getAllDevices()) - { - if (d.Name == device.Name || !d.IsEnabled) - continue; - - if (setAudioDevice(d.Name)) - break; - } - } - } - } - } - catch - { - } - } - - public override string ToString() => $@"{GetType().ReadableName()} ({currentAudioDevice})"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using ManagedBass; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.IO.Stores; +using osu.Framework.Threading; +using System.Linq; +using System.Diagnostics; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Framework.Audio +{ + public class AudioManager : AudioCollectionManager + { + /// + /// The manager component responsible for audio tracks (e.g. songs). + /// + public TrackManager Track => GetTrackManager(); + + /// + /// The manager component responsible for audio samples (e.g. sound effects). + /// + public SampleManager Sample => GetSampleManager(); + + /// + /// The thread audio operations (mainly Bass calls) are ran on. + /// + internal readonly AudioThread Thread; + + private List audioDevices = new List(); + private List audioDeviceNames = new List(); + + /// + /// The names of all available audio devices. + /// + public IEnumerable AudioDeviceNames => audioDeviceNames; + + /// + /// Is fired whenever a new audio device is discovered and provides its name. + /// + public event Action OnNewDevice; + + /// + /// Is fired whenever an audio device is lost and provides its name. + /// + public event Action OnLostDevice; + + /// + /// The preferred audio device we should use. A value of + /// denotes the OS default. + /// + public readonly Bindable AudioDevice = new Bindable(); + + private string currentAudioDevice; + + /// + /// Volume of all samples played game-wide. + /// + public readonly BindableDouble VolumeSample = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; + + /// + /// Volume of all tracks played game-wide. + /// + public readonly BindableDouble VolumeTrack = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; + + private Scheduler scheduler => Thread.Scheduler; + + private Scheduler eventScheduler => EventScheduler ?? scheduler; + + /// + /// The scheduler used for invoking publicly exposed delegate events. + /// + public Scheduler EventScheduler; + + private readonly Lazy globalTrackManager; + private readonly Lazy globalSampleManager; + + /// + /// Constructs an AudioManager given a track resource store, and a sample resource store. + /// + /// The resource store containing all audio tracks to be used in the future. + /// The sample store containing all audio samples to be used in the future. + public AudioManager(ResourceStore trackStore, ResourceStore sampleStore) + { + AudioDevice.ValueChanged += onDeviceChanged; + + trackStore.AddExtension(@"mp3"); + + sampleStore.AddExtension(@"wav"); + sampleStore.AddExtension(@"mp3"); + + Thread = new AudioThread(Update); + Thread.Start(); + + globalTrackManager = new Lazy(() => GetTrackManager(trackStore)); + globalSampleManager = new Lazy(() => GetSampleManager(sampleStore)); + + scheduler.Add(() => + { + try + { + setAudioDevice(); + } + catch + { + } + }); + + scheduler.AddDelayed(delegate + { + updateAvailableAudioDevices(); + checkAudioDeviceChanged(); + }, 1000, true); + } + + protected override void Dispose(bool disposing) + { + OnNewDevice = null; + OnLostDevice = null; + + base.Dispose(disposing); + } + + private void onDeviceChanged(string newDevice) + { + scheduler.Add(() => setAudioDevice(string.IsNullOrEmpty(newDevice) ? null : newDevice)); + } + + /// + /// Returns a list of the names of recognized audio devices. + /// + /// + /// The No Sound device that is in the list of Audio Devices that are stored internally is not returned. + /// Regarding the .Skip(1) as implementation for removing "No Sound", see http://bass.radio42.com/help/html/e5a666b4-1bdd-d1cb-555e-ce041997d52f.htm. + /// + /// A list of the names of recognized audio devices. + private IEnumerable getDeviceNames(List devices) => devices.Skip(1).Select(d => d.Name); + + /// + /// Obtains the corresponding to a given resource store. + /// Returns the global if no resource store is passed. + /// + /// The of which to retrieve the . + public TrackManager GetTrackManager(ResourceStore store = null) + { + if (store == null) return globalTrackManager.Value; + + TrackManager tm = new TrackManager(store); + AddItem(tm); + tm.AddAdjustment(AdjustableProperty.Volume, VolumeTrack); + VolumeTrack.ValueChanged += tm.InvalidateState; + + return tm; + } + + /// + /// Obtains the corresponding to a given resource store. + /// Returns the global if no resource store is passed. + /// + /// The of which to retrieve the . + public SampleManager GetSampleManager(IResourceStore store = null) + { + if (store == null) return globalSampleManager.Value; + + SampleManager sm = new SampleManager(store); + AddItem(sm); + sm.AddAdjustment(AdjustableProperty.Volume, VolumeSample); + VolumeSample.ValueChanged += sm.InvalidateState; + + return sm; + } + + private List getAllDevices() + { + int deviceCount = Bass.DeviceCount; + List info = new List(); + for (int i = 0; i < deviceCount; i++) + info.Add(Bass.GetDeviceInfo(i)); + + return info; + } + + private bool setAudioDevice(string preferredDevice = null) + { + updateAvailableAudioDevices(); + + string oldDevice = currentAudioDevice; + string newDevice = preferredDevice; + + if (string.IsNullOrEmpty(newDevice)) + newDevice = audioDevices.Find(df => df.IsDefault).Name; + + bool oldDeviceValid = Bass.CurrentDevice >= 0; + if (oldDeviceValid) + { + DeviceInfo oldDeviceInfo = Bass.GetDeviceInfo(Bass.CurrentDevice); + oldDeviceValid &= oldDeviceInfo.IsEnabled && oldDeviceInfo.IsInitialized; + } + + if (newDevice == oldDevice) + { + //check the old device is still valid + if (oldDeviceValid) + return true; + } + + if (string.IsNullOrEmpty(newDevice)) + return false; + + int newDeviceIndex = audioDevices.FindIndex(df => df.Name == newDevice); + + DeviceInfo newDeviceInfo = new DeviceInfo(); + + try + { + if (newDeviceIndex >= 0) + newDeviceInfo = Bass.GetDeviceInfo(newDeviceIndex); + //we may have previously initialised this device. + } + catch + { + } + + if (oldDeviceValid && (newDeviceInfo.Driver == null || !newDeviceInfo.IsEnabled)) + { + //handles the case we are trying to load a user setting which is currently unavailable, + //and we have already fallen back to a sane default. + return true; + } + + if (!Bass.Init(newDeviceIndex) && Bass.LastError != Errors.Already) + { + //the new device didn't go as planned. we need another option. + + if (preferredDevice == null) + { + //we're fucked. the default device won't initialise. + currentAudioDevice = null; + return false; + } + + //let's try again using the default device. + return setAudioDevice(); + } + + if (Bass.LastError == Errors.Already) + { + // We check if the initialization error is that we already initialized the device + // If it is, it means we can just tell Bass to use the already initialized device without much + // other fuzz. + Bass.CurrentDevice = newDeviceIndex; + Bass.Free(); + Bass.Init(newDeviceIndex); + } + + Trace.Assert(Bass.LastError == Errors.OK); + + //we have successfully initialised a new device. + currentAudioDevice = newDevice; + + UpdateDevice(newDeviceIndex); + + Bass.PlaybackBufferLength = 100; + Bass.UpdatePeriod = 5; + + return true; + } + + public override void UpdateDevice(int deviceIndex) + { + Sample.UpdateDevice(deviceIndex); + Track.UpdateDevice(deviceIndex); + } + + private void updateAvailableAudioDevices() + { + var currentDeviceList = getAllDevices().Where(d => d.IsEnabled).ToList(); + var currentDeviceNames = getDeviceNames(currentDeviceList).ToList(); + + var newDevices = currentDeviceNames.Except(audioDeviceNames).ToList(); + var lostDevices = audioDeviceNames.Except(currentDeviceNames).ToList(); + + if (newDevices.Count > 0 || lostDevices.Count > 0) + { + eventScheduler.Add(delegate + { + foreach (var d in newDevices) + OnNewDevice?.Invoke(d); + foreach (var d in lostDevices) + OnLostDevice?.Invoke(d); + }); + } + + audioDevices = currentDeviceList; + audioDeviceNames = currentDeviceNames; + } + + private void checkAudioDeviceChanged() + { + try + { + if (AudioDevice.Value == string.Empty) + { + // use default device + var device = Bass.GetDeviceInfo(Bass.CurrentDevice); + if (!device.IsDefault && !setAudioDevice()) + { + if (!device.IsEnabled || !setAudioDevice(device.Name)) + { + foreach (var d in getAllDevices()) + { + if (d.Name == device.Name || !d.IsEnabled) + continue; + + if (setAudioDevice(d.Name)) + break; + } + } + } + } + else + { + // use whatever is the preferred device + var device = Bass.GetDeviceInfo(Bass.CurrentDevice); + if (device.Name == AudioDevice.Value) + { + if (!device.IsEnabled && !setAudioDevice()) + { + foreach (var d in getAllDevices()) + { + if (d.Name == device.Name || !d.IsEnabled) + continue; + + if (setAudioDevice(d.Name)) + break; + } + } + } + else + { + var preferredDevice = getAllDevices().SingleOrDefault(d => d.Name == AudioDevice.Value); + if (preferredDevice.Name == AudioDevice.Value && preferredDevice.IsEnabled) + setAudioDevice(preferredDevice.Name); + else if (!device.IsEnabled && !setAudioDevice()) + { + foreach (var d in getAllDevices()) + { + if (d.Name == device.Name || !d.IsEnabled) + continue; + + if (setAudioDevice(d.Name)) + break; + } + } + } + } + } + catch + { + } + } + + public override string ToString() => $@"{GetType().ReadableName()} ({currentAudioDevice})"; + } +} diff --git a/osu.Framework/Audio/IBassAudio.cs b/osu.Framework/Audio/IBassAudio.cs index e49a25f2c..d1f579b3d 100644 --- a/osu.Framework/Audio/IBassAudio.cs +++ b/osu.Framework/Audio/IBassAudio.cs @@ -1,10 +1,10 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Audio -{ - internal interface IBassAudio - { - void UpdateDevice(int deviceIndex); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Audio +{ + internal interface IBassAudio + { + void UpdateDevice(int deviceIndex); + } +} diff --git a/osu.Framework/Audio/IHasPitchAdjust.cs b/osu.Framework/Audio/IHasPitchAdjust.cs index f8a6dd951..4a2599562 100644 --- a/osu.Framework/Audio/IHasPitchAdjust.cs +++ b/osu.Framework/Audio/IHasPitchAdjust.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Audio -{ - public interface IHasPitchAdjust - { - /// - /// The pitch this track is playing at, relative to original. - /// - double PitchAdjust { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Audio +{ + public interface IHasPitchAdjust + { + /// + /// The pitch this track is playing at, relative to original. + /// + double PitchAdjust { get; set; } + } +} diff --git a/osu.Framework/Audio/Sample/Sample.cs b/osu.Framework/Audio/Sample/Sample.cs index 79c2619f7..274951ba2 100644 --- a/osu.Framework/Audio/Sample/Sample.cs +++ b/osu.Framework/Audio/Sample/Sample.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Audio.Sample -{ - public abstract class Sample : AudioComponent - { - public const int DEFAULT_CONCURRENCY = 2; - - protected readonly int PlaybackConcurrency; - - /// - /// Construct a new sample. - /// - /// How many instances of this sample should be allowed to playback concurrently before stopping the longest playing. - protected Sample(int playbackConcurrency = DEFAULT_CONCURRENCY) - { - PlaybackConcurrency = playbackConcurrency; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Audio.Sample +{ + public abstract class Sample : AudioComponent + { + public const int DEFAULT_CONCURRENCY = 2; + + protected readonly int PlaybackConcurrency; + + /// + /// Construct a new sample. + /// + /// How many instances of this sample should be allowed to playback concurrently before stopping the longest playing. + protected Sample(int playbackConcurrency = DEFAULT_CONCURRENCY) + { + PlaybackConcurrency = playbackConcurrency; + } + } +} diff --git a/osu.Framework/Audio/Sample/SampleBass.cs b/osu.Framework/Audio/Sample/SampleBass.cs index e88165b6d..d2bd6963f 100644 --- a/osu.Framework/Audio/Sample/SampleBass.cs +++ b/osu.Framework/Audio/Sample/SampleBass.cs @@ -1,40 +1,40 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using ManagedBass; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace osu.Framework.Audio.Sample -{ - internal class SampleBass : Sample, IBassAudio - { - private volatile int sampleId; - - public override bool IsLoaded => sampleId != 0; - - public SampleBass(byte[] data, ConcurrentQueue customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY) - : base(concurrency) - { - if (customPendingActions != null) - PendingActions = customPendingActions; - - EnqueueAction(() => { sampleId = Bass.SampleLoad(data, 0, data.Length, PlaybackConcurrency, BassFlags.Default | BassFlags.SampleOverrideLongestPlaying); }); - } - - protected override void Dispose(bool disposing) - { - Bass.SampleFree(sampleId); - base.Dispose(disposing); - } - - void IBassAudio.UpdateDevice(int deviceIndex) - { - if (IsLoaded) - // counter-intuitively, this is the correct API to use to migrate a sample to a new device. - Bass.ChannelSetDevice(sampleId, deviceIndex); - } - - public int CreateChannel() => Bass.SampleGetChannel(sampleId); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using ManagedBass; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace osu.Framework.Audio.Sample +{ + internal class SampleBass : Sample, IBassAudio + { + private volatile int sampleId; + + public override bool IsLoaded => sampleId != 0; + + public SampleBass(byte[] data, ConcurrentQueue customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY) + : base(concurrency) + { + if (customPendingActions != null) + PendingActions = customPendingActions; + + EnqueueAction(() => { sampleId = Bass.SampleLoad(data, 0, data.Length, PlaybackConcurrency, BassFlags.Default | BassFlags.SampleOverrideLongestPlaying); }); + } + + protected override void Dispose(bool disposing) + { + Bass.SampleFree(sampleId); + base.Dispose(disposing); + } + + void IBassAudio.UpdateDevice(int deviceIndex) + { + if (IsLoaded) + // counter-intuitively, this is the correct API to use to migrate a sample to a new device. + Bass.ChannelSetDevice(sampleId, deviceIndex); + } + + public int CreateChannel() => Bass.SampleGetChannel(sampleId); + } +} diff --git a/osu.Framework/Audio/Sample/SampleChannel.cs b/osu.Framework/Audio/Sample/SampleChannel.cs index 60dd0ce47..2593d3ecd 100644 --- a/osu.Framework/Audio/Sample/SampleChannel.cs +++ b/osu.Framework/Audio/Sample/SampleChannel.cs @@ -1,56 +1,56 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; - -namespace osu.Framework.Audio.Sample -{ - public abstract class SampleChannel : AdjustableAudioComponent - { - protected bool WasStarted; - - protected Sample Sample { get; set; } - - private readonly Action onPlay; - - protected SampleChannel(Sample sample, Action onPlay) - { - Sample = sample ?? throw new ArgumentNullException(nameof(sample)); - this.onPlay = onPlay; - } - - public virtual void Play(bool restart = true) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not play disposed samples."); - - onPlay(this); - WasStarted = true; - } - - public virtual void Stop() - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not stop disposed samples."); - } - - protected override void Dispose(bool disposing) - { - Stop(); - base.Dispose(disposing); - } - - protected override void UpdateState() - { - FrameStatistics.Increment(StatisticsCounterType.SChannels); - base.UpdateState(); - } - - public abstract bool Playing { get; } - - public virtual bool Played => WasStarted && !Playing; - - public override bool IsAlive => base.IsAlive && !Played; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; + +namespace osu.Framework.Audio.Sample +{ + public abstract class SampleChannel : AdjustableAudioComponent + { + protected bool WasStarted; + + protected Sample Sample { get; set; } + + private readonly Action onPlay; + + protected SampleChannel(Sample sample, Action onPlay) + { + Sample = sample ?? throw new ArgumentNullException(nameof(sample)); + this.onPlay = onPlay; + } + + public virtual void Play(bool restart = true) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not play disposed samples."); + + onPlay(this); + WasStarted = true; + } + + public virtual void Stop() + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not stop disposed samples."); + } + + protected override void Dispose(bool disposing) + { + Stop(); + base.Dispose(disposing); + } + + protected override void UpdateState() + { + FrameStatistics.Increment(StatisticsCounterType.SChannels); + base.UpdateState(); + } + + public abstract bool Playing { get; } + + public virtual bool Played => WasStarted && !Playing; + + public override bool IsAlive => base.IsAlive && !Played; + } +} diff --git a/osu.Framework/Audio/Sample/SampleChannelBass.cs b/osu.Framework/Audio/Sample/SampleChannelBass.cs index d1004e2ca..b9528239f 100644 --- a/osu.Framework/Audio/Sample/SampleChannelBass.cs +++ b/osu.Framework/Audio/Sample/SampleChannelBass.cs @@ -1,97 +1,97 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using ManagedBass; - -namespace osu.Framework.Audio.Sample -{ - internal class SampleChannelBass : SampleChannel, IBassAudio - { - private volatile int channel; - private volatile bool playing; - - public override bool IsLoaded => Sample.IsLoaded; - - private float initialFrequency; - - public SampleChannelBass(Sample sample, Action onPlay) - : base(sample, onPlay) - { - } - - void IBassAudio.UpdateDevice(int deviceIndex) - { - // Channels created from samples can not be migrated, so we need to ensure - // a new channel is created after switching the device. We do not need to - // manually free the channel, because our Bass.Free call upon switching devices - // takes care of that. - channel = 0; - } - - internal override void OnStateChanged() - { - base.OnStateChanged(); - - if (channel != 0) - { - Bass.ChannelSetAttribute(channel, ChannelAttribute.Volume, VolumeCalculated); - Bass.ChannelSetAttribute(channel, ChannelAttribute.Pan, BalanceCalculated); - Bass.ChannelSetAttribute(channel, ChannelAttribute.Frequency, initialFrequency * FrequencyCalculated); - } - } - - public override void Play(bool restart = true) - { - EnqueueAction(() => - { - if (!IsLoaded) - { - channel = 0; - return; - } - - // We are creating a new channel for every playback, since old channels may - // be overridden when too many other channels are created from the same sample. - channel = ((SampleBass)Sample).CreateChannel(); - Bass.ChannelGetAttribute(channel, ChannelAttribute.Frequency, out initialFrequency); - }); - - InvalidateState(); - - EnqueueAction(() => - { - if (channel != 0) - Bass.ChannelPlay(channel, restart); - }); - - // Needs to happen on the main thread such that - // Played does not become true for a short moment. - playing = true; - - base.Play(restart); - } - - protected override void UpdateState() - { - base.UpdateState(); - playing = channel != 0 && Bass.ChannelIsActive(channel) != 0; - } - - public override void Stop() - { - if (channel == 0) return; - - base.Stop(); - - EnqueueAction(() => - { - Bass.ChannelStop(channel); - // ChannelStop frees the channel. - channel = 0; - }); - } - - public override bool Playing => playing; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using ManagedBass; + +namespace osu.Framework.Audio.Sample +{ + internal class SampleChannelBass : SampleChannel, IBassAudio + { + private volatile int channel; + private volatile bool playing; + + public override bool IsLoaded => Sample.IsLoaded; + + private float initialFrequency; + + public SampleChannelBass(Sample sample, Action onPlay) + : base(sample, onPlay) + { + } + + void IBassAudio.UpdateDevice(int deviceIndex) + { + // Channels created from samples can not be migrated, so we need to ensure + // a new channel is created after switching the device. We do not need to + // manually free the channel, because our Bass.Free call upon switching devices + // takes care of that. + channel = 0; + } + + internal override void OnStateChanged() + { + base.OnStateChanged(); + + if (channel != 0) + { + Bass.ChannelSetAttribute(channel, ChannelAttribute.Volume, VolumeCalculated); + Bass.ChannelSetAttribute(channel, ChannelAttribute.Pan, BalanceCalculated); + Bass.ChannelSetAttribute(channel, ChannelAttribute.Frequency, initialFrequency * FrequencyCalculated); + } + } + + public override void Play(bool restart = true) + { + EnqueueAction(() => + { + if (!IsLoaded) + { + channel = 0; + return; + } + + // We are creating a new channel for every playback, since old channels may + // be overridden when too many other channels are created from the same sample. + channel = ((SampleBass)Sample).CreateChannel(); + Bass.ChannelGetAttribute(channel, ChannelAttribute.Frequency, out initialFrequency); + }); + + InvalidateState(); + + EnqueueAction(() => + { + if (channel != 0) + Bass.ChannelPlay(channel, restart); + }); + + // Needs to happen on the main thread such that + // Played does not become true for a short moment. + playing = true; + + base.Play(restart); + } + + protected override void UpdateState() + { + base.UpdateState(); + playing = channel != 0 && Bass.ChannelIsActive(channel) != 0; + } + + public override void Stop() + { + if (channel == 0) return; + + base.Stop(); + + EnqueueAction(() => + { + Bass.ChannelStop(channel); + // ChannelStop frees the channel. + channel = 0; + }); + } + + public override bool Playing => playing; + } +} diff --git a/osu.Framework/Audio/Sample/SampleManager.cs b/osu.Framework/Audio/Sample/SampleManager.cs index 6b32d9c47..83933edf7 100644 --- a/osu.Framework/Audio/Sample/SampleManager.cs +++ b/osu.Framework/Audio/Sample/SampleManager.cs @@ -1,70 +1,70 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Concurrent; -using System.IO; -using osu.Framework.IO.Stores; -using osu.Framework.Statistics; -using System.Linq; - -namespace osu.Framework.Audio.Sample -{ - public class SampleManager : AudioCollectionManager, IResourceStore - { - private readonly IResourceStore store; - - private readonly ConcurrentDictionary sampleCache = new ConcurrentDictionary(); - - /// - /// How many instances of a single sample should be allowed to playback concurrently before stopping the longest playing. - /// - public int PlaybackConcurrency { get; set; } = Sample.DEFAULT_CONCURRENCY; - - public SampleManager(IResourceStore store) - { - this.store = store; - } - - public SampleChannel Get(string name) - { - if (string.IsNullOrEmpty(name)) return null; - - lock (sampleCache) - { - SampleChannel channel = null; - if (!sampleCache.TryGetValue(name, out Sample sample)) - { - byte[] data = store.Get(name); - sample = sampleCache[name] = data == null ? null : new SampleBass(data, PendingActions, PlaybackConcurrency); - } - - if (sample != null) - { - channel = new SampleChannelBass(sample, AddItemToList); - RegisterItem(channel); - } - - return channel; - } - } - - public override void UpdateDevice(int deviceIndex) - { - foreach (var sample in sampleCache.Values.OfType()) - sample.UpdateDevice(deviceIndex); - - base.UpdateDevice(deviceIndex); - } - - protected override void UpdateState() - { - FrameStatistics.Add(StatisticsCounterType.Samples, sampleCache.Count); - base.UpdateState(); - } - - public Stream GetStream(string name) - { - return store.GetStream(name); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Concurrent; +using System.IO; +using osu.Framework.IO.Stores; +using osu.Framework.Statistics; +using System.Linq; + +namespace osu.Framework.Audio.Sample +{ + public class SampleManager : AudioCollectionManager, IResourceStore + { + private readonly IResourceStore store; + + private readonly ConcurrentDictionary sampleCache = new ConcurrentDictionary(); + + /// + /// How many instances of a single sample should be allowed to playback concurrently before stopping the longest playing. + /// + public int PlaybackConcurrency { get; set; } = Sample.DEFAULT_CONCURRENCY; + + public SampleManager(IResourceStore store) + { + this.store = store; + } + + public SampleChannel Get(string name) + { + if (string.IsNullOrEmpty(name)) return null; + + lock (sampleCache) + { + SampleChannel channel = null; + if (!sampleCache.TryGetValue(name, out Sample sample)) + { + byte[] data = store.Get(name); + sample = sampleCache[name] = data == null ? null : new SampleBass(data, PendingActions, PlaybackConcurrency); + } + + if (sample != null) + { + channel = new SampleChannelBass(sample, AddItemToList); + RegisterItem(channel); + } + + return channel; + } + } + + public override void UpdateDevice(int deviceIndex) + { + foreach (var sample in sampleCache.Values.OfType()) + sample.UpdateDevice(deviceIndex); + + base.UpdateDevice(deviceIndex); + } + + protected override void UpdateState() + { + FrameStatistics.Add(StatisticsCounterType.Samples, sampleCache.Count); + base.UpdateState(); + } + + public Stream GetStream(string name) + { + return store.GetStream(name); + } + } +} diff --git a/osu.Framework/Audio/Track/DataStreamFileProcedures.cs b/osu.Framework/Audio/Track/DataStreamFileProcedures.cs index 9640a8ded..97212a20c 100644 --- a/osu.Framework/Audio/Track/DataStreamFileProcedures.cs +++ b/osu.Framework/Audio/Track/DataStreamFileProcedures.cs @@ -1,87 +1,87 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using System.Runtime.InteropServices; -using ManagedBass; - -namespace osu.Framework.Audio.Track -{ - internal class DataStreamFileProcedures - { - private byte[] readBuffer = new byte[32768]; - - private readonly Stream dataStream; - - public FileProcedures BassProcedures => new FileProcedures - { - Close = ac_Close, - Length = ac_Length, - Read = ac_Read, - Seek = ac_Seek - }; - - public DataStreamFileProcedures(Stream data) - { - dataStream = data; - } - - private void ac_Close(IntPtr user) - { - //manually handle closing of stream - } - - private long ac_Length(IntPtr user) - { - if (dataStream == null) return 0; - - try - { - return dataStream.Length; - } - catch - { - } - - return 0; - } - - private int ac_Read(IntPtr buffer, int length, IntPtr user) - { - if (dataStream == null) return 0; - - try - { - if (length > readBuffer.Length) - readBuffer = new byte[length]; - - if (!dataStream.CanRead) - return 0; - - int readBytes = dataStream.Read(readBuffer, 0, length); - Marshal.Copy(readBuffer, 0, buffer, readBytes); - return readBytes; - } - catch - { - } - - return 0; - } - - private bool ac_Seek(long offset, IntPtr user) - { - if (dataStream == null) return false; - - try - { - return dataStream.Seek(offset, SeekOrigin.Begin) == offset; - } - catch - { - } - return false; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using System.Runtime.InteropServices; +using ManagedBass; + +namespace osu.Framework.Audio.Track +{ + internal class DataStreamFileProcedures + { + private byte[] readBuffer = new byte[32768]; + + private readonly Stream dataStream; + + public FileProcedures BassProcedures => new FileProcedures + { + Close = ac_Close, + Length = ac_Length, + Read = ac_Read, + Seek = ac_Seek + }; + + public DataStreamFileProcedures(Stream data) + { + dataStream = data; + } + + private void ac_Close(IntPtr user) + { + //manually handle closing of stream + } + + private long ac_Length(IntPtr user) + { + if (dataStream == null) return 0; + + try + { + return dataStream.Length; + } + catch + { + } + + return 0; + } + + private int ac_Read(IntPtr buffer, int length, IntPtr user) + { + if (dataStream == null) return 0; + + try + { + if (length > readBuffer.Length) + readBuffer = new byte[length]; + + if (!dataStream.CanRead) + return 0; + + int readBytes = dataStream.Read(readBuffer, 0, length); + Marshal.Copy(readBuffer, 0, buffer, readBytes); + return readBytes; + } + catch + { + } + + return 0; + } + + private bool ac_Seek(long offset, IntPtr user) + { + if (dataStream == null) return false; + + try + { + return dataStream.Seek(offset, SeekOrigin.Begin) == offset; + } + catch + { + } + return false; + } + } +} diff --git a/osu.Framework/Audio/Track/Track.cs b/osu.Framework/Audio/Track/Track.cs index bf51614c7..40c31a424 100644 --- a/osu.Framework/Audio/Track/Track.cs +++ b/osu.Framework/Audio/Track/Track.cs @@ -1,134 +1,134 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Statistics; -using osu.Framework.Timing; -using System; - -namespace osu.Framework.Audio.Track -{ - public abstract class Track : AdjustableAudioComponent, IAdjustableClock - { - /// - /// Is this track capable of producing audio? - /// - public virtual bool IsDummyDevice => true; - - /// - /// States if this track should repeat. - /// - public bool Looping { get; set; } - - /// - /// The speed of track playback. Does not affect pitch, but will reduce playback quality due to skipped frames. - /// - public readonly BindableDouble Tempo = new BindableDouble(1); - - protected Track() - { - Tempo.ValueChanged += InvalidateState; - } - - /// - /// Reset this track to a logical default state. - /// - public virtual void Reset() - { - Volume.Value = 1; - - ResetSpeedAdjustments(); - - Stop(); - Seek(0); - } - - /// - /// Restarts this track from the beginning while retaining adjustments. - /// - public virtual void Restart() - { - Stop(); - Seek(0); - Start(); - } - - public virtual void ResetSpeedAdjustments() - { - Frequency.Value = 1; - Tempo.Value = 1; - } - - /// - /// Current position in milliseconds. - /// - public abstract double CurrentTime { get; } - - private double length; - - /// - /// Length of the track in milliseconds. - /// - public double Length - { - get => length; - set - { - if (value < 0) - throw new ArgumentException("Track length must be >= 0.", nameof(value)); - length = value; - } - } - - public virtual int? Bitrate => null; - - /// - /// Seek to a new position. - /// - /// New position in milliseconds - /// Whether the seek was successful. - public abstract bool Seek(double seek); - - public virtual void Start() - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not start disposed tracks."); - } - - public virtual void Stop() - { - } - - public abstract bool IsRunning { get; } - - /// - /// Overall playback rate (1 is 100%, -1 is reversed at 100%). - /// - public virtual double Rate - { - get { return Frequency * Tempo; } - set { Tempo.Value = value; } - } - - public bool IsReversed => Rate < 0; - - public override bool HasCompleted => IsLoaded && !IsRunning && CurrentTime >= Length; - - /// - /// Current amplitude of stereo channels where 1 is full volume and 0 is silent. - /// LeftChannel and RightChannel represent the maximum current amplitude of all of the left and right channels respectively. - /// The most recent values are returned. Synchronisation between channels should not be expected. - /// - public virtual TrackAmplitudes CurrentAmplitudes => new TrackAmplitudes(); - - protected override void UpdateState() - { - FrameStatistics.Increment(StatisticsCounterType.Tracks); - - if (Looping && HasCompleted) - Restart(); - - base.UpdateState(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Statistics; +using osu.Framework.Timing; +using System; + +namespace osu.Framework.Audio.Track +{ + public abstract class Track : AdjustableAudioComponent, IAdjustableClock + { + /// + /// Is this track capable of producing audio? + /// + public virtual bool IsDummyDevice => true; + + /// + /// States if this track should repeat. + /// + public bool Looping { get; set; } + + /// + /// The speed of track playback. Does not affect pitch, but will reduce playback quality due to skipped frames. + /// + public readonly BindableDouble Tempo = new BindableDouble(1); + + protected Track() + { + Tempo.ValueChanged += InvalidateState; + } + + /// + /// Reset this track to a logical default state. + /// + public virtual void Reset() + { + Volume.Value = 1; + + ResetSpeedAdjustments(); + + Stop(); + Seek(0); + } + + /// + /// Restarts this track from the beginning while retaining adjustments. + /// + public virtual void Restart() + { + Stop(); + Seek(0); + Start(); + } + + public virtual void ResetSpeedAdjustments() + { + Frequency.Value = 1; + Tempo.Value = 1; + } + + /// + /// Current position in milliseconds. + /// + public abstract double CurrentTime { get; } + + private double length; + + /// + /// Length of the track in milliseconds. + /// + public double Length + { + get => length; + set + { + if (value < 0) + throw new ArgumentException("Track length must be >= 0.", nameof(value)); + length = value; + } + } + + public virtual int? Bitrate => null; + + /// + /// Seek to a new position. + /// + /// New position in milliseconds + /// Whether the seek was successful. + public abstract bool Seek(double seek); + + public virtual void Start() + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not start disposed tracks."); + } + + public virtual void Stop() + { + } + + public abstract bool IsRunning { get; } + + /// + /// Overall playback rate (1 is 100%, -1 is reversed at 100%). + /// + public virtual double Rate + { + get { return Frequency * Tempo; } + set { Tempo.Value = value; } + } + + public bool IsReversed => Rate < 0; + + public override bool HasCompleted => IsLoaded && !IsRunning && CurrentTime >= Length; + + /// + /// Current amplitude of stereo channels where 1 is full volume and 0 is silent. + /// LeftChannel and RightChannel represent the maximum current amplitude of all of the left and right channels respectively. + /// The most recent values are returned. Synchronisation between channels should not be expected. + /// + public virtual TrackAmplitudes CurrentAmplitudes => new TrackAmplitudes(); + + protected override void UpdateState() + { + FrameStatistics.Increment(StatisticsCounterType.Tracks); + + if (Looping && HasCompleted) + Restart(); + + base.UpdateState(); + } + } +} diff --git a/osu.Framework/Audio/Track/TrackAmplitudes.cs b/osu.Framework/Audio/Track/TrackAmplitudes.cs index bbee0cf5b..20676d1d5 100644 --- a/osu.Framework/Audio/Track/TrackAmplitudes.cs +++ b/osu.Framework/Audio/Track/TrackAmplitudes.cs @@ -1,22 +1,22 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Audio.Track -{ - public struct TrackAmplitudes - { - public float LeftChannel; - public float RightChannel; - - public float Maximum => Math.Max(LeftChannel, RightChannel); - - public float Average => (LeftChannel + RightChannel) / 2; - - /// - /// 256 length array of bins containing the average frequency of both channels at every ~78Hz step of the audible spectrum (0Hz - 20,000Hz). - /// - public float[] FrequencyAmplitudes; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Audio.Track +{ + public struct TrackAmplitudes + { + public float LeftChannel; + public float RightChannel; + + public float Maximum => Math.Max(LeftChannel, RightChannel); + + public float Average => (LeftChannel + RightChannel) / 2; + + /// + /// 256 length array of bins containing the average frequency of both channels at every ~78Hz step of the audible spectrum (0Hz - 20,000Hz). + /// + public float[] FrequencyAmplitudes; + } +} diff --git a/osu.Framework/Audio/Track/TrackBass.cs b/osu.Framework/Audio/Track/TrackBass.cs index 01212fcc3..b5d9f18b7 100644 --- a/osu.Framework/Audio/Track/TrackBass.cs +++ b/osu.Framework/Audio/Track/TrackBass.cs @@ -1,248 +1,248 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using System.Threading; -using ManagedBass; -using ManagedBass.Fx; -using OpenTK; -using osu.Framework.IO; -using System.Diagnostics; -using System.Threading.Tasks; - -namespace osu.Framework.Audio.Track -{ - public class TrackBass : Track, IBassAudio, IHasPitchAdjust - { - private AsyncBufferStream dataStream; - - /// - /// Should this track only be used for preview purposes? This suggests it has not yet been fully loaded. - /// - public bool Preview { get; private set; } - - /// - /// The handle for this track, if there is one. - /// - private int activeStream; - - /// - /// The handle for adjusting tempo. - /// - private int tempoAdjustStream; - - /// - /// This marks if the track is paused, or stopped to the end. - /// - private bool isPlayed; - - private volatile bool isLoaded; - - public override bool IsLoaded => isLoaded; - - public TrackBass(Stream data, bool quick = false) - { - EnqueueAction(() => - { - Preview = quick; - - if (data == null) - throw new ArgumentNullException(nameof(data)); - //encapsulate incoming stream with async buffer if it isn't already. - dataStream = data as AsyncBufferStream ?? new AsyncBufferStream(data, quick ? 8 : -1); - - var procs = new DataStreamFileProcedures(dataStream); - - BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan | BassFlags.Float; - activeStream = Bass.CreateStream(StreamSystem.NoBuffer, flags, procs.BassProcedures, IntPtr.Zero); - - if (!Preview) - { - // We assign the BassFlags.Decode streams to the device "bass_nodevice" to prevent them from getting - // cleaned up during a Bass.Free call. This is necessary for seamless switching between audio devices. - // Further, we provide the flag BassFlags.FxFreeSource such that freeing the activeStream also frees - // all parent decoding streams. - const int bass_nodevice = 0x20000; - - Bass.ChannelSetDevice(activeStream, bass_nodevice); - tempoAdjustStream = BassFx.TempoCreate(activeStream, BassFlags.Decode | BassFlags.FxFreeSource); - Bass.ChannelSetDevice(activeStream, bass_nodevice); - activeStream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource); - - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoUseQuickAlgorithm, 1); - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoOverlapMilliseconds, 4); - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoSequenceMilliseconds, 30); - } - - Length = Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetLength(activeStream)) * 1000; - - Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Frequency, out float frequency); - initialFrequency = frequency; - bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate); - - isLoaded = true; - }); - - InvalidateState(); - } - - void IBassAudio.UpdateDevice(int deviceIndex) - { - Bass.ChannelSetDevice(activeStream, deviceIndex); - Trace.Assert(Bass.LastError == Errors.OK); - } - - protected override void UpdateState() - { - isRunning = Bass.ChannelIsActive(activeStream) == PlaybackState.Playing; - - double currentTimeLocal = Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetPosition(activeStream)) * 1000; - Interlocked.Exchange(ref currentTime, currentTimeLocal == Length && !isPlayed ? 0 : currentTimeLocal); - - var leftChannel = isPlayed ? Bass.ChannelGetLevelLeft(activeStream) / 32768f : -1; - var rightChannel = isPlayed ? Bass.ChannelGetLevelRight(activeStream) / 32768f : -1; - - if (leftChannel >= 0 && rightChannel >= 0) - { - currentAmplitudes.LeftChannel = leftChannel; - currentAmplitudes.RightChannel = rightChannel; - - float[] tempFrequencyData = new float[256]; - Bass.ChannelGetData(activeStream, tempFrequencyData, (int)DataFlags.FFT512); - currentAmplitudes.FrequencyAmplitudes = tempFrequencyData; - } - else - { - currentAmplitudes.LeftChannel = 0; - currentAmplitudes.RightChannel = 0; - currentAmplitudes.FrequencyAmplitudes = new float[256]; - } - - base.UpdateState(); - } - - protected override void Dispose(bool disposing) - { - if (activeStream != 0) - { - isRunning = false; - Bass.ChannelStop(activeStream); - Bass.StreamFree(activeStream); - } - - activeStream = 0; - - dataStream?.Dispose(); - dataStream = null; - - base.Dispose(disposing); - } - - public override bool IsDummyDevice => false; - - public override void Stop() - { - base.Stop(); - StopAsync().Wait(); - } - - public async Task StopAsync() - { - await EnqueueAction(() => - { - if (Bass.ChannelIsActive(activeStream) == PlaybackState.Playing) - Bass.ChannelPause(activeStream); - - isPlayed = false; - }); - } - - private int direction; - - private void setDirection(bool reverse) - { - direction = reverse ? -1 : 1; - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.ReverseDirection, direction); - } - - public override void Start() - { - base.Start(); - - StartAsync().Wait(); - } - - public async Task StartAsync() - { - await EnqueueAction(() => - { - if (Bass.ChannelPlay(activeStream)) - isPlayed = true; - else - isRunning = false; - }); - } - - public override bool Seek(double seek) => SeekAsync(seek).Result; - - public async Task SeekAsync(double seek) - { - // At this point the track may not yet be loaded which is indicated by a 0 length. - // In that case we still want to return true, hence the conservative length. - double conservativeLength = Length == 0 ? double.MaxValue : Length; - double conservativeClamped = MathHelper.Clamp(seek, 0, conservativeLength); - - await EnqueueAction(() => - { - double clamped = MathHelper.Clamp(seek, 0, Length); - - if (clamped != CurrentTime) - { - long pos = Bass.ChannelSeconds2Bytes(activeStream, clamped / 1000d); - Bass.ChannelSetPosition(activeStream, pos); - } - }); - - return conservativeClamped == seek; - } - - private double currentTime; - - public override double CurrentTime => currentTime; - - private volatile bool isRunning; - - public override bool IsRunning => isRunning; - - internal override void OnStateChanged() - { - base.OnStateChanged(); - - setDirection(FrequencyCalculated.Value < 0); - - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Volume, VolumeCalculated); - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Pan, BalanceCalculated); - Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Frequency, bassFreq); - Bass.ChannelSetAttribute(tempoAdjustStream, ChannelAttribute.Tempo, (Math.Abs(Tempo) - 1) * 100); - } - - private volatile float initialFrequency; - - private int bassFreq => (int)MathHelper.Clamp(Math.Abs(initialFrequency * FrequencyCalculated), 100, 100000); - - private volatile int bitrate; - - public override int? Bitrate => bitrate; - - public double PitchAdjust - { - get { return Frequency.Value; } - set { Frequency.Value = value; } - } - - private TrackAmplitudes currentAmplitudes; - - public override TrackAmplitudes CurrentAmplitudes => currentAmplitudes; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using System.Threading; +using ManagedBass; +using ManagedBass.Fx; +using OpenTK; +using osu.Framework.IO; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace osu.Framework.Audio.Track +{ + public class TrackBass : Track, IBassAudio, IHasPitchAdjust + { + private AsyncBufferStream dataStream; + + /// + /// Should this track only be used for preview purposes? This suggests it has not yet been fully loaded. + /// + public bool Preview { get; private set; } + + /// + /// The handle for this track, if there is one. + /// + private int activeStream; + + /// + /// The handle for adjusting tempo. + /// + private int tempoAdjustStream; + + /// + /// This marks if the track is paused, or stopped to the end. + /// + private bool isPlayed; + + private volatile bool isLoaded; + + public override bool IsLoaded => isLoaded; + + public TrackBass(Stream data, bool quick = false) + { + EnqueueAction(() => + { + Preview = quick; + + if (data == null) + throw new ArgumentNullException(nameof(data)); + //encapsulate incoming stream with async buffer if it isn't already. + dataStream = data as AsyncBufferStream ?? new AsyncBufferStream(data, quick ? 8 : -1); + + var procs = new DataStreamFileProcedures(dataStream); + + BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan | BassFlags.Float; + activeStream = Bass.CreateStream(StreamSystem.NoBuffer, flags, procs.BassProcedures, IntPtr.Zero); + + if (!Preview) + { + // We assign the BassFlags.Decode streams to the device "bass_nodevice" to prevent them from getting + // cleaned up during a Bass.Free call. This is necessary for seamless switching between audio devices. + // Further, we provide the flag BassFlags.FxFreeSource such that freeing the activeStream also frees + // all parent decoding streams. + const int bass_nodevice = 0x20000; + + Bass.ChannelSetDevice(activeStream, bass_nodevice); + tempoAdjustStream = BassFx.TempoCreate(activeStream, BassFlags.Decode | BassFlags.FxFreeSource); + Bass.ChannelSetDevice(activeStream, bass_nodevice); + activeStream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource); + + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoUseQuickAlgorithm, 1); + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoOverlapMilliseconds, 4); + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.TempoSequenceMilliseconds, 30); + } + + Length = Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetLength(activeStream)) * 1000; + + Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Frequency, out float frequency); + initialFrequency = frequency; + bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate); + + isLoaded = true; + }); + + InvalidateState(); + } + + void IBassAudio.UpdateDevice(int deviceIndex) + { + Bass.ChannelSetDevice(activeStream, deviceIndex); + Trace.Assert(Bass.LastError == Errors.OK); + } + + protected override void UpdateState() + { + isRunning = Bass.ChannelIsActive(activeStream) == PlaybackState.Playing; + + double currentTimeLocal = Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetPosition(activeStream)) * 1000; + Interlocked.Exchange(ref currentTime, currentTimeLocal == Length && !isPlayed ? 0 : currentTimeLocal); + + var leftChannel = isPlayed ? Bass.ChannelGetLevelLeft(activeStream) / 32768f : -1; + var rightChannel = isPlayed ? Bass.ChannelGetLevelRight(activeStream) / 32768f : -1; + + if (leftChannel >= 0 && rightChannel >= 0) + { + currentAmplitudes.LeftChannel = leftChannel; + currentAmplitudes.RightChannel = rightChannel; + + float[] tempFrequencyData = new float[256]; + Bass.ChannelGetData(activeStream, tempFrequencyData, (int)DataFlags.FFT512); + currentAmplitudes.FrequencyAmplitudes = tempFrequencyData; + } + else + { + currentAmplitudes.LeftChannel = 0; + currentAmplitudes.RightChannel = 0; + currentAmplitudes.FrequencyAmplitudes = new float[256]; + } + + base.UpdateState(); + } + + protected override void Dispose(bool disposing) + { + if (activeStream != 0) + { + isRunning = false; + Bass.ChannelStop(activeStream); + Bass.StreamFree(activeStream); + } + + activeStream = 0; + + dataStream?.Dispose(); + dataStream = null; + + base.Dispose(disposing); + } + + public override bool IsDummyDevice => false; + + public override void Stop() + { + base.Stop(); + StopAsync().Wait(); + } + + public async Task StopAsync() + { + await EnqueueAction(() => + { + if (Bass.ChannelIsActive(activeStream) == PlaybackState.Playing) + Bass.ChannelPause(activeStream); + + isPlayed = false; + }); + } + + private int direction; + + private void setDirection(bool reverse) + { + direction = reverse ? -1 : 1; + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.ReverseDirection, direction); + } + + public override void Start() + { + base.Start(); + + StartAsync().Wait(); + } + + public async Task StartAsync() + { + await EnqueueAction(() => + { + if (Bass.ChannelPlay(activeStream)) + isPlayed = true; + else + isRunning = false; + }); + } + + public override bool Seek(double seek) => SeekAsync(seek).Result; + + public async Task SeekAsync(double seek) + { + // At this point the track may not yet be loaded which is indicated by a 0 length. + // In that case we still want to return true, hence the conservative length. + double conservativeLength = Length == 0 ? double.MaxValue : Length; + double conservativeClamped = MathHelper.Clamp(seek, 0, conservativeLength); + + await EnqueueAction(() => + { + double clamped = MathHelper.Clamp(seek, 0, Length); + + if (clamped != CurrentTime) + { + long pos = Bass.ChannelSeconds2Bytes(activeStream, clamped / 1000d); + Bass.ChannelSetPosition(activeStream, pos); + } + }); + + return conservativeClamped == seek; + } + + private double currentTime; + + public override double CurrentTime => currentTime; + + private volatile bool isRunning; + + public override bool IsRunning => isRunning; + + internal override void OnStateChanged() + { + base.OnStateChanged(); + + setDirection(FrequencyCalculated.Value < 0); + + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Volume, VolumeCalculated); + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Pan, BalanceCalculated); + Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Frequency, bassFreq); + Bass.ChannelSetAttribute(tempoAdjustStream, ChannelAttribute.Tempo, (Math.Abs(Tempo) - 1) * 100); + } + + private volatile float initialFrequency; + + private int bassFreq => (int)MathHelper.Clamp(Math.Abs(initialFrequency * FrequencyCalculated), 100, 100000); + + private volatile int bitrate; + + public override int? Bitrate => bitrate; + + public double PitchAdjust + { + get { return Frequency.Value; } + set { Frequency.Value = value; } + } + + private TrackAmplitudes currentAmplitudes; + + public override TrackAmplitudes CurrentAmplitudes => currentAmplitudes; + } +} diff --git a/osu.Framework/Audio/Track/TrackManager.cs b/osu.Framework/Audio/Track/TrackManager.cs index cdd98f273..902876fd3 100644 --- a/osu.Framework/Audio/Track/TrackManager.cs +++ b/osu.Framework/Audio/Track/TrackManager.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.IO.Stores; - -namespace osu.Framework.Audio.Track -{ - public class TrackManager : AudioCollectionManager - { - private readonly IResourceStore store; - - public TrackManager(IResourceStore store) - { - this.store = store; - } - - public Track Get(string name) - { - if (string.IsNullOrEmpty(name)) return null; - - TrackBass track = new TrackBass(store.GetStream(name)); - AddItem(track); - return track; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.IO.Stores; + +namespace osu.Framework.Audio.Track +{ + public class TrackManager : AudioCollectionManager + { + private readonly IResourceStore store; + + public TrackManager(IResourceStore store) + { + this.store = store; + } + + public Track Get(string name) + { + if (string.IsNullOrEmpty(name)) return null; + + TrackBass track = new TrackBass(store.GetStream(name)); + AddItem(track); + return track; + } + } +} diff --git a/osu.Framework/Audio/Track/TrackVirtual.cs b/osu.Framework/Audio/Track/TrackVirtual.cs index 1c4fec7f7..25aafaba4 100644 --- a/osu.Framework/Audio/Track/TrackVirtual.cs +++ b/osu.Framework/Audio/Track/TrackVirtual.cs @@ -1,92 +1,92 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Timing; -using OpenTK; - -namespace osu.Framework.Audio.Track -{ - public class TrackVirtual : Track - { - private readonly StopwatchClock clock = new StopwatchClock(); - - private double seekOffset; - - public TrackVirtual() - { - Length = double.PositiveInfinity; - } - - public override bool Seek(double seek) - { - double current = CurrentTime; - - seekOffset = seek; - - lock (clock) - { - if (IsRunning) - clock.Restart(); - else - clock.Reset(); - } - - seekOffset = MathHelper.Clamp(seekOffset, 0, Length); - - return current != seekOffset; - } - - public override void Start() - { - lock (clock) clock.Start(); - } - - public override void Reset() - { - lock (clock) clock.Reset(); - seekOffset = 0; - - base.Reset(); - } - - public override void Stop() - { - lock (clock) clock.Stop(); - } - - public override bool IsRunning - { - get - { - lock (clock) return clock.IsRunning; - } - } - - public override double CurrentTime - { - get - { - lock (clock) return seekOffset + clock.CurrentTime; - } - } - - protected override void UpdateState() - { - base.UpdateState(); - - lock (clock) - { - if (CurrentTime >= Length) - Stop(); - } - } - - internal override void OnStateChanged() - { - base.OnStateChanged(); - - lock (clock) - clock.Rate = Tempo; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Timing; +using OpenTK; + +namespace osu.Framework.Audio.Track +{ + public class TrackVirtual : Track + { + private readonly StopwatchClock clock = new StopwatchClock(); + + private double seekOffset; + + public TrackVirtual() + { + Length = double.PositiveInfinity; + } + + public override bool Seek(double seek) + { + double current = CurrentTime; + + seekOffset = seek; + + lock (clock) + { + if (IsRunning) + clock.Restart(); + else + clock.Reset(); + } + + seekOffset = MathHelper.Clamp(seekOffset, 0, Length); + + return current != seekOffset; + } + + public override void Start() + { + lock (clock) clock.Start(); + } + + public override void Reset() + { + lock (clock) clock.Reset(); + seekOffset = 0; + + base.Reset(); + } + + public override void Stop() + { + lock (clock) clock.Stop(); + } + + public override bool IsRunning + { + get + { + lock (clock) return clock.IsRunning; + } + } + + public override double CurrentTime + { + get + { + lock (clock) return seekOffset + clock.CurrentTime; + } + } + + protected override void UpdateState() + { + base.UpdateState(); + + lock (clock) + { + if (CurrentTime >= Length) + Stop(); + } + } + + internal override void OnStateChanged() + { + base.OnStateChanged(); + + lock (clock) + clock.Rate = Tempo; + } + } +} diff --git a/osu.Framework/Audio/Track/Waveform.cs b/osu.Framework/Audio/Track/Waveform.cs index 5cf775546..7e71dafa6 100644 --- a/osu.Framework/Audio/Track/Waveform.cs +++ b/osu.Framework/Audio/Track/Waveform.cs @@ -1,216 +1,216 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ManagedBass; - -namespace osu.Framework.Audio.Track -{ - /// - /// Procsses audio sample data such that it can then be consumed to generate waveform plots of the audio. - /// - public class Waveform : IDisposable - { - /// - /// s are initially generated to a 1ms resolution to cover most use cases. - /// - private const float resolution = 0.001f; - /// - /// The data stream is iteratively decoded to provide this many points per iteration so as to not exceed BASS's internal buffer size. - /// - private const int points_per_iteration = 100000; - private const int bytes_per_sample = 4; - - private int channels; - private List points = new List(); - - private readonly CancellationTokenSource cancelSource = new CancellationTokenSource(); - private readonly Task readTask; - - /// - /// Constructs a new from provided audio data. - /// - /// The sample data stream. If null, an empty waveform is constructed. - public Waveform(Stream data = null) - { - if (data == null) return; - - readTask = Task.Run(() => - { - var procs = new DataStreamFileProcedures(data); - - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, procs.BassProcedures, IntPtr.Zero); - - Bass.ChannelGetInfo(decodeStream, out ChannelInfo info); - - long length = Bass.ChannelGetLength(decodeStream); - - // Each "point" is generated from a number of samples, each sample contains a number of channels - int sampleDataPerPoint = (int)(info.Frequency * resolution * info.Channels); - points.Capacity = (int)(length / sampleDataPerPoint); - - int bytesPerIteration = sampleDataPerPoint * points_per_iteration; - var dataBuffer = new float[bytesPerIteration / bytes_per_sample]; - - while (length > 0) - { - length = Bass.ChannelGetData(decodeStream, dataBuffer, bytesPerIteration); - int samplesRead = (int)(length / bytes_per_sample); - - // Process a sequence of samples for each point - for (int i = 0; i < samplesRead; i += sampleDataPerPoint) - { - // Process each sample in the sequence - var point = new WaveformPoint(info.Channels); - for (int j = i; j < i + sampleDataPerPoint; j += info.Channels) - { - // Process each channel in the sample - for (int c = 0; c < info.Channels; c++) - point.Amplitude[c] = Math.Max(point.Amplitude[c], Math.Abs(dataBuffer[j + c])); - } - - for (int c = 0; c < info.Channels; c++) - point.Amplitude[c] = Math.Min(1, point.Amplitude[c]); - - points.Add(point); - } - } - - channels = info.Channels; - }, cancelSource.Token); - } - - /// - /// Creates a new containing a specific number of data points by selecting the average value of each sampled group. - /// - /// The number of points the resulting should contain. - /// The token to cancel the task. - /// An async task for the generation of the . - public async Task GenerateResampledAsync(int pointCount, CancellationToken cancellationToken = default(CancellationToken)) - { - if (pointCount < 0) throw new ArgumentOutOfRangeException(nameof(pointCount)); - - if (readTask == null) - return new Waveform(); - - await readTask; - - return await Task.Run(() => - { - var generatedPoints = new List(); - float pointsPerGeneratedPoint = (float)points.Count / pointCount; - - for (float i = 0; i < points.Count; i += pointsPerGeneratedPoint) - { - int startIndex = (int)i; - int endIndex = (int)Math.Min(points.Count, Math.Ceiling(i + pointsPerGeneratedPoint)); - - var point = new WaveformPoint(channels); - for (int j = startIndex; j < endIndex; j++) - { - for (int c = 0; c < channels; c++) - point.Amplitude[c] += points[j].Amplitude[c]; - } - - // Mean - for (int c = 0; c < channels; c++) - point.Amplitude[c] /= endIndex - startIndex; - - generatedPoints.Add(point); - } - - return new Waveform - { - points = generatedPoints, - channels = channels - }; - }, cancellationToken); - } - - /// - /// Gets all the points represented by this . - /// - public List GetPoints() => GetPointsAsync().Result; - - /// - /// Gets all the points represented by this . - /// - public async Task> GetPointsAsync() - { - if (readTask == null) - return points; - - await readTask; - return points; - } - - /// - /// Gets the number of channels represented by each . - /// - public int GetChannels() => GetChannelsAsync().Result; - - /// - /// Gets the number of channels represented by each . - /// - public async Task GetChannelsAsync() - { - if (readTask == null) - return channels; - - await readTask; - return channels; - } - - #region Disposal - - ~Waveform() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (isDisposed) - return; - isDisposed = true; - - cancelSource?.Cancel(); - cancelSource?.Dispose(); - points = null; - } - - #endregion - } - - /// - /// Represents a singular point of data in a . - /// - public struct WaveformPoint - { - /// - /// An array of amplitudes, one for each channel. - /// - public readonly float[] Amplitude; - - /// - /// Cconstructs a . - /// - /// The number of channels that contain data. - public WaveformPoint(int channels) - { - Amplitude = new float[channels]; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ManagedBass; + +namespace osu.Framework.Audio.Track +{ + /// + /// Procsses audio sample data such that it can then be consumed to generate waveform plots of the audio. + /// + public class Waveform : IDisposable + { + /// + /// s are initially generated to a 1ms resolution to cover most use cases. + /// + private const float resolution = 0.001f; + /// + /// The data stream is iteratively decoded to provide this many points per iteration so as to not exceed BASS's internal buffer size. + /// + private const int points_per_iteration = 100000; + private const int bytes_per_sample = 4; + + private int channels; + private List points = new List(); + + private readonly CancellationTokenSource cancelSource = new CancellationTokenSource(); + private readonly Task readTask; + + /// + /// Constructs a new from provided audio data. + /// + /// The sample data stream. If null, an empty waveform is constructed. + public Waveform(Stream data = null) + { + if (data == null) return; + + readTask = Task.Run(() => + { + var procs = new DataStreamFileProcedures(data); + + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, procs.BassProcedures, IntPtr.Zero); + + Bass.ChannelGetInfo(decodeStream, out ChannelInfo info); + + long length = Bass.ChannelGetLength(decodeStream); + + // Each "point" is generated from a number of samples, each sample contains a number of channels + int sampleDataPerPoint = (int)(info.Frequency * resolution * info.Channels); + points.Capacity = (int)(length / sampleDataPerPoint); + + int bytesPerIteration = sampleDataPerPoint * points_per_iteration; + var dataBuffer = new float[bytesPerIteration / bytes_per_sample]; + + while (length > 0) + { + length = Bass.ChannelGetData(decodeStream, dataBuffer, bytesPerIteration); + int samplesRead = (int)(length / bytes_per_sample); + + // Process a sequence of samples for each point + for (int i = 0; i < samplesRead; i += sampleDataPerPoint) + { + // Process each sample in the sequence + var point = new WaveformPoint(info.Channels); + for (int j = i; j < i + sampleDataPerPoint; j += info.Channels) + { + // Process each channel in the sample + for (int c = 0; c < info.Channels; c++) + point.Amplitude[c] = Math.Max(point.Amplitude[c], Math.Abs(dataBuffer[j + c])); + } + + for (int c = 0; c < info.Channels; c++) + point.Amplitude[c] = Math.Min(1, point.Amplitude[c]); + + points.Add(point); + } + } + + channels = info.Channels; + }, cancelSource.Token); + } + + /// + /// Creates a new containing a specific number of data points by selecting the average value of each sampled group. + /// + /// The number of points the resulting should contain. + /// The token to cancel the task. + /// An async task for the generation of the . + public async Task GenerateResampledAsync(int pointCount, CancellationToken cancellationToken = default(CancellationToken)) + { + if (pointCount < 0) throw new ArgumentOutOfRangeException(nameof(pointCount)); + + if (readTask == null) + return new Waveform(); + + await readTask; + + return await Task.Run(() => + { + var generatedPoints = new List(); + float pointsPerGeneratedPoint = (float)points.Count / pointCount; + + for (float i = 0; i < points.Count; i += pointsPerGeneratedPoint) + { + int startIndex = (int)i; + int endIndex = (int)Math.Min(points.Count, Math.Ceiling(i + pointsPerGeneratedPoint)); + + var point = new WaveformPoint(channels); + for (int j = startIndex; j < endIndex; j++) + { + for (int c = 0; c < channels; c++) + point.Amplitude[c] += points[j].Amplitude[c]; + } + + // Mean + for (int c = 0; c < channels; c++) + point.Amplitude[c] /= endIndex - startIndex; + + generatedPoints.Add(point); + } + + return new Waveform + { + points = generatedPoints, + channels = channels + }; + }, cancellationToken); + } + + /// + /// Gets all the points represented by this . + /// + public List GetPoints() => GetPointsAsync().Result; + + /// + /// Gets all the points represented by this . + /// + public async Task> GetPointsAsync() + { + if (readTask == null) + return points; + + await readTask; + return points; + } + + /// + /// Gets the number of channels represented by each . + /// + public int GetChannels() => GetChannelsAsync().Result; + + /// + /// Gets the number of channels represented by each . + /// + public async Task GetChannelsAsync() + { + if (readTask == null) + return channels; + + await readTask; + return channels; + } + + #region Disposal + + ~Waveform() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + + cancelSource?.Cancel(); + cancelSource?.Dispose(); + points = null; + } + + #endregion + } + + /// + /// Represents a singular point of data in a . + /// + public struct WaveformPoint + { + /// + /// An array of amplitudes, one for each channel. + /// + public readonly float[] Amplitude; + + /// + /// Cconstructs a . + /// + /// The number of channels that contain data. + public WaveformPoint(int channels) + { + Amplitude = new float[channels]; + } + } +} diff --git a/osu.Framework/Caching/Cached.cs b/osu.Framework/Caching/Cached.cs index 429ce5ea2..9b25ea189 100644 --- a/osu.Framework/Caching/Cached.cs +++ b/osu.Framework/Caching/Cached.cs @@ -1,86 +1,86 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; - -namespace osu.Framework.Caching -{ - public static class StaticCached - { - internal static bool BypassCache = false; - } - - public struct Cached - { - private T value; - - public T Value - { - get - { - if (!isValid) - throw new InvalidOperationException($"May not query {nameof(Value)} of an invalid {nameof(Cached)}."); - return value; - } - - set - { - this.value = value; - isValid = true; - FrameStatistics.Increment(StatisticsCounterType.Refreshes); - } - } - - private bool isValid; - - public bool IsValid => !StaticCached.BypassCache && isValid; - - public static implicit operator T(Cached value) => value.Value; - - /// - /// Invalidate the cache of this object. - /// - /// True if we invalidated from a valid state. - public bool Invalidate() - { - if (isValid) - { - isValid = false; - FrameStatistics.Increment(StatisticsCounterType.Invalidations); - return true; - } - - return false; - } - } - - public struct Cached - { - private bool isValid; - - public bool IsValid => !StaticCached.BypassCache && isValid; - - /// - /// Invalidate the cache of this object. - /// - /// True if we invalidated from a valid state. - public bool Invalidate() - { - if (isValid) - { - isValid = false; - FrameStatistics.Increment(StatisticsCounterType.Invalidations); - return true; - } - - return false; - } - - public void Validate() - { - isValid = true; - FrameStatistics.Increment(StatisticsCounterType.Refreshes); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; + +namespace osu.Framework.Caching +{ + public static class StaticCached + { + internal static bool BypassCache = false; + } + + public struct Cached + { + private T value; + + public T Value + { + get + { + if (!isValid) + throw new InvalidOperationException($"May not query {nameof(Value)} of an invalid {nameof(Cached)}."); + return value; + } + + set + { + this.value = value; + isValid = true; + FrameStatistics.Increment(StatisticsCounterType.Refreshes); + } + } + + private bool isValid; + + public bool IsValid => !StaticCached.BypassCache && isValid; + + public static implicit operator T(Cached value) => value.Value; + + /// + /// Invalidate the cache of this object. + /// + /// True if we invalidated from a valid state. + public bool Invalidate() + { + if (isValid) + { + isValid = false; + FrameStatistics.Increment(StatisticsCounterType.Invalidations); + return true; + } + + return false; + } + } + + public struct Cached + { + private bool isValid; + + public bool IsValid => !StaticCached.BypassCache && isValid; + + /// + /// Invalidate the cache of this object. + /// + /// True if we invalidated from a valid state. + public bool Invalidate() + { + if (isValid) + { + isValid = false; + FrameStatistics.Increment(StatisticsCounterType.Invalidations); + return true; + } + + return false; + } + + public void Validate() + { + isValid = true; + FrameStatistics.Increment(StatisticsCounterType.Refreshes); + } + } +} diff --git a/osu.Framework/Configuration/Bindable.cs b/osu.Framework/Configuration/Bindable.cs index d45e7a3ee..c0dbdee02 100644 --- a/osu.Framework/Configuration/Bindable.cs +++ b/osu.Framework/Configuration/Bindable.cs @@ -1,221 +1,221 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Globalization; -using osu.Framework.Lists; - -namespace osu.Framework.Configuration -{ - /// - /// A generic implementation of a - /// - /// The type of our stored . - public class Bindable : IBindable - { - private T value; - - /// - /// The default value of this bindable. Used when calling or querying . - /// - public T Default; - - private bool disabled; - - /// - /// Whether this bindable has been disabled. When disabled, attempting to change the will result in an . - /// - public bool Disabled - { - get { return disabled; } - set - { - if (disabled == value) return; - - disabled = value; - - TriggerDisabledChange(); - } - } - - /// - /// Check whether the current is equal to . - /// - public virtual bool IsDefault => Equals(value, Default); - - /// - /// Revert the current to the defined . - /// - public void SetDefault() => Value = Default; - - /// - /// An event which is raised when has changed (or manually via ). - /// - public event Action ValueChanged; - - /// - /// An event which is raised when 's state has changed (or manually via ). - /// - public event Action DisabledChanged; - - /// - /// The current value of this bindable. - /// - public virtual T Value - { - get { return value; } - set - { - if (EqualityComparer.Default.Equals(this.value, value)) return; - - if (Disabled) - throw new InvalidOperationException($"Can not set value to \"{value.ToString()}\" as bindable is disabled."); - - this.value = value; - - TriggerValueChange(); - } - } - - /// - /// Creates a new bindable instance. - /// - /// The initial value. - public Bindable(T value = default(T)) - { - this.value = value; - } - - public static implicit operator T(Bindable value) => value.Value; - - protected WeakList> Bindings; - - private WeakReference> weakReference => new WeakReference>(this); - - /// - /// Binds outselves to another bindable such that they receive bi-directional updates. - /// We will take on any value limitations of the bindable we bind width. - /// - /// The foreign bindable. This should always be the most permanent end of the bind (ie. a ConfigManager) - public virtual void BindTo(Bindable them) - { - Value = them.Value; - Disabled = them.Disabled; - Default = them.Default; - - AddWeakReference(them.weakReference); - them.AddWeakReference(weakReference); - } - - protected void AddWeakReference(WeakReference> weakReference) - { - if (Bindings == null) - Bindings = new WeakList>(); - - Bindings.Add(weakReference); - } - - /// - /// Parse an object into this instance. - /// An object deriving T can be parsed, or a string can be parsed if T is an enum type. - /// - /// The input which is to be parsed. - public virtual void Parse(object input) - { - switch (input) - { - case T t: - Value = t; - break; - case string s: - Value = typeof(T).IsEnum - ? (T)Enum.Parse(typeof(T), s) - : (T)Convert.ChangeType(s, typeof(T), CultureInfo.InvariantCulture); - break; - default: - throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}."); - } - } - - /// - /// Raise and once, without any changes actually occurring. - /// This does not propagate to any outward bound bindables. - /// - public virtual void TriggerChange() - { - TriggerValueChange(false); - TriggerDisabledChange(false); - } - - protected void TriggerValueChange(bool propagateToBindings = true) - { - ValueChanged?.Invoke(value); - if (propagateToBindings) Bindings?.ForEachAlive(b => b.Value = value); - } - - protected void TriggerDisabledChange(bool propagateToBindings = true) - { - DisabledChanged?.Invoke(disabled); - if (propagateToBindings) Bindings?.ForEachAlive(b => b.Disabled = disabled); - } - - /// - /// Unbind any events bound to and . - /// - public void UnbindEvents() - { - ValueChanged = null; - DisabledChanged = null; - } - - /// - /// Remove all bound s via or . - /// - public void UnbindBindings() - { - Bindings?.ForEachAlive(b => b.Unbind(this)); - Bindings?.Clear(); - } - - protected void Unbind(Bindable binding) => Bindings.Remove(binding.weakReference); - - /// - /// Calls and - /// - public void UnbindAll() - { - UnbindEvents(); - UnbindBindings(); - } - - public string Description { get; set; } - - public override string ToString() - { - return value?.ToString() ?? string.Empty; - } - - /// - /// Reset this bindable to its value and set to false. - /// - internal void Reset() - { - Value = Default; - Disabled = false; - } - - /// - /// Retrieve a new bindable instance weakly bound to the configuration backing. - /// If you are further binding to events of a bindable retrieved using this method, ensure to hold - /// a local reference. - /// - /// A weakly bound copy of the specified bindable. - public Bindable GetBoundCopy() - { - var copy = (Bindable)Activator.CreateInstance(GetType(), Value); - copy.BindTo(this); - return copy; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Globalization; +using osu.Framework.Lists; + +namespace osu.Framework.Configuration +{ + /// + /// A generic implementation of a + /// + /// The type of our stored . + public class Bindable : IBindable + { + private T value; + + /// + /// The default value of this bindable. Used when calling or querying . + /// + public T Default; + + private bool disabled; + + /// + /// Whether this bindable has been disabled. When disabled, attempting to change the will result in an . + /// + public bool Disabled + { + get { return disabled; } + set + { + if (disabled == value) return; + + disabled = value; + + TriggerDisabledChange(); + } + } + + /// + /// Check whether the current is equal to . + /// + public virtual bool IsDefault => Equals(value, Default); + + /// + /// Revert the current to the defined . + /// + public void SetDefault() => Value = Default; + + /// + /// An event which is raised when has changed (or manually via ). + /// + public event Action ValueChanged; + + /// + /// An event which is raised when 's state has changed (or manually via ). + /// + public event Action DisabledChanged; + + /// + /// The current value of this bindable. + /// + public virtual T Value + { + get { return value; } + set + { + if (EqualityComparer.Default.Equals(this.value, value)) return; + + if (Disabled) + throw new InvalidOperationException($"Can not set value to \"{value.ToString()}\" as bindable is disabled."); + + this.value = value; + + TriggerValueChange(); + } + } + + /// + /// Creates a new bindable instance. + /// + /// The initial value. + public Bindable(T value = default(T)) + { + this.value = value; + } + + public static implicit operator T(Bindable value) => value.Value; + + protected WeakList> Bindings; + + private WeakReference> weakReference => new WeakReference>(this); + + /// + /// Binds outselves to another bindable such that they receive bi-directional updates. + /// We will take on any value limitations of the bindable we bind width. + /// + /// The foreign bindable. This should always be the most permanent end of the bind (ie. a ConfigManager) + public virtual void BindTo(Bindable them) + { + Value = them.Value; + Disabled = them.Disabled; + Default = them.Default; + + AddWeakReference(them.weakReference); + them.AddWeakReference(weakReference); + } + + protected void AddWeakReference(WeakReference> weakReference) + { + if (Bindings == null) + Bindings = new WeakList>(); + + Bindings.Add(weakReference); + } + + /// + /// Parse an object into this instance. + /// An object deriving T can be parsed, or a string can be parsed if T is an enum type. + /// + /// The input which is to be parsed. + public virtual void Parse(object input) + { + switch (input) + { + case T t: + Value = t; + break; + case string s: + Value = typeof(T).IsEnum + ? (T)Enum.Parse(typeof(T), s) + : (T)Convert.ChangeType(s, typeof(T), CultureInfo.InvariantCulture); + break; + default: + throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}."); + } + } + + /// + /// Raise and once, without any changes actually occurring. + /// This does not propagate to any outward bound bindables. + /// + public virtual void TriggerChange() + { + TriggerValueChange(false); + TriggerDisabledChange(false); + } + + protected void TriggerValueChange(bool propagateToBindings = true) + { + ValueChanged?.Invoke(value); + if (propagateToBindings) Bindings?.ForEachAlive(b => b.Value = value); + } + + protected void TriggerDisabledChange(bool propagateToBindings = true) + { + DisabledChanged?.Invoke(disabled); + if (propagateToBindings) Bindings?.ForEachAlive(b => b.Disabled = disabled); + } + + /// + /// Unbind any events bound to and . + /// + public void UnbindEvents() + { + ValueChanged = null; + DisabledChanged = null; + } + + /// + /// Remove all bound s via or . + /// + public void UnbindBindings() + { + Bindings?.ForEachAlive(b => b.Unbind(this)); + Bindings?.Clear(); + } + + protected void Unbind(Bindable binding) => Bindings.Remove(binding.weakReference); + + /// + /// Calls and + /// + public void UnbindAll() + { + UnbindEvents(); + UnbindBindings(); + } + + public string Description { get; set; } + + public override string ToString() + { + return value?.ToString() ?? string.Empty; + } + + /// + /// Reset this bindable to its value and set to false. + /// + internal void Reset() + { + Value = Default; + Disabled = false; + } + + /// + /// Retrieve a new bindable instance weakly bound to the configuration backing. + /// If you are further binding to events of a bindable retrieved using this method, ensure to hold + /// a local reference. + /// + /// A weakly bound copy of the specified bindable. + public Bindable GetBoundCopy() + { + var copy = (Bindable)Activator.CreateInstance(GetType(), Value); + copy.BindTo(this); + return copy; + } + } +} diff --git a/osu.Framework/Configuration/BindableBool.cs b/osu.Framework/Configuration/BindableBool.cs index f2e3b959b..52460b266 100644 --- a/osu.Framework/Configuration/BindableBool.cs +++ b/osu.Framework/Configuration/BindableBool.cs @@ -1,31 +1,31 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Configuration -{ - public class BindableBool : Bindable - { - public BindableBool(bool value = false) - : base(value) - { - } - - public static implicit operator bool(BindableBool value) => value?.Value ?? throw new InvalidCastException($"Casting a null {nameof(BindableBool)} to a bool is likely a mistake"); - - public override string ToString() => Value.ToString(); - - public override void Parse(object input) - { - if (input.Equals("1")) - Value = true; - else if (input.Equals("0")) - Value = false; - else - base.Parse(input); - } - - public void Toggle() => Value = !Value; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Configuration +{ + public class BindableBool : Bindable + { + public BindableBool(bool value = false) + : base(value) + { + } + + public static implicit operator bool(BindableBool value) => value?.Value ?? throw new InvalidCastException($"Casting a null {nameof(BindableBool)} to a bool is likely a mistake"); + + public override string ToString() => Value.ToString(); + + public override void Parse(object input) + { + if (input.Equals("1")) + Value = true; + else if (input.Equals("0")) + Value = false; + else + base.Parse(input); + } + + public void Toggle() => Value = !Value; + } +} diff --git a/osu.Framework/Configuration/BindableDouble.cs b/osu.Framework/Configuration/BindableDouble.cs index 2a0e72332..8b0a837b6 100644 --- a/osu.Framework/Configuration/BindableDouble.cs +++ b/osu.Framework/Configuration/BindableDouble.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Globalization; - -namespace osu.Framework.Configuration -{ - public class BindableDouble : BindableNumber - { - public override bool IsDefault => Math.Abs(Value - Default) < Precision; - - protected override double DefaultMinValue => double.MinValue; - protected override double DefaultMaxValue => double.MaxValue; - protected override double DefaultPrecision => double.Epsilon; - - public BindableDouble(double value = 0) - : base(value) - { - } - - public override string ToString() => Value.ToString("0.0###", NumberFormatInfo.InvariantInfo); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Globalization; + +namespace osu.Framework.Configuration +{ + public class BindableDouble : BindableNumber + { + public override bool IsDefault => Math.Abs(Value - Default) < Precision; + + protected override double DefaultMinValue => double.MinValue; + protected override double DefaultMaxValue => double.MaxValue; + protected override double DefaultPrecision => double.Epsilon; + + public BindableDouble(double value = 0) + : base(value) + { + } + + public override string ToString() => Value.ToString("0.0###", NumberFormatInfo.InvariantInfo); + } +} diff --git a/osu.Framework/Configuration/BindableFloat.cs b/osu.Framework/Configuration/BindableFloat.cs index 17ef39d93..801910744 100644 --- a/osu.Framework/Configuration/BindableFloat.cs +++ b/osu.Framework/Configuration/BindableFloat.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Globalization; - -namespace osu.Framework.Configuration -{ - public class BindableFloat : BindableNumber - { - public override bool IsDefault => Math.Abs(Value - Default) < Precision; - - protected override float DefaultMinValue => float.MinValue; - protected override float DefaultMaxValue => float.MaxValue; - protected override float DefaultPrecision => float.Epsilon; - - public BindableFloat(float value = 0) - : base(value) - { - } - - public override string ToString() => Value.ToString("0.0###", NumberFormatInfo.InvariantInfo); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Globalization; + +namespace osu.Framework.Configuration +{ + public class BindableFloat : BindableNumber + { + public override bool IsDefault => Math.Abs(Value - Default) < Precision; + + protected override float DefaultMinValue => float.MinValue; + protected override float DefaultMaxValue => float.MaxValue; + protected override float DefaultPrecision => float.Epsilon; + + public BindableFloat(float value = 0) + : base(value) + { + } + + public override string ToString() => Value.ToString("0.0###", NumberFormatInfo.InvariantInfo); + } +} diff --git a/osu.Framework/Configuration/BindableInt.cs b/osu.Framework/Configuration/BindableInt.cs index 1335bb28a..1f5244ed9 100644 --- a/osu.Framework/Configuration/BindableInt.cs +++ b/osu.Framework/Configuration/BindableInt.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Globalization; - -namespace osu.Framework.Configuration -{ - public class BindableInt : BindableNumber - { - protected override int DefaultMinValue => int.MinValue; - protected override int DefaultMaxValue => int.MaxValue; - protected override int DefaultPrecision => 1; - - public BindableInt(int value = 0) - : base(value) - { - } - - public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Globalization; + +namespace osu.Framework.Configuration +{ + public class BindableInt : BindableNumber + { + protected override int DefaultMinValue => int.MinValue; + protected override int DefaultMaxValue => int.MaxValue; + protected override int DefaultPrecision => 1; + + public BindableInt(int value = 0) + : base(value) + { + } + + public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); + } +} diff --git a/osu.Framework/Configuration/BindableLong.cs b/osu.Framework/Configuration/BindableLong.cs index b01b0d184..86c045f4a 100644 --- a/osu.Framework/Configuration/BindableLong.cs +++ b/osu.Framework/Configuration/BindableLong.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Globalization; - -namespace osu.Framework.Configuration -{ - public class BindableLong : BindableNumber - { - protected override long DefaultMinValue => long.MinValue; - protected override long DefaultMaxValue => long.MaxValue; - protected override long DefaultPrecision => 1; - - public BindableLong(long value = 0) - : base(value) - { - } - - public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Globalization; + +namespace osu.Framework.Configuration +{ + public class BindableLong : BindableNumber + { + protected override long DefaultMinValue => long.MinValue; + protected override long DefaultMaxValue => long.MaxValue; + protected override long DefaultPrecision => 1; + + public BindableLong(long value = 0) + : base(value) + { + } + + public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); + } +} diff --git a/osu.Framework/Configuration/BindableNumber.cs b/osu.Framework/Configuration/BindableNumber.cs index f9ad92e2f..d817f3335 100644 --- a/osu.Framework/Configuration/BindableNumber.cs +++ b/osu.Framework/Configuration/BindableNumber.cs @@ -1,330 +1,330 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Globalization; -using OpenTK; - -namespace osu.Framework.Configuration -{ - public abstract class BindableNumber : Bindable - where T : struct, IComparable, IConvertible - { - static BindableNumber() - { - // check supported types against provided type argument. - var allowedTypes = new HashSet - { - typeof(sbyte), - typeof(byte), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(float), - typeof(double) - }; - - if (!allowedTypes.Contains(typeof(T))) - throw new ArgumentException( - $"{nameof(BindableNumber)} only accepts the primitive numeric types (except for {typeof(decimal).FullName}) as type arguments. You provided {typeof(T).FullName}."); - } - - /// - /// An event which is raised when has changed (or manually via ). - /// - public event Action PrecisionChanged; - - protected BindableNumber(T value = default(T)) - : base(value) - { - MinValue = DefaultMinValue; - MaxValue = DefaultMaxValue; - precision = DefaultPrecision; - } - - - private T precision; - - /// - /// The precision up to which the value of this bindable should be rounded. - /// - public T Precision - { - get => precision; - set - { - if (precision.Equals(value)) - return; - - if (Convert.ToDouble(value) <= 0) - throw new ArgumentOutOfRangeException(nameof(Precision), "Must be greater than 0."); - - precision = value; - - TriggerPrecisionChange(); - } - } - - public override T Value - { - get { return base.Value; } - set - { - if (Precision.CompareTo(DefaultPrecision) > 0) - { - double doubleValue = Convert.ToDouble(clamp(value, MinValue, MaxValue)); - doubleValue = Math.Round(doubleValue / Convert.ToDouble(Precision)) * Convert.ToDouble(Precision); - - // ReSharper disable once PossibleNullReferenceException - // https://youtrack.jetbrains.com/issue/RIDER-12652 - base.Value = (T)Convert.ChangeType(doubleValue, typeof(T), CultureInfo.InvariantCulture); - } - else - base.Value = clamp(value, MinValue, MaxValue); - } - } - - /// - /// The minimum value of this bindable. will never go below this value. - /// - public T MinValue { get; set; } - - /// - /// The maximim value of this bindable. will never go above this value. - /// - public T MaxValue { get; set; } - - /// - /// The default . This should be equal to the minimum value of type . - /// - protected abstract T DefaultMinValue { get; } - - /// - /// The default . This should be equal to the maximum value of type . - /// - protected abstract T DefaultMaxValue { get; } - - /// - /// The default . - /// - protected abstract T DefaultPrecision { get; } - - public override void TriggerChange() - { - base.TriggerChange(); - - TriggerPrecisionChange(false); - } - - protected void TriggerPrecisionChange(bool propagateToBindings = true) - { - PrecisionChanged?.Invoke(MinValue); - - if (!propagateToBindings) - return; - - Bindings?.ForEachAlive(b => - { - if (b is BindableNumber other) - other.Precision = Precision; - }); - } - - public override void BindTo(Bindable them) - { - if (them is BindableNumber other) - { - Precision = max(Precision, other.Precision); - MinValue = max(MinValue, other.MinValue); - MaxValue = min(MaxValue, other.MaxValue); - - if (MinValue.CompareTo(MaxValue) > 0) - throw new ArgumentOutOfRangeException( - $"Can not weld bindable longs with non-overlapping min/max-ranges. The ranges were [{MinValue} - {MaxValue}] and [{other.MinValue} - {other.MaxValue}].", nameof(them)); - } - - base.BindTo(them); - } - - /// - /// Whether this bindable has a user-defined range that is not the full range of the type. - /// - public bool HasDefinedRange => !MinValue.Equals(DefaultMinValue) || !MaxValue.Equals(DefaultMaxValue); - - public static implicit operator T(BindableNumber value) => value?.Value ?? throw new InvalidCastException($"Casting a null {nameof(BindableNumber)} to a {nameof(T)} is likely a mistake"); - - public bool IsInteger - { - get - { - switch (Type.GetTypeCode(typeof(T))) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.Int16: - case TypeCode.UInt32: - case TypeCode.Int32: - case TypeCode.UInt64: - case TypeCode.Int64: - return true; - default: - return false; - } - } - } - - public void Set(U val) where U : struct, - IComparable, IFormattable, IConvertible, IComparable, IEquatable - { - switch (Type.GetTypeCode(typeof(T))) - { - case TypeCode.Byte: - var byteBindable = this as BindableNumber; - if (byteBindable == null) throw new ArgumentNullException(nameof(byteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - byteBindable.Value = Convert.ToByte(val); - break; - case TypeCode.SByte: - var sbyteBindable = this as BindableNumber; - if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - sbyteBindable.Value = Convert.ToSByte(val); - break; - case TypeCode.UInt16: - var ushortBindable = this as BindableNumber; - if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - ushortBindable.Value = Convert.ToUInt16(val); - break; - case TypeCode.Int16: - var shortBindable = this as BindableNumber; - if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - shortBindable.Value = Convert.ToInt16(val); - break; - case TypeCode.UInt32: - var uintBindable = this as BindableNumber; - if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - uintBindable.Value = Convert.ToUInt32(val); - break; - case TypeCode.Int32: - var intBindable = this as BindableNumber; - if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - intBindable.Value = Convert.ToInt32(val); - break; - case TypeCode.UInt64: - var ulongBindable = this as BindableNumber; - if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - ulongBindable.Value = Convert.ToUInt64(val); - break; - case TypeCode.Int64: - var longBindable = this as BindableNumber; - if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - longBindable.Value = Convert.ToInt64(val); - break; - case TypeCode.Single: - var floatBindable = this as BindableNumber; - if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - floatBindable.Value = Convert.ToSingle(val); - break; - case TypeCode.Double: - var doubleBindable = this as BindableNumber; - if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - doubleBindable.Value = Convert.ToDouble(val); - break; - } - } - - public void Add(U val) where U : struct, - IComparable, IFormattable, IConvertible, IComparable, IEquatable - { - switch (Type.GetTypeCode(typeof(T))) - { - case TypeCode.Byte: - var byteBindable = this as BindableNumber; - if (byteBindable == null) throw new ArgumentNullException(nameof(byteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - byteBindable.Value += Convert.ToByte(val); - break; - case TypeCode.SByte: - var sbyteBindable = this as BindableNumber; - if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - sbyteBindable.Value += Convert.ToSByte(val); - break; - case TypeCode.UInt16: - var ushortBindable = this as BindableNumber; - if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - ushortBindable.Value += Convert.ToUInt16(val); - break; - case TypeCode.Int16: - var shortBindable = this as BindableNumber; - if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - shortBindable.Value += Convert.ToInt16(val); - break; - case TypeCode.UInt32: - var uintBindable = this as BindableNumber; - if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - uintBindable.Value += Convert.ToUInt32(val); - break; - case TypeCode.Int32: - var intBindable = this as BindableNumber; - if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - intBindable.Value += Convert.ToInt32(val); - break; - case TypeCode.UInt64: - var ulongBindable = this as BindableNumber; - if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - ulongBindable.Value += Convert.ToUInt64(val); - break; - case TypeCode.Int64: - var longBindable = this as BindableNumber; - if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - longBindable.Value += Convert.ToInt64(val); - break; - case TypeCode.Single: - var floatBindable = this as BindableNumber; - if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - floatBindable.Value += Convert.ToSingle(val); - break; - case TypeCode.Double: - var doubleBindable = this as BindableNumber; - if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); - doubleBindable.Value += Convert.ToDouble(val); - break; - } - } - - /// - /// Sets the value of the bindable to Min + (Max - Min) * amt - /// The proportional amount to set, ranging from 0 to 1. - /// If greater than 0, snap the final value to the closest multiple of this number. - /// - public void SetProportional(float amt, float snap = 0) - { - var min = Convert.ToDouble(MinValue); - var max = Convert.ToDouble(MaxValue); - var value = min + (max - min) * amt; - if (snap > 0) - { - var floor = Math.Floor(value / snap) * snap; - value = MathHelper.Clamp(value - floor < snap / 2f ? floor : floor + snap, min, max); - } - Set(value); - } - - private static T max(T value1, T value2) - { - var comparison = value1.CompareTo(value2); - return comparison > 0 ? value1 : value2; - } - - private static T min(T value1, T value2) - { - var comparison = value1.CompareTo(value2); - return comparison > 0 ? value2 : value1; - } - - private static T clamp(T value, T minValue, T maxValue) - => max(minValue, min(maxValue, value)); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Globalization; +using OpenTK; + +namespace osu.Framework.Configuration +{ + public abstract class BindableNumber : Bindable + where T : struct, IComparable, IConvertible + { + static BindableNumber() + { + // check supported types against provided type argument. + var allowedTypes = new HashSet + { + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double) + }; + + if (!allowedTypes.Contains(typeof(T))) + throw new ArgumentException( + $"{nameof(BindableNumber)} only accepts the primitive numeric types (except for {typeof(decimal).FullName}) as type arguments. You provided {typeof(T).FullName}."); + } + + /// + /// An event which is raised when has changed (or manually via ). + /// + public event Action PrecisionChanged; + + protected BindableNumber(T value = default(T)) + : base(value) + { + MinValue = DefaultMinValue; + MaxValue = DefaultMaxValue; + precision = DefaultPrecision; + } + + + private T precision; + + /// + /// The precision up to which the value of this bindable should be rounded. + /// + public T Precision + { + get => precision; + set + { + if (precision.Equals(value)) + return; + + if (Convert.ToDouble(value) <= 0) + throw new ArgumentOutOfRangeException(nameof(Precision), "Must be greater than 0."); + + precision = value; + + TriggerPrecisionChange(); + } + } + + public override T Value + { + get { return base.Value; } + set + { + if (Precision.CompareTo(DefaultPrecision) > 0) + { + double doubleValue = Convert.ToDouble(clamp(value, MinValue, MaxValue)); + doubleValue = Math.Round(doubleValue / Convert.ToDouble(Precision)) * Convert.ToDouble(Precision); + + // ReSharper disable once PossibleNullReferenceException + // https://youtrack.jetbrains.com/issue/RIDER-12652 + base.Value = (T)Convert.ChangeType(doubleValue, typeof(T), CultureInfo.InvariantCulture); + } + else + base.Value = clamp(value, MinValue, MaxValue); + } + } + + /// + /// The minimum value of this bindable. will never go below this value. + /// + public T MinValue { get; set; } + + /// + /// The maximim value of this bindable. will never go above this value. + /// + public T MaxValue { get; set; } + + /// + /// The default . This should be equal to the minimum value of type . + /// + protected abstract T DefaultMinValue { get; } + + /// + /// The default . This should be equal to the maximum value of type . + /// + protected abstract T DefaultMaxValue { get; } + + /// + /// The default . + /// + protected abstract T DefaultPrecision { get; } + + public override void TriggerChange() + { + base.TriggerChange(); + + TriggerPrecisionChange(false); + } + + protected void TriggerPrecisionChange(bool propagateToBindings = true) + { + PrecisionChanged?.Invoke(MinValue); + + if (!propagateToBindings) + return; + + Bindings?.ForEachAlive(b => + { + if (b is BindableNumber other) + other.Precision = Precision; + }); + } + + public override void BindTo(Bindable them) + { + if (them is BindableNumber other) + { + Precision = max(Precision, other.Precision); + MinValue = max(MinValue, other.MinValue); + MaxValue = min(MaxValue, other.MaxValue); + + if (MinValue.CompareTo(MaxValue) > 0) + throw new ArgumentOutOfRangeException( + $"Can not weld bindable longs with non-overlapping min/max-ranges. The ranges were [{MinValue} - {MaxValue}] and [{other.MinValue} - {other.MaxValue}].", nameof(them)); + } + + base.BindTo(them); + } + + /// + /// Whether this bindable has a user-defined range that is not the full range of the type. + /// + public bool HasDefinedRange => !MinValue.Equals(DefaultMinValue) || !MaxValue.Equals(DefaultMaxValue); + + public static implicit operator T(BindableNumber value) => value?.Value ?? throw new InvalidCastException($"Casting a null {nameof(BindableNumber)} to a {nameof(T)} is likely a mistake"); + + public bool IsInteger + { + get + { + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.Int16: + case TypeCode.UInt32: + case TypeCode.Int32: + case TypeCode.UInt64: + case TypeCode.Int64: + return true; + default: + return false; + } + } + } + + public void Set(U val) where U : struct, + IComparable, IFormattable, IConvertible, IComparable, IEquatable + { + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Byte: + var byteBindable = this as BindableNumber; + if (byteBindable == null) throw new ArgumentNullException(nameof(byteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + byteBindable.Value = Convert.ToByte(val); + break; + case TypeCode.SByte: + var sbyteBindable = this as BindableNumber; + if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + sbyteBindable.Value = Convert.ToSByte(val); + break; + case TypeCode.UInt16: + var ushortBindable = this as BindableNumber; + if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + ushortBindable.Value = Convert.ToUInt16(val); + break; + case TypeCode.Int16: + var shortBindable = this as BindableNumber; + if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + shortBindable.Value = Convert.ToInt16(val); + break; + case TypeCode.UInt32: + var uintBindable = this as BindableNumber; + if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + uintBindable.Value = Convert.ToUInt32(val); + break; + case TypeCode.Int32: + var intBindable = this as BindableNumber; + if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + intBindable.Value = Convert.ToInt32(val); + break; + case TypeCode.UInt64: + var ulongBindable = this as BindableNumber; + if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + ulongBindable.Value = Convert.ToUInt64(val); + break; + case TypeCode.Int64: + var longBindable = this as BindableNumber; + if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + longBindable.Value = Convert.ToInt64(val); + break; + case TypeCode.Single: + var floatBindable = this as BindableNumber; + if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + floatBindable.Value = Convert.ToSingle(val); + break; + case TypeCode.Double: + var doubleBindable = this as BindableNumber; + if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + doubleBindable.Value = Convert.ToDouble(val); + break; + } + } + + public void Add(U val) where U : struct, + IComparable, IFormattable, IConvertible, IComparable, IEquatable + { + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Byte: + var byteBindable = this as BindableNumber; + if (byteBindable == null) throw new ArgumentNullException(nameof(byteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + byteBindable.Value += Convert.ToByte(val); + break; + case TypeCode.SByte: + var sbyteBindable = this as BindableNumber; + if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + sbyteBindable.Value += Convert.ToSByte(val); + break; + case TypeCode.UInt16: + var ushortBindable = this as BindableNumber; + if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + ushortBindable.Value += Convert.ToUInt16(val); + break; + case TypeCode.Int16: + var shortBindable = this as BindableNumber; + if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + shortBindable.Value += Convert.ToInt16(val); + break; + case TypeCode.UInt32: + var uintBindable = this as BindableNumber; + if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + uintBindable.Value += Convert.ToUInt32(val); + break; + case TypeCode.Int32: + var intBindable = this as BindableNumber; + if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + intBindable.Value += Convert.ToInt32(val); + break; + case TypeCode.UInt64: + var ulongBindable = this as BindableNumber; + if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + ulongBindable.Value += Convert.ToUInt64(val); + break; + case TypeCode.Int64: + var longBindable = this as BindableNumber; + if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + longBindable.Value += Convert.ToInt64(val); + break; + case TypeCode.Single: + var floatBindable = this as BindableNumber; + if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + floatBindable.Value += Convert.ToSingle(val); + break; + case TypeCode.Double: + var doubleBindable = this as BindableNumber; + if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}."); + doubleBindable.Value += Convert.ToDouble(val); + break; + } + } + + /// + /// Sets the value of the bindable to Min + (Max - Min) * amt + /// The proportional amount to set, ranging from 0 to 1. + /// If greater than 0, snap the final value to the closest multiple of this number. + /// + public void SetProportional(float amt, float snap = 0) + { + var min = Convert.ToDouble(MinValue); + var max = Convert.ToDouble(MaxValue); + var value = min + (max - min) * amt; + if (snap > 0) + { + var floor = Math.Floor(value / snap) * snap; + value = MathHelper.Clamp(value - floor < snap / 2f ? floor : floor + snap, min, max); + } + Set(value); + } + + private static T max(T value1, T value2) + { + var comparison = value1.CompareTo(value2); + return comparison > 0 ? value1 : value2; + } + + private static T min(T value1, T value2) + { + var comparison = value1.CompareTo(value2); + return comparison > 0 ? value2 : value1; + } + + private static T clamp(T value, T minValue, T maxValue) + => max(minValue, min(maxValue, value)); + } +} diff --git a/osu.Framework/Configuration/ConfigManager.cs b/osu.Framework/Configuration/ConfigManager.cs index 57e48bc68..b40ab26da 100644 --- a/osu.Framework/Configuration/ConfigManager.cs +++ b/osu.Framework/Configuration/ConfigManager.cs @@ -1,239 +1,239 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Configuration.Tracking; - -namespace osu.Framework.Configuration -{ - public abstract class ConfigManager : ITrackableConfigManager, IDisposable - where T : struct - { - protected virtual bool AddMissingEntries => true; - - protected readonly Dictionary ConfigStore = new Dictionary(); - - protected virtual void InitialiseDefaults() - { - } - - public BindableDouble Set(T lookup, double value, double? min = null, double? max = null, double? precision = null) - { - BindableDouble bindable = GetOriginalBindable(lookup) as BindableDouble; - - if (bindable == null) - { - bindable = new BindableDouble(value); - AddBindable(lookup, bindable); - } - else - { - bindable.Value = value; - } - - bindable.Default = value; - if (min.HasValue) bindable.MinValue = min.Value; - if (max.HasValue) bindable.MaxValue = max.Value; - if (precision.HasValue) bindable.Precision = precision.Value; - - return bindable; - } - - public BindableFloat Set(T lookup, float value, float? min = null, float? max = null, float? precision = null) - { - BindableFloat bindable = GetOriginalBindable(lookup) as BindableFloat; - - if (bindable == null) - { - bindable = new BindableFloat(value); - AddBindable(lookup, bindable); - } - else - { - bindable.Value = value; - } - - bindable.Default = value; - if (min.HasValue) bindable.MinValue = min.Value; - if (max.HasValue) bindable.MaxValue = max.Value; - if (precision.HasValue) bindable.Precision = precision.Value; - - return bindable; - } - - public BindableInt Set(T lookup, int value, int? min = null, int? max = null) - { - BindableInt bindable = GetOriginalBindable(lookup) as BindableInt; - - if (bindable == null) - { - bindable = new BindableInt(value); - AddBindable(lookup, bindable); - } - else - { - bindable.Value = value; - } - - bindable.Default = value; - if (min.HasValue) bindable.MinValue = min.Value; - if (max.HasValue) bindable.MaxValue = max.Value; - - return bindable; - } - - public BindableBool Set(T lookup, bool value) - { - BindableBool bindable = GetOriginalBindable(lookup) as BindableBool; - - if (bindable == null) - { - bindable = new BindableBool(value); - AddBindable(lookup, bindable); - } - else - { - bindable.Value = value; - } - - bindable.Default = value; - - return bindable; - } - - public Bindable Set(T lookup, U value) - { - Bindable bindable = GetOriginalBindable(lookup); - - if (bindable == null) - bindable = set(lookup, value); - else - bindable.Value = value; - - bindable.Default = value; - - return bindable; - } - - protected virtual void AddBindable(T lookup, Bindable bindable) - { - ConfigStore[lookup] = bindable; - bindable.ValueChanged += _ => backgroundSave(); - } - - private Bindable set(T lookup, U value) - { - Bindable bindable = new Bindable(value); - AddBindable(lookup, bindable); - return bindable; - } - - public U Get(T lookup) => GetOriginalBindable(lookup).Value; - - protected Bindable GetOriginalBindable(T lookup) - { - if (ConfigStore.TryGetValue(lookup, out IBindable obj)) - return obj as Bindable; - - return null; - } - - /// - /// Retrieve a bindable. This will be a new instance weakly bound to the configuration backing. - /// If you are further binding to events of a bindable retrieved using this method, ensure to hold - /// a local reference. - /// - /// A weakly bound copy of the specified bindable. - public Bindable GetBindable(T lookup) => GetOriginalBindable(lookup)?.GetBoundCopy(); - - /// - /// Binds a local bindable with a configuration-backed bindable. - /// - public void BindWith(T lookup, Bindable bindable) => bindable.BindTo(GetOriginalBindable(lookup)); - - #region IDisposable Support - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (!isDisposed) - { - Save(); - isDisposed = true; - } - } - - ~ConfigManager() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - public virtual TrackedSettings CreateTrackedSettings() => null; - - public void LoadInto(TrackedSettings settings) => settings.LoadFrom(this); - - public class TrackedSetting : Tracking.TrackedSetting - { - /// - /// Constructs a new . - /// - /// The config setting to be tracked. - /// A function that generates the description for the setting, invoked every time the value changes. - public TrackedSetting(T setting, Func generateDescription) - : base(setting, generateDescription) - { - } - } - - private bool hasLoaded; - - public void Load() - { - PerformLoad(); - hasLoaded = true; - } - - private int lastSave; - - /// - /// Perform a save with debounce. - /// - private void backgroundSave() - { - var current = Interlocked.Increment(ref lastSave); - Task.Delay(100).ContinueWith(task => - { - if (current == lastSave) Save(); - }); - } - - private readonly object saveLock = new object(); - - public bool Save() - { - if (!hasLoaded) return false; - - lock (saveLock) - { - Interlocked.Increment(ref lastSave); - return PerformSave(); - } - } - - protected abstract void PerformLoad(); - - protected abstract bool PerformSave(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Configuration.Tracking; + +namespace osu.Framework.Configuration +{ + public abstract class ConfigManager : ITrackableConfigManager, IDisposable + where T : struct + { + protected virtual bool AddMissingEntries => true; + + protected readonly Dictionary ConfigStore = new Dictionary(); + + protected virtual void InitialiseDefaults() + { + } + + public BindableDouble Set(T lookup, double value, double? min = null, double? max = null, double? precision = null) + { + BindableDouble bindable = GetOriginalBindable(lookup) as BindableDouble; + + if (bindable == null) + { + bindable = new BindableDouble(value); + AddBindable(lookup, bindable); + } + else + { + bindable.Value = value; + } + + bindable.Default = value; + if (min.HasValue) bindable.MinValue = min.Value; + if (max.HasValue) bindable.MaxValue = max.Value; + if (precision.HasValue) bindable.Precision = precision.Value; + + return bindable; + } + + public BindableFloat Set(T lookup, float value, float? min = null, float? max = null, float? precision = null) + { + BindableFloat bindable = GetOriginalBindable(lookup) as BindableFloat; + + if (bindable == null) + { + bindable = new BindableFloat(value); + AddBindable(lookup, bindable); + } + else + { + bindable.Value = value; + } + + bindable.Default = value; + if (min.HasValue) bindable.MinValue = min.Value; + if (max.HasValue) bindable.MaxValue = max.Value; + if (precision.HasValue) bindable.Precision = precision.Value; + + return bindable; + } + + public BindableInt Set(T lookup, int value, int? min = null, int? max = null) + { + BindableInt bindable = GetOriginalBindable(lookup) as BindableInt; + + if (bindable == null) + { + bindable = new BindableInt(value); + AddBindable(lookup, bindable); + } + else + { + bindable.Value = value; + } + + bindable.Default = value; + if (min.HasValue) bindable.MinValue = min.Value; + if (max.HasValue) bindable.MaxValue = max.Value; + + return bindable; + } + + public BindableBool Set(T lookup, bool value) + { + BindableBool bindable = GetOriginalBindable(lookup) as BindableBool; + + if (bindable == null) + { + bindable = new BindableBool(value); + AddBindable(lookup, bindable); + } + else + { + bindable.Value = value; + } + + bindable.Default = value; + + return bindable; + } + + public Bindable Set(T lookup, U value) + { + Bindable bindable = GetOriginalBindable(lookup); + + if (bindable == null) + bindable = set(lookup, value); + else + bindable.Value = value; + + bindable.Default = value; + + return bindable; + } + + protected virtual void AddBindable(T lookup, Bindable bindable) + { + ConfigStore[lookup] = bindable; + bindable.ValueChanged += _ => backgroundSave(); + } + + private Bindable set(T lookup, U value) + { + Bindable bindable = new Bindable(value); + AddBindable(lookup, bindable); + return bindable; + } + + public U Get(T lookup) => GetOriginalBindable(lookup).Value; + + protected Bindable GetOriginalBindable(T lookup) + { + if (ConfigStore.TryGetValue(lookup, out IBindable obj)) + return obj as Bindable; + + return null; + } + + /// + /// Retrieve a bindable. This will be a new instance weakly bound to the configuration backing. + /// If you are further binding to events of a bindable retrieved using this method, ensure to hold + /// a local reference. + /// + /// A weakly bound copy of the specified bindable. + public Bindable GetBindable(T lookup) => GetOriginalBindable(lookup)?.GetBoundCopy(); + + /// + /// Binds a local bindable with a configuration-backed bindable. + /// + public void BindWith(T lookup, Bindable bindable) => bindable.BindTo(GetOriginalBindable(lookup)); + + #region IDisposable Support + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + Save(); + isDisposed = true; + } + } + + ~ConfigManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + public virtual TrackedSettings CreateTrackedSettings() => null; + + public void LoadInto(TrackedSettings settings) => settings.LoadFrom(this); + + public class TrackedSetting : Tracking.TrackedSetting + { + /// + /// Constructs a new . + /// + /// The config setting to be tracked. + /// A function that generates the description for the setting, invoked every time the value changes. + public TrackedSetting(T setting, Func generateDescription) + : base(setting, generateDescription) + { + } + } + + private bool hasLoaded; + + public void Load() + { + PerformLoad(); + hasLoaded = true; + } + + private int lastSave; + + /// + /// Perform a save with debounce. + /// + private void backgroundSave() + { + var current = Interlocked.Increment(ref lastSave); + Task.Delay(100).ContinueWith(task => + { + if (current == lastSave) Save(); + }); + } + + private readonly object saveLock = new object(); + + public bool Save() + { + if (!hasLoaded) return false; + + lock (saveLock) + { + Interlocked.Increment(ref lastSave); + return PerformSave(); + } + } + + protected abstract void PerformLoad(); + + protected abstract bool PerformSave(); + } +} diff --git a/osu.Framework/Configuration/FrameSync.cs b/osu.Framework/Configuration/FrameSync.cs index 44fd04d77..3097c63ef 100644 --- a/osu.Framework/Configuration/FrameSync.cs +++ b/osu.Framework/Configuration/FrameSync.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.ComponentModel; - -namespace osu.Framework.Configuration -{ - public enum FrameSync - { - VSync, - [Description("2x refresh rate")] - Limit2x, - [Description("4x refresh rate")] - Limit4x, - [Description("8x refresh rate")] - Limit8x, - [Description("Unlimited")] - Unlimited, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.ComponentModel; + +namespace osu.Framework.Configuration +{ + public enum FrameSync + { + VSync, + [Description("2x refresh rate")] + Limit2x, + [Description("4x refresh rate")] + Limit4x, + [Description("8x refresh rate")] + Limit8x, + [Description("Unlimited")] + Unlimited, + } +} diff --git a/osu.Framework/Configuration/FrameworkConfigManager.cs b/osu.Framework/Configuration/FrameworkConfigManager.cs index 8a6cb40fa..c70537eda 100644 --- a/osu.Framework/Configuration/FrameworkConfigManager.cs +++ b/osu.Framework/Configuration/FrameworkConfigManager.cs @@ -1,99 +1,99 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration.Tracking; -using osu.Framework.Extensions; -using osu.Framework.Input; -using osu.Framework.Platform; - -namespace osu.Framework.Configuration -{ - public class FrameworkConfigManager : IniConfigManager - { - protected override string Filename => @"framework.ini"; - - protected override void InitialiseDefaults() - { - Set(FrameworkSetting.ShowLogOverlay, false); - - Set(FrameworkSetting.Width, 1366, 640); - Set(FrameworkSetting.Height, 768, 480); - Set(FrameworkSetting.ConfineMouseMode, ConfineMouseMode.Fullscreen); - Set(FrameworkSetting.MapAbsoluteInputToWindow, false); - Set(FrameworkSetting.WindowedPositionX, 0.5, -0.1, 1.1); - Set(FrameworkSetting.WindowedPositionY, 0.5, -0.1, 1.1); - Set(FrameworkSetting.AudioDevice, string.Empty); - Set(FrameworkSetting.VolumeUniversal, 1.0, 0.0, 1.0, 0.01); - Set(FrameworkSetting.VolumeMusic, 1.0, 0.0, 1.0, 0.01); - Set(FrameworkSetting.VolumeEffect, 1.0, 0.0, 1.0, 0.01); - Set(FrameworkSetting.WidthFullscreen, 9999, 320, 9999); - Set(FrameworkSetting.HeightFullscreen, 9999, 240, 9999); - Set(FrameworkSetting.Letterboxing, true); - Set(FrameworkSetting.LetterboxPositionX, 0.0, -1.0, 1.0, 0.01); - Set(FrameworkSetting.LetterboxPositionY, 0.0, -1.0, 1.0, 0.01); - Set(FrameworkSetting.FrameSync, FrameSync.Limit2x); - Set(FrameworkSetting.WindowMode, WindowMode.Windowed); - Set(FrameworkSetting.ShowUnicode, false); - Set(FrameworkSetting.ActiveInputHandlers, string.Empty); - Set(FrameworkSetting.CursorSensitivity, 1.0, 0.1, 6, 0.01); - Set(FrameworkSetting.Locale, string.Empty); - Set(FrameworkSetting.PerformanceLogging, false); - } - - public FrameworkConfigManager(Storage storage) - : base(storage) - { - } - - public override TrackedSettings CreateTrackedSettings() => new TrackedSettings - { - new TrackedSetting(FrameworkSetting.FrameSync, v => new SettingDescription(v, "Frame Limiter", v.GetDescription(), "Ctrl+F7")), - new TrackedSetting(FrameworkSetting.AudioDevice, v => new SettingDescription(v, "Audio Device", string.IsNullOrEmpty(v) ? "Default" : v, v)), - new TrackedSetting(FrameworkSetting.ShowLogOverlay, v => new SettingDescription(v, "Debug Logs", v ? "visible" : "hidden", "Ctrl+F10")), - new TrackedSetting(FrameworkSetting.Width, v => createResolutionDescription()), - new TrackedSetting(FrameworkSetting.Height, v => createResolutionDescription()), - new TrackedSetting(FrameworkSetting.CursorSensitivity, v => new SettingDescription(v, "Cursor Sensitivity", v.ToString(@"0.##x"), "Ctrl+Alt+R to reset")), - new TrackedSetting(FrameworkSetting.ActiveInputHandlers, v => - { - bool raw = v.Contains("Raw"); - return new SettingDescription(raw, "Raw Input", raw ? "enabled" : "disabled", "Ctrl+Alt+R to reset"); - }), - new TrackedSetting(FrameworkSetting.WindowMode, v => new SettingDescription(v, "Screen Mode", v.ToString(), "Alt+Enter")) - }; - - private SettingDescription createResolutionDescription() => new SettingDescription(null, "Screen Resolution", Get(FrameworkSetting.Width) + "x" + Get(FrameworkSetting.Height)); - } - - public enum FrameworkSetting - { - ShowLogOverlay, - - AudioDevice, - VolumeUniversal, - VolumeEffect, - VolumeMusic, - - Width, - Height, - WindowedPositionX, - WindowedPositionY, - - HeightFullscreen, - WidthFullscreen, - - WindowMode, - ConfineMouseMode, - Letterboxing, - LetterboxPositionX, - LetterboxPositionY, - FrameSync, - - ShowUnicode, - Locale, - ActiveInputHandlers, - CursorSensitivity, - MapAbsoluteInputToWindow, - - PerformanceLogging - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration.Tracking; +using osu.Framework.Extensions; +using osu.Framework.Input; +using osu.Framework.Platform; + +namespace osu.Framework.Configuration +{ + public class FrameworkConfigManager : IniConfigManager + { + protected override string Filename => @"framework.ini"; + + protected override void InitialiseDefaults() + { + Set(FrameworkSetting.ShowLogOverlay, false); + + Set(FrameworkSetting.Width, 1366, 640); + Set(FrameworkSetting.Height, 768, 480); + Set(FrameworkSetting.ConfineMouseMode, ConfineMouseMode.Fullscreen); + Set(FrameworkSetting.MapAbsoluteInputToWindow, false); + Set(FrameworkSetting.WindowedPositionX, 0.5, -0.1, 1.1); + Set(FrameworkSetting.WindowedPositionY, 0.5, -0.1, 1.1); + Set(FrameworkSetting.AudioDevice, string.Empty); + Set(FrameworkSetting.VolumeUniversal, 1.0, 0.0, 1.0, 0.01); + Set(FrameworkSetting.VolumeMusic, 1.0, 0.0, 1.0, 0.01); + Set(FrameworkSetting.VolumeEffect, 1.0, 0.0, 1.0, 0.01); + Set(FrameworkSetting.WidthFullscreen, 9999, 320, 9999); + Set(FrameworkSetting.HeightFullscreen, 9999, 240, 9999); + Set(FrameworkSetting.Letterboxing, true); + Set(FrameworkSetting.LetterboxPositionX, 0.0, -1.0, 1.0, 0.01); + Set(FrameworkSetting.LetterboxPositionY, 0.0, -1.0, 1.0, 0.01); + Set(FrameworkSetting.FrameSync, FrameSync.Limit2x); + Set(FrameworkSetting.WindowMode, WindowMode.Windowed); + Set(FrameworkSetting.ShowUnicode, false); + Set(FrameworkSetting.ActiveInputHandlers, string.Empty); + Set(FrameworkSetting.CursorSensitivity, 1.0, 0.1, 6, 0.01); + Set(FrameworkSetting.Locale, string.Empty); + Set(FrameworkSetting.PerformanceLogging, false); + } + + public FrameworkConfigManager(Storage storage) + : base(storage) + { + } + + public override TrackedSettings CreateTrackedSettings() => new TrackedSettings + { + new TrackedSetting(FrameworkSetting.FrameSync, v => new SettingDescription(v, "Frame Limiter", v.GetDescription(), "Ctrl+F7")), + new TrackedSetting(FrameworkSetting.AudioDevice, v => new SettingDescription(v, "Audio Device", string.IsNullOrEmpty(v) ? "Default" : v, v)), + new TrackedSetting(FrameworkSetting.ShowLogOverlay, v => new SettingDescription(v, "Debug Logs", v ? "visible" : "hidden", "Ctrl+F10")), + new TrackedSetting(FrameworkSetting.Width, v => createResolutionDescription()), + new TrackedSetting(FrameworkSetting.Height, v => createResolutionDescription()), + new TrackedSetting(FrameworkSetting.CursorSensitivity, v => new SettingDescription(v, "Cursor Sensitivity", v.ToString(@"0.##x"), "Ctrl+Alt+R to reset")), + new TrackedSetting(FrameworkSetting.ActiveInputHandlers, v => + { + bool raw = v.Contains("Raw"); + return new SettingDescription(raw, "Raw Input", raw ? "enabled" : "disabled", "Ctrl+Alt+R to reset"); + }), + new TrackedSetting(FrameworkSetting.WindowMode, v => new SettingDescription(v, "Screen Mode", v.ToString(), "Alt+Enter")) + }; + + private SettingDescription createResolutionDescription() => new SettingDescription(null, "Screen Resolution", Get(FrameworkSetting.Width) + "x" + Get(FrameworkSetting.Height)); + } + + public enum FrameworkSetting + { + ShowLogOverlay, + + AudioDevice, + VolumeUniversal, + VolumeEffect, + VolumeMusic, + + Width, + Height, + WindowedPositionX, + WindowedPositionY, + + HeightFullscreen, + WidthFullscreen, + + WindowMode, + ConfineMouseMode, + Letterboxing, + LetterboxPositionX, + LetterboxPositionY, + FrameSync, + + ShowUnicode, + Locale, + ActiveInputHandlers, + CursorSensitivity, + MapAbsoluteInputToWindow, + + PerformanceLogging + } +} diff --git a/osu.Framework/Configuration/FrameworkDebugConfig.cs b/osu.Framework/Configuration/FrameworkDebugConfig.cs index 65fbcc01b..58a133f21 100644 --- a/osu.Framework/Configuration/FrameworkDebugConfig.cs +++ b/osu.Framework/Configuration/FrameworkDebugConfig.cs @@ -1,32 +1,32 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Runtime; -using osu.Framework.Caching; - -namespace osu.Framework.Configuration -{ - public class FrameworkDebugConfigManager : IniConfigManager - { - protected override string Filename => null; - - public FrameworkDebugConfigManager() - : base(null) - { - } - - protected override void InitialiseDefaults() - { - base.InitialiseDefaults(); - - Set(DebugSetting.ActiveGCMode, GCLatencyMode.SustainedLowLatency); - Set(DebugSetting.BypassCaching, false).ValueChanged += delegate { StaticCached.BypassCache = Get(DebugSetting.BypassCaching); }; - } - } - - public enum DebugSetting - { - ActiveGCMode, - BypassCaching - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Runtime; +using osu.Framework.Caching; + +namespace osu.Framework.Configuration +{ + public class FrameworkDebugConfigManager : IniConfigManager + { + protected override string Filename => null; + + public FrameworkDebugConfigManager() + : base(null) + { + } + + protected override void InitialiseDefaults() + { + base.InitialiseDefaults(); + + Set(DebugSetting.ActiveGCMode, GCLatencyMode.SustainedLowLatency); + Set(DebugSetting.BypassCaching, false).ValueChanged += delegate { StaticCached.BypassCache = Get(DebugSetting.BypassCaching); }; + } + } + + public enum DebugSetting + { + ActiveGCMode, + BypassCaching + } +} diff --git a/osu.Framework/Configuration/IBindable.cs b/osu.Framework/Configuration/IBindable.cs index 95c1d382b..9b0f46b51 100644 --- a/osu.Framework/Configuration/IBindable.cs +++ b/osu.Framework/Configuration/IBindable.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration -{ - /// - /// An interface which can be bound to in order to watch for (and react to) value changes. - /// - public interface IBindable : IParseable - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration +{ + /// + /// An interface which can be bound to in order to watch for (and react to) value changes. + /// + public interface IBindable : IParseable + { + } +} diff --git a/osu.Framework/Configuration/IConfigManager.cs b/osu.Framework/Configuration/IConfigManager.cs index 41793b299..490b9889b 100644 --- a/osu.Framework/Configuration/IConfigManager.cs +++ b/osu.Framework/Configuration/IConfigManager.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration -{ - public interface IConfigManager - { - /// - /// Loads this config. - /// - void Load(); - - /// - /// Saves this config. - /// - /// Whether the operation succeeded. - bool Save(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration +{ + public interface IConfigManager + { + /// + /// Loads this config. + /// + void Load(); + + /// + /// Saves this config. + /// + /// Whether the operation succeeded. + bool Save(); + } +} diff --git a/osu.Framework/Configuration/IParseable.cs b/osu.Framework/Configuration/IParseable.cs index 9f96efc2b..376d99322 100644 --- a/osu.Framework/Configuration/IParseable.cs +++ b/osu.Framework/Configuration/IParseable.cs @@ -1,17 +1,17 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration -{ - /// - /// Represents a class which can be parsed from an arbitrary object. - /// - public interface IParseable - { - /// - /// Parse an input into this instance. - /// - /// The input which is to be parsed. - void Parse(object input); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration +{ + /// + /// Represents a class which can be parsed from an arbitrary object. + /// + public interface IParseable + { + /// + /// Parse an input into this instance. + /// + /// The input which is to be parsed. + void Parse(object input); + } +} diff --git a/osu.Framework/Configuration/IniConfigManager.cs b/osu.Framework/Configuration/IniConfigManager.cs index 61841a16b..fa9410a01 100644 --- a/osu.Framework/Configuration/IniConfigManager.cs +++ b/osu.Framework/Configuration/IniConfigManager.cs @@ -1,91 +1,91 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using osu.Framework.Logging; -using osu.Framework.Platform; - -namespace osu.Framework.Configuration -{ - public class IniConfigManager : ConfigManager - where T : struct - { - /// - /// The backing file used to store the config. Null means no persistent storage. - /// - protected virtual string Filename => @"game.ini"; - - private readonly Storage storage; - - public IniConfigManager(Storage storage) - { - this.storage = storage; - - InitialiseDefaults(); - Load(); - } - - protected override void PerformLoad() - { - if (string.IsNullOrEmpty(Filename)) return; - - using (var stream = storage.GetStream(Filename)) - { - if (stream == null) - return; - - using (var reader = new StreamReader(stream)) - { - string line; - - while ((line = reader.ReadLine()) != null) - { - int equalsIndex = line.IndexOf('='); - - if (line.Length == 0 || line[0] == '#' || equalsIndex < 0) continue; - - string key = line.Substring(0, equalsIndex).Trim(); - string val = line.Remove(0, equalsIndex + 1).Trim(); - - if (!Enum.TryParse(key, out T lookup)) - continue; - - if (ConfigStore.TryGetValue(lookup, out IBindable b)) - try - { - b.Parse(val); - } - catch (Exception e) - { - Logger.Log($@"Unable to parse config key {lookup}: {e}", LoggingTarget.Runtime, LogLevel.Important); - } - else if (AddMissingEntries) - Set(lookup, val); - } - } - } - } - - protected override bool PerformSave() - { - if (string.IsNullOrEmpty(Filename)) return false; - - try - { - using (var stream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create)) - using (var w = new StreamWriter(stream)) - { - foreach (var p in ConfigStore) - w.WriteLine(@"{0} = {1}", p.Key, p.Value); - } - } - catch - { - return false; - } - - return true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using osu.Framework.Logging; +using osu.Framework.Platform; + +namespace osu.Framework.Configuration +{ + public class IniConfigManager : ConfigManager + where T : struct + { + /// + /// The backing file used to store the config. Null means no persistent storage. + /// + protected virtual string Filename => @"game.ini"; + + private readonly Storage storage; + + public IniConfigManager(Storage storage) + { + this.storage = storage; + + InitialiseDefaults(); + Load(); + } + + protected override void PerformLoad() + { + if (string.IsNullOrEmpty(Filename)) return; + + using (var stream = storage.GetStream(Filename)) + { + if (stream == null) + return; + + using (var reader = new StreamReader(stream)) + { + string line; + + while ((line = reader.ReadLine()) != null) + { + int equalsIndex = line.IndexOf('='); + + if (line.Length == 0 || line[0] == '#' || equalsIndex < 0) continue; + + string key = line.Substring(0, equalsIndex).Trim(); + string val = line.Remove(0, equalsIndex + 1).Trim(); + + if (!Enum.TryParse(key, out T lookup)) + continue; + + if (ConfigStore.TryGetValue(lookup, out IBindable b)) + try + { + b.Parse(val); + } + catch (Exception e) + { + Logger.Log($@"Unable to parse config key {lookup}: {e}", LoggingTarget.Runtime, LogLevel.Important); + } + else if (AddMissingEntries) + Set(lookup, val); + } + } + } + } + + protected override bool PerformSave() + { + if (string.IsNullOrEmpty(Filename)) return false; + + try + { + using (var stream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create)) + using (var w = new StreamWriter(stream)) + { + foreach (var p in ConfigStore) + w.WriteLine(@"{0} = {1}", p.Key, p.Value); + } + } + catch + { + return false; + } + + return true; + } + } +} diff --git a/osu.Framework/Configuration/NonNullableBindable.cs b/osu.Framework/Configuration/NonNullableBindable.cs index 4760999ed..a6555139f 100644 --- a/osu.Framework/Configuration/NonNullableBindable.cs +++ b/osu.Framework/Configuration/NonNullableBindable.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Configuration -{ - public class NonNullableBindable : Bindable - { - public NonNullableBindable(T defaultValue) - { - if (defaultValue == null) - throw new ArgumentNullException(nameof(defaultValue)); - - Value = Default = defaultValue; - } - public override T Value - { - get - { - return base.Value; - } - - set - { - if (value == null) - throw new ArgumentNullException(nameof(Value), $"Cannot set {nameof(Value)} of a {nameof(NonNullableBindable)} to null."); - - base.Value = value; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Configuration +{ + public class NonNullableBindable : Bindable + { + public NonNullableBindable(T defaultValue) + { + if (defaultValue == null) + throw new ArgumentNullException(nameof(defaultValue)); + + Value = Default = defaultValue; + } + public override T Value + { + get + { + return base.Value; + } + + set + { + if (value == null) + throw new ArgumentNullException(nameof(Value), $"Cannot set {nameof(Value)} of a {nameof(NonNullableBindable)} to null."); + + base.Value = value; + } + } + } +} diff --git a/osu.Framework/Configuration/Tracking/ITrackableConfigManager.cs b/osu.Framework/Configuration/Tracking/ITrackableConfigManager.cs index 23d2e006a..72435451a 100644 --- a/osu.Framework/Configuration/Tracking/ITrackableConfigManager.cs +++ b/osu.Framework/Configuration/Tracking/ITrackableConfigManager.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration.Tracking -{ - /// - /// An that provides a way to track its config settings. - /// - public interface ITrackableConfigManager : IConfigManager - { - /// - /// Retrieves all the settings of this that are to be tracked for changes. - /// - /// A list of . - TrackedSettings CreateTrackedSettings(); - - /// - /// Loads s into . - /// - /// The settings to load into. - void LoadInto(TrackedSettings settings); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration.Tracking +{ + /// + /// An that provides a way to track its config settings. + /// + public interface ITrackableConfigManager : IConfigManager + { + /// + /// Retrieves all the settings of this that are to be tracked for changes. + /// + /// A list of . + TrackedSettings CreateTrackedSettings(); + + /// + /// Loads s into . + /// + /// The settings to load into. + void LoadInto(TrackedSettings settings); + } +} diff --git a/osu.Framework/Configuration/Tracking/ITrackedSetting.cs b/osu.Framework/Configuration/Tracking/ITrackedSetting.cs index 935ee9c8a..7d80e4085 100644 --- a/osu.Framework/Configuration/Tracking/ITrackedSetting.cs +++ b/osu.Framework/Configuration/Tracking/ITrackedSetting.cs @@ -1,30 +1,30 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Configuration.Tracking -{ - /// - /// A singular tracked setting. - /// - public interface ITrackedSetting - { - /// - /// Invoked when this setting has changed. - /// - event Action SettingChanged; - - /// - /// Loads a into this tracked setting, binding to . - /// - /// The to load from. - void LoadFrom(ConfigManager configManager) - where T : struct; - - /// - /// Unloads the from this tracked setting, unbinding from . - /// - void Unload(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Configuration.Tracking +{ + /// + /// A singular tracked setting. + /// + public interface ITrackedSetting + { + /// + /// Invoked when this setting has changed. + /// + event Action SettingChanged; + + /// + /// Loads a into this tracked setting, binding to . + /// + /// The to load from. + void LoadFrom(ConfigManager configManager) + where T : struct; + + /// + /// Unloads the from this tracked setting, unbinding from . + /// + void Unload(); + } +} diff --git a/osu.Framework/Configuration/Tracking/SettingDescription.cs b/osu.Framework/Configuration/Tracking/SettingDescription.cs index 520f412be..642b90517 100644 --- a/osu.Framework/Configuration/Tracking/SettingDescription.cs +++ b/osu.Framework/Configuration/Tracking/SettingDescription.cs @@ -1,46 +1,46 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration.Tracking -{ - /// - /// Contains information that can be displayed when tracked settings change. - /// - public class SettingDescription - { - /// - /// The raw setting value. - /// - public readonly object RawValue; - - /// - /// The readable setting name. - /// - public readonly string Name; - - /// - /// The readable setting value. - /// - public readonly string Value; - - /// - /// The shortcut keys that cause this setting to change. - /// - public readonly string Shortcut; - - /// - /// Constructs a new . - /// - /// The raw setting value. - /// The readable setting name. - /// The readable setting value. - /// The shortcut keys that cause this setting to change. - public SettingDescription(object rawValue, string name, string value, string shortcut = @"") - { - RawValue = rawValue; - Name = name; - Value = value; - Shortcut = shortcut; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration.Tracking +{ + /// + /// Contains information that can be displayed when tracked settings change. + /// + public class SettingDescription + { + /// + /// The raw setting value. + /// + public readonly object RawValue; + + /// + /// The readable setting name. + /// + public readonly string Name; + + /// + /// The readable setting value. + /// + public readonly string Value; + + /// + /// The shortcut keys that cause this setting to change. + /// + public readonly string Shortcut; + + /// + /// Constructs a new . + /// + /// The raw setting value. + /// The readable setting name. + /// The readable setting value. + /// The shortcut keys that cause this setting to change. + public SettingDescription(object rawValue, string name, string value, string shortcut = @"") + { + RawValue = rawValue; + Name = name; + Value = value; + Shortcut = shortcut; + } + } +} diff --git a/osu.Framework/Configuration/Tracking/TrackedSetting.cs b/osu.Framework/Configuration/Tracking/TrackedSetting.cs index 9d3584c5a..cb01bf138 100644 --- a/osu.Framework/Configuration/Tracking/TrackedSetting.cs +++ b/osu.Framework/Configuration/Tracking/TrackedSetting.cs @@ -1,46 +1,46 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Configuration.Tracking -{ - /// - /// A singular tracked setting. - /// - /// The type of the tracked value. - public abstract class TrackedSetting : ITrackedSetting - { - public event Action SettingChanged; - - private readonly object setting; - private readonly Func generateDescription; - - private Bindable bindable; - - /// - /// Constructs a new . - /// - /// The config setting to be tracked. - /// A function that generates the description for the setting, invoked every time the value changes. - protected TrackedSetting(object setting, Func generateDescription) - { - this.setting = setting; - this.generateDescription = generateDescription; - } - - public void LoadFrom(ConfigManager configManager) - where T : struct - { - bindable = configManager.GetBindable((T)setting); - bindable.ValueChanged += displaySetting; - } - - public void Unload() - { - bindable.ValueChanged -= displaySetting; - } - - private void displaySetting(U value) => SettingChanged?.Invoke(generateDescription(value)); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Configuration.Tracking +{ + /// + /// A singular tracked setting. + /// + /// The type of the tracked value. + public abstract class TrackedSetting : ITrackedSetting + { + public event Action SettingChanged; + + private readonly object setting; + private readonly Func generateDescription; + + private Bindable bindable; + + /// + /// Constructs a new . + /// + /// The config setting to be tracked. + /// A function that generates the description for the setting, invoked every time the value changes. + protected TrackedSetting(object setting, Func generateDescription) + { + this.setting = setting; + this.generateDescription = generateDescription; + } + + public void LoadFrom(ConfigManager configManager) + where T : struct + { + bindable = configManager.GetBindable((T)setting); + bindable.ValueChanged += displaySetting; + } + + public void Unload() + { + bindable.ValueChanged -= displaySetting; + } + + private void displaySetting(U value) => SettingChanged?.Invoke(generateDescription(value)); + } +} diff --git a/osu.Framework/Configuration/Tracking/TrackedSettings.cs b/osu.Framework/Configuration/Tracking/TrackedSettings.cs index e6b24cfab..f7c0e75b0 100644 --- a/osu.Framework/Configuration/Tracking/TrackedSettings.cs +++ b/osu.Framework/Configuration/Tracking/TrackedSettings.cs @@ -1,29 +1,29 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Configuration.Tracking -{ - public class TrackedSettings : List - { - public event Action SettingChanged; - - public void LoadFrom(ConfigManager configManager) - where T : struct - { - foreach (var value in this) - { - value.LoadFrom(configManager); - value.SettingChanged += d => SettingChanged?.Invoke(d); - } - } - - public void Unload() - { - foreach (var value in this) - value.Unload(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Configuration.Tracking +{ + public class TrackedSettings : List + { + public event Action SettingChanged; + + public void LoadFrom(ConfigManager configManager) + where T : struct + { + foreach (var value in this) + { + value.LoadFrom(configManager); + value.SettingChanged += d => SettingChanged?.Invoke(d); + } + } + + public void Unload() + { + foreach (var value in this) + value.Unload(); + } + } +} diff --git a/osu.Framework/Configuration/WindowMode.cs b/osu.Framework/Configuration/WindowMode.cs index f4292f184..83e4b77ef 100644 --- a/osu.Framework/Configuration/WindowMode.cs +++ b/osu.Framework/Configuration/WindowMode.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Configuration -{ - public enum WindowMode - { - Windowed = 0, - Borderless = 1, - Fullscreen = 2 - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Configuration +{ + public enum WindowMode + { + Windowed = 0, + Borderless = 1, + Fullscreen = 2 + } +} diff --git a/osu.Framework/Development/DebugUtils.cs b/osu.Framework/Development/DebugUtils.cs index 7f187dfb7..f67d099bf 100644 --- a/osu.Framework/Development/DebugUtils.cs +++ b/osu.Framework/Development/DebugUtils.cs @@ -1,38 +1,38 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using System.Linq; - -namespace osu.Framework.Development -{ - public static class DebugUtils - { - public static bool IsDebug - { - get - { - // ReSharper disable once RedundantAssignment - bool isDebug = false; - // Debug.Assert conditions are only evaluated in debug mode - System.Diagnostics.Debug.Assert(isDebug = true); - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - return isDebug; - } - } - - /// - /// Find the containing solution path. - /// - /// An absolute path containing the first parent .sln file. Null if no such file exists in any parent. - public static string GetSolutionPath() - { - var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); - while (!Directory.GetFiles(di.FullName, "*.sln").Any() && di.Parent != null) - di = di.Parent; - - return di?.FullName; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using System.Linq; + +namespace osu.Framework.Development +{ + public static class DebugUtils + { + public static bool IsDebug + { + get + { + // ReSharper disable once RedundantAssignment + bool isDebug = false; + // Debug.Assert conditions are only evaluated in debug mode + System.Diagnostics.Debug.Assert(isDebug = true); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + return isDebug; + } + } + + /// + /// Find the containing solution path. + /// + /// An absolute path containing the first parent .sln file. Null if no such file exists in any parent. + public static string GetSolutionPath() + { + var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); + while (!Directory.GetFiles(di.FullName, "*.sln").Any() && di.Parent != null) + di = di.Parent; + + return di?.FullName; + } + } +} diff --git a/osu.Framework/Development/ThreadSafety.cs b/osu.Framework/Development/ThreadSafety.cs index 075238330..8403f2e79 100644 --- a/osu.Framework/Development/ThreadSafety.cs +++ b/osu.Framework/Development/ThreadSafety.cs @@ -1,45 +1,45 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Diagnostics; -using System.Threading; -using osu.Framework.Threading; - -namespace osu.Framework.Development -{ - internal static class ThreadSafety - { - [Conditional("DEBUG")] - internal static void EnsureUpdateThread() - { - //This check is very intrusive on performance, so let's only run when a debugger is actually attached. - if (!Debugger.IsAttached) return; - - Debug.Assert(IsUpdateThread); - } - - [Conditional("DEBUG")] - internal static void EnsureNotUpdateThread() - { - //This check is very intrusive on performance, so let's only run when a debugger is actually attached. - if (!Debugger.IsAttached) return; - - Debug.Assert(!IsUpdateThread); - } - - [Conditional("DEBUG")] - internal static void EnsureDrawThread() - { - //This check is very intrusive on performance, so let's only run when a debugger is actually attached. - if (!Debugger.IsAttached) return; - - Debug.Assert(IsDrawThread); - } - - public static bool IsUpdateThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Update"); - - public static bool IsDrawThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Draw"); - - public static bool IsAudioThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Audio"); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Diagnostics; +using System.Threading; +using osu.Framework.Threading; + +namespace osu.Framework.Development +{ + internal static class ThreadSafety + { + [Conditional("DEBUG")] + internal static void EnsureUpdateThread() + { + //This check is very intrusive on performance, so let's only run when a debugger is actually attached. + if (!Debugger.IsAttached) return; + + Debug.Assert(IsUpdateThread); + } + + [Conditional("DEBUG")] + internal static void EnsureNotUpdateThread() + { + //This check is very intrusive on performance, so let's only run when a debugger is actually attached. + if (!Debugger.IsAttached) return; + + Debug.Assert(!IsUpdateThread); + } + + [Conditional("DEBUG")] + internal static void EnsureDrawThread() + { + //This check is very intrusive on performance, so let's only run when a debugger is actually attached. + if (!Debugger.IsAttached) return; + + Debug.Assert(IsDrawThread); + } + + public static bool IsUpdateThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Update"); + + public static bool IsDrawThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Draw"); + + public static bool IsAudioThread => Thread.CurrentThread.Name == GameThread.PrefixedThreadNameFor("Audio"); + } +} diff --git a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs index 6f8adcaf5..3f9a1e167 100644 --- a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs +++ b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs @@ -1,110 +1,110 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics; -using System; - -namespace osu.Framework.Extensions.Color4Extensions -{ - public static class Color4Extensions - { - public const double GAMMA = 2.4; - - public static double ToLinear(double color) - { - return color <= 0.04045 ? color / 12.92 : Math.Pow((color + 0.055) / 1.055, GAMMA); - } - - public static double ToSRGB(double color) - { - return color < 0.0031308 ? 12.92 * color : 1.055 * Math.Pow(color, 1.0 / GAMMA) - 0.055; - } - - public static Color4 Opacity(this Color4 color, float a) => new Color4(color.R, color.G, color.B, a); - - public static Color4 Opacity(this Color4 color, byte a) => new Color4(color.R, color.G, color.B, a / 255f); - - public static Color4 ToLinear(this Color4 colour) - { - return new Color4( - (float)ToLinear(colour.R), - (float)ToLinear(colour.G), - (float)ToLinear(colour.B), - colour.A); - } - - public static Color4 ToSRGB(this Color4 colour) - { - return new Color4( - (float)ToSRGB(colour.R), - (float)ToSRGB(colour.G), - (float)ToSRGB(colour.B), - colour.A); - } - - public static Color4 MultiplySRGB(Color4 first, Color4 second) - { - if (first.Equals(Color4.White)) - return second; - - if (second.Equals(Color4.White)) - return first; - - first = first.ToLinear(); - second = second.ToLinear(); - - return new Color4( - first.R * second.R, - first.G * second.G, - first.B * second.B, - first.A * second.A).ToSRGB(); - } - - public static Color4 Multiply(Color4 first, Color4 second) - { - if (first.Equals(Color4.White)) - return second; - - if (second.Equals(Color4.White)) - return first; - - return new Color4( - first.R * second.R, - first.G * second.G, - first.B * second.B, - first.A * second.A); - } - - /// - /// Returns a lightened version of the colour. - /// - /// Original colour - /// Decimal light addition - public static Color4 Lighten(this Color4 colour, float amount) => Multiply(colour, 1 + amount); - - /// - /// Returns a darkened version of the colour. - /// - /// Original colour - /// Percentage light reduction - public static Color4 Darken(this Color4 colour, float amount) => Multiply(colour, 1 / (1 + amount)); - - /// - /// Multiply the RGB coordinates by a scalar. - /// - /// Original colour - /// A scalar to multiply with - /// - public static Color4 Multiply(this Color4 colour, float scalar) - { - if (scalar < 0) - throw new ArgumentOutOfRangeException(nameof(scalar), scalar, "Can not multiply colours by negative values."); - - return new Color4( - Math.Min(1, colour.R * scalar), - Math.Min(1, colour.G * scalar), - Math.Min(1, colour.B * scalar), - colour.A); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics; +using System; + +namespace osu.Framework.Extensions.Color4Extensions +{ + public static class Color4Extensions + { + public const double GAMMA = 2.4; + + public static double ToLinear(double color) + { + return color <= 0.04045 ? color / 12.92 : Math.Pow((color + 0.055) / 1.055, GAMMA); + } + + public static double ToSRGB(double color) + { + return color < 0.0031308 ? 12.92 * color : 1.055 * Math.Pow(color, 1.0 / GAMMA) - 0.055; + } + + public static Color4 Opacity(this Color4 color, float a) => new Color4(color.R, color.G, color.B, a); + + public static Color4 Opacity(this Color4 color, byte a) => new Color4(color.R, color.G, color.B, a / 255f); + + public static Color4 ToLinear(this Color4 colour) + { + return new Color4( + (float)ToLinear(colour.R), + (float)ToLinear(colour.G), + (float)ToLinear(colour.B), + colour.A); + } + + public static Color4 ToSRGB(this Color4 colour) + { + return new Color4( + (float)ToSRGB(colour.R), + (float)ToSRGB(colour.G), + (float)ToSRGB(colour.B), + colour.A); + } + + public static Color4 MultiplySRGB(Color4 first, Color4 second) + { + if (first.Equals(Color4.White)) + return second; + + if (second.Equals(Color4.White)) + return first; + + first = first.ToLinear(); + second = second.ToLinear(); + + return new Color4( + first.R * second.R, + first.G * second.G, + first.B * second.B, + first.A * second.A).ToSRGB(); + } + + public static Color4 Multiply(Color4 first, Color4 second) + { + if (first.Equals(Color4.White)) + return second; + + if (second.Equals(Color4.White)) + return first; + + return new Color4( + first.R * second.R, + first.G * second.G, + first.B * second.B, + first.A * second.A); + } + + /// + /// Returns a lightened version of the colour. + /// + /// Original colour + /// Decimal light addition + public static Color4 Lighten(this Color4 colour, float amount) => Multiply(colour, 1 + amount); + + /// + /// Returns a darkened version of the colour. + /// + /// Original colour + /// Percentage light reduction + public static Color4 Darken(this Color4 colour, float amount) => Multiply(colour, 1 / (1 + amount)); + + /// + /// Multiply the RGB coordinates by a scalar. + /// + /// Original colour + /// A scalar to multiply with + /// + public static Color4 Multiply(this Color4 colour, float scalar) + { + if (scalar < 0) + throw new ArgumentOutOfRangeException(nameof(scalar), scalar, "Can not multiply colours by negative values."); + + return new Color4( + Math.Min(1, colour.R * scalar), + Math.Min(1, colour.G * scalar), + Math.Min(1, colour.B * scalar), + colour.A); + } + } +} diff --git a/osu.Framework/Extensions/ExceptionExtensions/ExceptionExtensions.cs b/osu.Framework/Extensions/ExceptionExtensions/ExceptionExtensions.cs index 600105074..8019bc938 100644 --- a/osu.Framework/Extensions/ExceptionExtensions/ExceptionExtensions.cs +++ b/osu.Framework/Extensions/ExceptionExtensions/ExceptionExtensions.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Reflection; -using System.Runtime.ExceptionServices; - -namespace osu.Framework.Extensions.ExceptionExtensions -{ - public static class ExceptionExtensions - { - /// - /// Rethrows as if it was captured in the current context. - /// This preserves the stack trace of , and will not include the point of rethrow. - /// - /// The captured exception. - public static void Rethrow(this Exception exception) - { - ExceptionDispatchInfo.Capture(exception).Throw(); - } - - /// - /// Rethrows the of an if it exists, - /// otherwise, rethrows . - /// This preserves the stack trace of the exception that is rethrown, and will not include the point of rethrow. - /// - /// The captured exception. - public static void RethrowIfSingular(this AggregateException aggregateException) => aggregateException.AsSingular().Rethrow(); - - /// - /// Flattens into a singular if the - /// contains only a single . Otherwise, returns . - /// - /// The captured exception. - /// The highest level of flattening possible. - public static Exception AsSingular(this AggregateException aggregateException) - { - if (aggregateException.InnerExceptions.Count != 1) - return aggregateException; - - while (aggregateException.InnerExceptions.Count == 1) - { - var innerAggregate = aggregateException.InnerException as AggregateException; - if (innerAggregate == null) - return aggregateException.InnerException; - - aggregateException = innerAggregate; - } - - return aggregateException; - } - - /// - /// Retrieves the last exception from a recursive . - /// - /// The exception to retrieve the exception from. - /// The exception at the point of invocation. - public static Exception GetLastInvocation(this TargetInvocationException exception) - { - var inner = exception.InnerException; - while (inner is TargetInvocationException) - inner = inner.InnerException; - return inner; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; + +namespace osu.Framework.Extensions.ExceptionExtensions +{ + public static class ExceptionExtensions + { + /// + /// Rethrows as if it was captured in the current context. + /// This preserves the stack trace of , and will not include the point of rethrow. + /// + /// The captured exception. + public static void Rethrow(this Exception exception) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + /// + /// Rethrows the of an if it exists, + /// otherwise, rethrows . + /// This preserves the stack trace of the exception that is rethrown, and will not include the point of rethrow. + /// + /// The captured exception. + public static void RethrowIfSingular(this AggregateException aggregateException) => aggregateException.AsSingular().Rethrow(); + + /// + /// Flattens into a singular if the + /// contains only a single . Otherwise, returns . + /// + /// The captured exception. + /// The highest level of flattening possible. + public static Exception AsSingular(this AggregateException aggregateException) + { + if (aggregateException.InnerExceptions.Count != 1) + return aggregateException; + + while (aggregateException.InnerExceptions.Count == 1) + { + var innerAggregate = aggregateException.InnerException as AggregateException; + if (innerAggregate == null) + return aggregateException.InnerException; + + aggregateException = innerAggregate; + } + + return aggregateException; + } + + /// + /// Retrieves the last exception from a recursive . + /// + /// The exception to retrieve the exception from. + /// The exception at the point of invocation. + public static Exception GetLastInvocation(this TargetInvocationException exception) + { + var inner = exception.InnerException; + while (inner is TargetInvocationException) + inner = inner.InnerException; + return inner; + } + } +} diff --git a/osu.Framework/Extensions/ExtensionMethods.cs b/osu.Framework/Extensions/ExtensionMethods.cs index 42ee204ed..6b849185d 100644 --- a/osu.Framework/Extensions/ExtensionMethods.cs +++ b/osu.Framework/Extensions/ExtensionMethods.cs @@ -1,242 +1,242 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -// this is an abusive thing to do, but it increases the visibility of Extension Methods to virtually every file. - -namespace osu.Framework.Extensions -{ - /// - /// This class holds extension methods for various purposes and should not be used explicitly, ever. - /// - public static class ExtensionMethods - { - /// - /// Searches for an element that matches the conditions defined by the specified predicate. - /// - /// The list to take values - /// The predicate that needs to be matched. - /// The index to start conditional search. - /// The matched item, or the default value for the type if no item was matched. - public static T Find(this List list, Predicate match, int startIndex) - { - if (!list.IsValidIndex(startIndex)) return default(T); - - int val = list.FindIndex(startIndex, list.Count - startIndex - 1, match); - - return list.ElementAtOrDefault(val); - } - - /// - /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. - /// - /// The list to take values - /// The item that should be added. - /// The index in the list where the item was inserted. - public static int AddInPlace(this List list, T item) - { - int index = list.BinarySearch(item); - if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. - list.Insert(index, item); - return index; - } - - /// - /// Adds the given item to the list according to the comparers sorting rules. Do not use on unsorted lists. - /// - /// The list to take values - /// The item that should be added. - /// The comparer that should be used for sorting. - /// The index in the list where the item was inserted. - public static int AddInPlace(this List list, T item, IComparer comparer) - { - int index = list.BinarySearch(item, comparer); - if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. - list.Insert(index, item); - return index; - } - - /// - /// Try to get a value from the . Returns a default(TValue) if the key does not exist. - /// - /// The dictionary. - /// The lookup key. - /// - public static TValue GetOrDefault(this Dictionary dictionary, TKey lookup) - { - return dictionary.TryGetValue(lookup, out TValue outVal) ? outVal : default(TValue); - } - - public static bool IsValidIndex(this List list, int index) - { - return index >= 0 && index < list.Count; - } - - /// - /// Compares every item in list to given list. - /// - public static bool CompareTo(this List list, List list2) - { - if (list.Count != list2.Count) return false; - - return !list.Where((t, i) => !EqualityComparer.Default.Equals(t, list2[i])).Any(); - } - - /// - /// Converts a rectangular array to a jagged array. - /// - /// The jagged array will contain empty arrays if there are no columns in the rectangular array. - /// - /// - /// The rectangular array. - /// The jagged array. - public static T[][] ToJagged(this T[,] rectangular) - { - if (rectangular == null) - return null; - - var jagged = new T[rectangular.GetLength(0)][]; - for (int r = 0; r < rectangular.GetLength(0); r++) - { - jagged[r] = new T[rectangular.GetLength(1)]; - for (int c = 0; c < rectangular.GetLength(1); c++) - jagged[r][c] = rectangular[r, c]; - } - - return jagged; - } - - /// - /// Converts a jagged array to a rectangular array. - /// - /// All elements that did not exist in the original jagged array are initialized to their default values. - /// - /// - /// The jagged array. - /// The rectangular array. - public static T[,] ToRectangular(this T[][] jagged) - { - if (jagged == null) - return null; - - var rows = jagged.Length; - var cols = rows == 0 ? 0 : jagged.Max(c => c?.Length ?? 0); - - var rectangular = new T[rows, cols]; - for (int r = 0; r < rows; r++) - for (int c = 0; c < cols; c++) - { - if (jagged[r] == null) - continue; - - if (c >= jagged[r].Length) - continue; - - rectangular[r, c] = jagged[r][c]; - } - - return rectangular; - } - - public static string ToResolutionString(this Size size) - { - return size.Width.ToString() + 'x' + size.Height; - } - - public static void WriteLineExplicit(this Stream s, string str = @"") - { - byte[] data = Encoding.UTF8.GetBytes($"{str}\r\n"); - s.Write(data, 0, data.Length); - } - - public static string UnsecureRepresentation(this SecureString s) - { - IntPtr bstr = Marshal.SecureStringToBSTR(s); - - try - { - return Marshal.PtrToStringBSTR(bstr); - } - finally - { - Marshal.FreeBSTR(bstr); - } - } - - public static IEnumerable GetLoadableTypes(this Assembly assembly) - { - if (assembly == null) throw new ArgumentNullException(nameof(assembly)); - try - { - return assembly.GetTypes(); - } - catch (ReflectionTypeLoadException e) - { - return e.Types.Where(t => t != null); - } - } - - public static string GetDescription(this object value) - => value.GetType().GetField(value.ToString()) - .GetCustomAttribute()?.Description ?? value.ToString(); - - public static void ThrowIfFaulted(this Task task, Type expectedBaseType = null) - { - if (!task.IsFaulted) return; - - Exception e = task.Exception; - - while (e?.InnerException != null && e.GetType() != expectedBaseType) - e = e.InnerException; - - ExceptionDispatchInfo.Capture(e).Throw(); - } - - /// - /// Gets a SHA-2 (256bit) hash for the given stream, seeking the stream before and after. - /// - /// The stream to create a hash from. - /// A lower-case hex string representation of the has (64 characters). - public static string ComputeSHA2Hash(this Stream stream) - { - string hash; - - stream.Seek(0, SeekOrigin.Begin); - - using (var alg = SHA256.Create()) - { - alg.ComputeHash(stream); - hash = BitConverter.ToString(alg.Hash).Replace("-", "").ToLowerInvariant(); - } - - stream.Seek(0, SeekOrigin.Begin); - - return hash; - } - - public static string ComputeMD5Hash(this Stream stream) - { - string hash; - - stream.Seek(0, SeekOrigin.Begin); - using (var md5 = MD5.Create()) - hash = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); - stream.Seek(0, SeekOrigin.Begin); - - return hash; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +// this is an abusive thing to do, but it increases the visibility of Extension Methods to virtually every file. + +namespace osu.Framework.Extensions +{ + /// + /// This class holds extension methods for various purposes and should not be used explicitly, ever. + /// + public static class ExtensionMethods + { + /// + /// Searches for an element that matches the conditions defined by the specified predicate. + /// + /// The list to take values + /// The predicate that needs to be matched. + /// The index to start conditional search. + /// The matched item, or the default value for the type if no item was matched. + public static T Find(this List list, Predicate match, int startIndex) + { + if (!list.IsValidIndex(startIndex)) return default(T); + + int val = list.FindIndex(startIndex, list.Count - startIndex - 1, match); + + return list.ElementAtOrDefault(val); + } + + /// + /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. + /// + /// The list to take values + /// The item that should be added. + /// The index in the list where the item was inserted. + public static int AddInPlace(this List list, T item) + { + int index = list.BinarySearch(item); + if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. + list.Insert(index, item); + return index; + } + + /// + /// Adds the given item to the list according to the comparers sorting rules. Do not use on unsorted lists. + /// + /// The list to take values + /// The item that should be added. + /// The comparer that should be used for sorting. + /// The index in the list where the item was inserted. + public static int AddInPlace(this List list, T item, IComparer comparer) + { + int index = list.BinarySearch(item, comparer); + if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. + list.Insert(index, item); + return index; + } + + /// + /// Try to get a value from the . Returns a default(TValue) if the key does not exist. + /// + /// The dictionary. + /// The lookup key. + /// + public static TValue GetOrDefault(this Dictionary dictionary, TKey lookup) + { + return dictionary.TryGetValue(lookup, out TValue outVal) ? outVal : default(TValue); + } + + public static bool IsValidIndex(this List list, int index) + { + return index >= 0 && index < list.Count; + } + + /// + /// Compares every item in list to given list. + /// + public static bool CompareTo(this List list, List list2) + { + if (list.Count != list2.Count) return false; + + return !list.Where((t, i) => !EqualityComparer.Default.Equals(t, list2[i])).Any(); + } + + /// + /// Converts a rectangular array to a jagged array. + /// + /// The jagged array will contain empty arrays if there are no columns in the rectangular array. + /// + /// + /// The rectangular array. + /// The jagged array. + public static T[][] ToJagged(this T[,] rectangular) + { + if (rectangular == null) + return null; + + var jagged = new T[rectangular.GetLength(0)][]; + for (int r = 0; r < rectangular.GetLength(0); r++) + { + jagged[r] = new T[rectangular.GetLength(1)]; + for (int c = 0; c < rectangular.GetLength(1); c++) + jagged[r][c] = rectangular[r, c]; + } + + return jagged; + } + + /// + /// Converts a jagged array to a rectangular array. + /// + /// All elements that did not exist in the original jagged array are initialized to their default values. + /// + /// + /// The jagged array. + /// The rectangular array. + public static T[,] ToRectangular(this T[][] jagged) + { + if (jagged == null) + return null; + + var rows = jagged.Length; + var cols = rows == 0 ? 0 : jagged.Max(c => c?.Length ?? 0); + + var rectangular = new T[rows, cols]; + for (int r = 0; r < rows; r++) + for (int c = 0; c < cols; c++) + { + if (jagged[r] == null) + continue; + + if (c >= jagged[r].Length) + continue; + + rectangular[r, c] = jagged[r][c]; + } + + return rectangular; + } + + public static string ToResolutionString(this Size size) + { + return size.Width.ToString() + 'x' + size.Height; + } + + public static void WriteLineExplicit(this Stream s, string str = @"") + { + byte[] data = Encoding.UTF8.GetBytes($"{str}\r\n"); + s.Write(data, 0, data.Length); + } + + public static string UnsecureRepresentation(this SecureString s) + { + IntPtr bstr = Marshal.SecureStringToBSTR(s); + + try + { + return Marshal.PtrToStringBSTR(bstr); + } + finally + { + Marshal.FreeBSTR(bstr); + } + } + + public static IEnumerable GetLoadableTypes(this Assembly assembly) + { + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null); + } + } + + public static string GetDescription(this object value) + => value.GetType().GetField(value.ToString()) + .GetCustomAttribute()?.Description ?? value.ToString(); + + public static void ThrowIfFaulted(this Task task, Type expectedBaseType = null) + { + if (!task.IsFaulted) return; + + Exception e = task.Exception; + + while (e?.InnerException != null && e.GetType() != expectedBaseType) + e = e.InnerException; + + ExceptionDispatchInfo.Capture(e).Throw(); + } + + /// + /// Gets a SHA-2 (256bit) hash for the given stream, seeking the stream before and after. + /// + /// The stream to create a hash from. + /// A lower-case hex string representation of the has (64 characters). + public static string ComputeSHA2Hash(this Stream stream) + { + string hash; + + stream.Seek(0, SeekOrigin.Begin); + + using (var alg = SHA256.Create()) + { + alg.ComputeHash(stream); + hash = BitConverter.ToString(alg.Hash).Replace("-", "").ToLowerInvariant(); + } + + stream.Seek(0, SeekOrigin.Begin); + + return hash; + } + + public static string ComputeMD5Hash(this Stream stream) + { + string hash; + + stream.Seek(0, SeekOrigin.Begin); + using (var md5 = MD5.Create()) + hash = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); + stream.Seek(0, SeekOrigin.Begin); + + return hash; + } + } +} diff --git a/osu.Framework/Extensions/IEnumerableExtensions/EnumerableExtensions.cs b/osu.Framework/Extensions/IEnumerableExtensions/EnumerableExtensions.cs index 9fe57d499..75b6ddd54 100644 --- a/osu.Framework/Extensions/IEnumerableExtensions/EnumerableExtensions.cs +++ b/osu.Framework/Extensions/IEnumerableExtensions/EnumerableExtensions.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Extensions.IEnumerableExtensions -{ - public static class EnumerableExtensions - { - /// - /// Performs an action on all the items in an IEnumearble collection. - /// - /// The type of the items stored in the collection. - /// The collection to iterate on. - /// The action to be performed. - public static void ForEach(this IEnumerable collection, Action action) - { - if (collection == null) return; - - foreach (var item in collection) - action(item); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Extensions.IEnumerableExtensions +{ + public static class EnumerableExtensions + { + /// + /// Performs an action on all the items in an IEnumearble collection. + /// + /// The type of the items stored in the collection. + /// The collection to iterate on. + /// The action to be performed. + public static void ForEach(this IEnumerable collection, Action action) + { + if (collection == null) return; + + foreach (var item in collection) + action(item); + } + } +} diff --git a/osu.Framework/Extensions/MatrixExtensions/MatrixExtensions.cs b/osu.Framework/Extensions/MatrixExtensions/MatrixExtensions.cs index 9000f63a9..a8dc20600 100644 --- a/osu.Framework/Extensions/MatrixExtensions/MatrixExtensions.cs +++ b/osu.Framework/Extensions/MatrixExtensions/MatrixExtensions.cs @@ -1,154 +1,154 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; - -namespace osu.Framework.Extensions.MatrixExtensions -{ - public static class MatrixExtensions - { - public static void TranslateFromLeft(ref Matrix3 m, Vector2 v) - { - m.Row2 += m.Row0 * v.X + m.Row1 * v.Y; - } - - public static void TranslateFromRight(ref Matrix3 m, Vector2 v) - { - //m.Column0 += m.Column2 * v.X; - m.M11 += m.M13 * v.X; - m.M21 += m.M23 * v.X; - m.M31 += m.M33 * v.X; - - //m.Column1 += m.Column2 * v.Y; - m.M12 += m.M13 * v.Y; - m.M22 += m.M23 * v.Y; - m.M32 += m.M33 * v.Y; - } - - public static void RotateFromLeft(ref Matrix3 m, float radians) - { - float cos = (float)Math.Cos(radians); - float sin = (float)Math.Sin(radians); - - Vector3 row0 = m.Row0 * cos + m.Row1 * sin; - m.Row1 = m.Row1 * cos - m.Row0 * sin; - m.Row0 = row0; - } - - public static void RotateFromRight(ref Matrix3 m, float radians) - { - float cos = (float)Math.Cos(radians); - float sin = (float)Math.Sin(radians); - - //Vector3 column0 = m.Column0 * cos + m.Column1 * sin; - float m11 = m.M11 * cos - m.M12 * sin; - float m21 = m.M21 * cos - m.M22 * sin; - float m31 = m.M31 * cos - m.M32 * sin; - - //m.Column1 = m.Column1 * cos - m.Column0 * sin; - m.M12 = m.M12 * cos + m.M11 * sin; - m.M22 = m.M22 * cos + m.M21 * sin; - m.M32 = m.M32 * cos + m.M31 * sin; - - //m.Column0 = row0; - m.M11 = m11; - m.M21 = m21; - m.M31 = m31; - } - - public static void ScaleFromLeft(ref Matrix3 m, Vector2 v) - { - m.Row0 *= v.X; - m.Row1 *= v.Y; - } - - public static void ScaleFromRight(ref Matrix3 m, Vector2 v) - { - //m.Column0 *= v.X; - m.M11 *= v.X; - m.M21 *= v.X; - m.M31 *= v.X; - - //m.Column1 *= v.Y; - m.M12 *= v.Y; - m.M22 *= v.Y; - m.M32 *= v.Y; - } - - /// - /// Apply shearing in X and Y direction from the left hand side. - /// Since shearing is non-commutative it is important to note that we - /// first shear in the X direction, and then in the Y direction. - /// - /// The matrix to apply the shearing operation to. - /// The X and Y amounts of shearing. - public static void ShearFromLeft(ref Matrix3 m, Vector2 v) - { - Vector3 row0 = m.Row0 + m.Row1 * v.Y + m.Row0 * v.X * v.Y; - m.Row1 += m.Row0 * v.X; - m.Row0 = row0; - } - - /// - /// Apply shearing in X and Y direction from the right hand side. - /// Since shearing is non-commutative it is important to note that we - /// first shear in the Y direction, and then in the X direction. - /// - /// The matrix to apply the shearing operation to. - /// The X and Y amounts of shearing. - public static void ShearFromRight(ref Matrix3 m, Vector2 v) - { - float xy = v.X * v.Y; - - //m.Column0 += m.Column1 * v.X; - float m11 = m.M11 + m.M12 * v.X; - float m21 = m.M21 + m.M22 * v.X; - float m31 = m.M31 + m.M32 * v.X; - - //m.Column1 += m.Column0 * v.Y + m.Column1 * xy; - m.M12 += m.M11 * v.Y + m.M12 * xy; - m.M22 += m.M21 * v.Y + m.M22 * xy; - m.M32 += m.M31 * v.Y + m.M32 * xy; - - m.M11 = m11; - m.M21 = m21; - m.M31 = m31; - } - - public static void FastInvert(ref Matrix3 value) - { - float d11 = value.M22 * value.M33 + value.M23 * -value.M32; - float d12 = value.M21 * value.M33 + value.M23 * -value.M31; - float d13 = value.M21 * value.M32 + value.M22 * -value.M31; - - float det = value.M11 * d11 - value.M12 * d12 + value.M13 * d13; - - if (Math.Abs(det) == 0.0f) - { - value = Matrix3.Zero; - return; - } - - det = 1f / det; - - float d21 = value.M12 * value.M33 + value.M13 * -value.M32; - float d22 = value.M11 * value.M33 + value.M13 * -value.M31; - float d23 = value.M11 * value.M32 + value.M12 * -value.M31; - - float d31 = value.M12 * value.M23 - value.M13 * value.M22; - float d32 = value.M11 * value.M23 - value.M13 * value.M21; - float d33 = value.M11 * value.M22 - value.M12 * value.M21; - - value.M11 = +d11 * det; - value.M12 = -d21 * det; - value.M13 = +d31 * det; - value.M21 = -d12 * det; - value.M22 = +d22 * det; - value.M23 = -d32 * det; - value.M31 = +d13 * det; - value.M32 = -d23 * det; - value.M33 = +d33 * det; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; + +namespace osu.Framework.Extensions.MatrixExtensions +{ + public static class MatrixExtensions + { + public static void TranslateFromLeft(ref Matrix3 m, Vector2 v) + { + m.Row2 += m.Row0 * v.X + m.Row1 * v.Y; + } + + public static void TranslateFromRight(ref Matrix3 m, Vector2 v) + { + //m.Column0 += m.Column2 * v.X; + m.M11 += m.M13 * v.X; + m.M21 += m.M23 * v.X; + m.M31 += m.M33 * v.X; + + //m.Column1 += m.Column2 * v.Y; + m.M12 += m.M13 * v.Y; + m.M22 += m.M23 * v.Y; + m.M32 += m.M33 * v.Y; + } + + public static void RotateFromLeft(ref Matrix3 m, float radians) + { + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); + + Vector3 row0 = m.Row0 * cos + m.Row1 * sin; + m.Row1 = m.Row1 * cos - m.Row0 * sin; + m.Row0 = row0; + } + + public static void RotateFromRight(ref Matrix3 m, float radians) + { + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); + + //Vector3 column0 = m.Column0 * cos + m.Column1 * sin; + float m11 = m.M11 * cos - m.M12 * sin; + float m21 = m.M21 * cos - m.M22 * sin; + float m31 = m.M31 * cos - m.M32 * sin; + + //m.Column1 = m.Column1 * cos - m.Column0 * sin; + m.M12 = m.M12 * cos + m.M11 * sin; + m.M22 = m.M22 * cos + m.M21 * sin; + m.M32 = m.M32 * cos + m.M31 * sin; + + //m.Column0 = row0; + m.M11 = m11; + m.M21 = m21; + m.M31 = m31; + } + + public static void ScaleFromLeft(ref Matrix3 m, Vector2 v) + { + m.Row0 *= v.X; + m.Row1 *= v.Y; + } + + public static void ScaleFromRight(ref Matrix3 m, Vector2 v) + { + //m.Column0 *= v.X; + m.M11 *= v.X; + m.M21 *= v.X; + m.M31 *= v.X; + + //m.Column1 *= v.Y; + m.M12 *= v.Y; + m.M22 *= v.Y; + m.M32 *= v.Y; + } + + /// + /// Apply shearing in X and Y direction from the left hand side. + /// Since shearing is non-commutative it is important to note that we + /// first shear in the X direction, and then in the Y direction. + /// + /// The matrix to apply the shearing operation to. + /// The X and Y amounts of shearing. + public static void ShearFromLeft(ref Matrix3 m, Vector2 v) + { + Vector3 row0 = m.Row0 + m.Row1 * v.Y + m.Row0 * v.X * v.Y; + m.Row1 += m.Row0 * v.X; + m.Row0 = row0; + } + + /// + /// Apply shearing in X and Y direction from the right hand side. + /// Since shearing is non-commutative it is important to note that we + /// first shear in the Y direction, and then in the X direction. + /// + /// The matrix to apply the shearing operation to. + /// The X and Y amounts of shearing. + public static void ShearFromRight(ref Matrix3 m, Vector2 v) + { + float xy = v.X * v.Y; + + //m.Column0 += m.Column1 * v.X; + float m11 = m.M11 + m.M12 * v.X; + float m21 = m.M21 + m.M22 * v.X; + float m31 = m.M31 + m.M32 * v.X; + + //m.Column1 += m.Column0 * v.Y + m.Column1 * xy; + m.M12 += m.M11 * v.Y + m.M12 * xy; + m.M22 += m.M21 * v.Y + m.M22 * xy; + m.M32 += m.M31 * v.Y + m.M32 * xy; + + m.M11 = m11; + m.M21 = m21; + m.M31 = m31; + } + + public static void FastInvert(ref Matrix3 value) + { + float d11 = value.M22 * value.M33 + value.M23 * -value.M32; + float d12 = value.M21 * value.M33 + value.M23 * -value.M31; + float d13 = value.M21 * value.M32 + value.M22 * -value.M31; + + float det = value.M11 * d11 - value.M12 * d12 + value.M13 * d13; + + if (Math.Abs(det) == 0.0f) + { + value = Matrix3.Zero; + return; + } + + det = 1f / det; + + float d21 = value.M12 * value.M33 + value.M13 * -value.M32; + float d22 = value.M11 * value.M33 + value.M13 * -value.M31; + float d23 = value.M11 * value.M32 + value.M12 * -value.M31; + + float d31 = value.M12 * value.M23 - value.M13 * value.M22; + float d32 = value.M11 * value.M23 - value.M13 * value.M21; + float d33 = value.M11 * value.M22 - value.M12 * value.M21; + + value.M11 = +d11 * det; + value.M12 = -d21 * det; + value.M13 = +d31 * det; + value.M21 = -d12 * det; + value.M22 = +d22 * det; + value.M23 = -d32 * det; + value.M31 = +d13 * det; + value.M32 = -d23 * det; + value.M33 = +d33 * det; + } + } +} diff --git a/osu.Framework/Extensions/PolygonExtensions/ConvexPolygonExtensions.cs b/osu.Framework/Extensions/PolygonExtensions/ConvexPolygonExtensions.cs index d7f57bd55..215556027 100644 --- a/osu.Framework/Extensions/PolygonExtensions/ConvexPolygonExtensions.cs +++ b/osu.Framework/Extensions/PolygonExtensions/ConvexPolygonExtensions.cs @@ -1,46 +1,46 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using OpenTK; - -namespace osu.Framework.Extensions.PolygonExtensions -{ - /// - /// Todo: Support segment containment and circles. - /// Todo: Might be overkill, but possibly support convex decomposition? - /// - public static class ConvexPolygonExtensions - { - /// - /// Determines whether two convex polygons intersect. - /// - /// The first polygon. - /// The second polygon. - /// Whether the two polygons intersect. - public static bool Intersects(this IConvexPolygon first, IConvexPolygon second) - { - Vector2[][] bothAxes = { first.GetAxes(), second.GetAxes() }; - Vector2[][] bothVertices = { first.Vertices, second.Vertices }; - - return intersects(bothAxes, bothVertices); - } - - private static bool intersects(Vector2[][] bothAxes, Vector2[][] bothVertices) - { - foreach (Vector2[] axes in bothAxes) - { - foreach (Vector2 axis in axes) - { - ProjectionRange firstRange = new ProjectionRange(axis, bothVertices[0]); - ProjectionRange secondRange = new ProjectionRange(axis, bothVertices[1]); - - if (!firstRange.Overlaps(secondRange)) - return false; - } - } - - return true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using OpenTK; + +namespace osu.Framework.Extensions.PolygonExtensions +{ + /// + /// Todo: Support segment containment and circles. + /// Todo: Might be overkill, but possibly support convex decomposition? + /// + public static class ConvexPolygonExtensions + { + /// + /// Determines whether two convex polygons intersect. + /// + /// The first polygon. + /// The second polygon. + /// Whether the two polygons intersect. + public static bool Intersects(this IConvexPolygon first, IConvexPolygon second) + { + Vector2[][] bothAxes = { first.GetAxes(), second.GetAxes() }; + Vector2[][] bothVertices = { first.Vertices, second.Vertices }; + + return intersects(bothAxes, bothVertices); + } + + private static bool intersects(Vector2[][] bothAxes, Vector2[][] bothVertices) + { + foreach (Vector2[] axes in bothAxes) + { + foreach (Vector2 axis in axes) + { + ProjectionRange firstRange = new ProjectionRange(axis, bothVertices[0]); + ProjectionRange secondRange = new ProjectionRange(axis, bothVertices[1]); + + if (!firstRange.Overlaps(secondRange)) + return false; + } + } + + return true; + } + } +} diff --git a/osu.Framework/Extensions/PolygonExtensions/PolygonExtensions.cs b/osu.Framework/Extensions/PolygonExtensions/PolygonExtensions.cs index 35bb01123..db5cdaf4b 100644 --- a/osu.Framework/Extensions/PolygonExtensions/PolygonExtensions.cs +++ b/osu.Framework/Extensions/PolygonExtensions/PolygonExtensions.cs @@ -1,40 +1,40 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using OpenTK; - -namespace osu.Framework.Extensions.PolygonExtensions -{ - public static class PolygonExtensions - { - /// - /// Computes the axes for each edge in a polygon. - /// - /// The polygon to return the axes of. - /// Whether the normals should be normalized. Allows computation of the exact intersection point. - /// The axes of the polygon. - public static Vector2[] GetAxes(this IPolygon polygon, bool normalize = false) - { - Vector2[] axes = new Vector2[polygon.AxisVertices.Length]; - - for (int i = 0; i < polygon.AxisVertices.Length; i++) - { - // Construct an edge between two sequential points - Vector2 v1 = polygon.AxisVertices[i]; - Vector2 v2 = polygon.AxisVertices[i == polygon.AxisVertices.Length - 1 ? 0 : i + 1]; - Vector2 edge = v2 - v1; - - // Find the normal to the edge - Vector2 normal = new Vector2(-edge.Y, edge.X); - - if (normalize) - normal = Vector2.Normalize(normal); - - axes[i] = normal; - } - - return axes; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using OpenTK; + +namespace osu.Framework.Extensions.PolygonExtensions +{ + public static class PolygonExtensions + { + /// + /// Computes the axes for each edge in a polygon. + /// + /// The polygon to return the axes of. + /// Whether the normals should be normalized. Allows computation of the exact intersection point. + /// The axes of the polygon. + public static Vector2[] GetAxes(this IPolygon polygon, bool normalize = false) + { + Vector2[] axes = new Vector2[polygon.AxisVertices.Length]; + + for (int i = 0; i < polygon.AxisVertices.Length; i++) + { + // Construct an edge between two sequential points + Vector2 v1 = polygon.AxisVertices[i]; + Vector2 v2 = polygon.AxisVertices[i == polygon.AxisVertices.Length - 1 ? 0 : i + 1]; + Vector2 edge = v2 - v1; + + // Find the normal to the edge + Vector2 normal = new Vector2(-edge.Y, edge.X); + + if (normalize) + normal = Vector2.Normalize(normal); + + axes[i] = normal; + } + + return axes; + } + } +} diff --git a/osu.Framework/Extensions/TypeExtensions/TypeExtensions.cs b/osu.Framework/Extensions/TypeExtensions/TypeExtensions.cs index bb08cac33..b5634ca96 100644 --- a/osu.Framework/Extensions/TypeExtensions/TypeExtensions.cs +++ b/osu.Framework/Extensions/TypeExtensions/TypeExtensions.cs @@ -1,39 +1,39 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Framework.Extensions.TypeExtensions -{ - public static class TypeExtensions - { - private static string readableName(Type t, HashSet usedTypes) - { - usedTypes.Add(t); - - string result = t.Name; - - // Trim away amount of type arguments - int amountTypeArgumentsPos = result.IndexOf("`", StringComparison.Ordinal); - if (amountTypeArgumentsPos >= 0) - result = result.Substring(0, amountTypeArgumentsPos); - - // We were declared inside another class. Preprend the name of that class. - if (t.DeclaringType != null && !usedTypes.Contains(t.DeclaringType)) - result = readableName(t.DeclaringType, usedTypes) + "+" + result; - - if (t.IsGenericType) - { - var typeArgs = t.GetGenericArguments().Except(usedTypes); - if (typeArgs.Any()) - result += "<" + string.Join(",", typeArgs.Select(genType => readableName(genType, usedTypes))) + ">"; - } - - return result; - } - - public static string ReadableName(this Type t) => readableName(t, new HashSet()); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Extensions.TypeExtensions +{ + public static class TypeExtensions + { + private static string readableName(Type t, HashSet usedTypes) + { + usedTypes.Add(t); + + string result = t.Name; + + // Trim away amount of type arguments + int amountTypeArgumentsPos = result.IndexOf("`", StringComparison.Ordinal); + if (amountTypeArgumentsPos >= 0) + result = result.Substring(0, amountTypeArgumentsPos); + + // We were declared inside another class. Preprend the name of that class. + if (t.DeclaringType != null && !usedTypes.Contains(t.DeclaringType)) + result = readableName(t.DeclaringType, usedTypes) + "+" + result; + + if (t.IsGenericType) + { + var typeArgs = t.GetGenericArguments().Except(usedTypes); + if (typeArgs.Any()) + result += "<" + string.Join(",", typeArgs.Select(genType => readableName(genType, usedTypes))) + ">"; + } + + return result; + } + + public static string ReadableName(this Type t) => readableName(t, new HashSet()); + } +} diff --git a/osu.Framework/Game.cs b/osu.Framework/Game.cs index 7a54a0831..a0b243594 100644 --- a/osu.Framework/Game.cs +++ b/osu.Framework/Game.cs @@ -1,257 +1,257 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Linq; -using osu.Framework.Audio; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Visualisation; -using osu.Framework.Input; -using osu.Framework.IO.Stores; -using osu.Framework.Platform; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Input.Bindings; -using OpenTK; -using GameWindow = osu.Framework.Platform.GameWindow; - -namespace osu.Framework -{ - public abstract class Game : Container, IKeyBindingHandler - { - public GameWindow Window => Host?.Window; - - public ResourceStore Resources; - - public TextureStore Textures; - - /// - /// This should point to the main resource dll file. If not specified, it will use resources embedded in your executable. - /// - protected virtual string MainResourceFile => Host.FullPath; - - protected GameHost Host { get; private set; } - - private bool isActive; - - public AudioManager Audio; - - public ShaderManager Shaders; - - public FontStore Fonts; - - private readonly Container content; - private PerformanceOverlay performanceContainer; - internal DrawVisualiser DrawVisualiser; - - private LogOverlay logOverlay; - - protected override Container Content => content; - - protected Game() - { - RelativeSizeAxes = Axes.Both; - - AddRangeInternal(new Drawable[] - { - content = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, - }); - } - - private void addDebugTools() - { - LoadComponentAsync(DrawVisualiser = new DrawVisualiser - { - Depth = float.MinValue / 2, - }, AddInternal); - - LoadComponentAsync(logOverlay = new LogOverlay - { - Depth = float.MinValue / 2, - }, AddInternal); - } - - /// - /// As Load is run post host creation, you can override this method to alter properties of the host before it makes itself visible to the user. - /// - /// - public virtual void SetHost(GameHost host) - { - Host = host; - host.Exiting += OnExiting; - host.Activated += () => IsActive = true; - host.Deactivated += () => IsActive = false; - } - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => - dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); - - [BackgroundDependencyLoader] - private void load(FrameworkConfigManager config) - { - Resources = new ResourceStore(); - Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(@"osu.Framework.dll"), @"Resources")); - Resources.AddStore(new DllResourceStore(MainResourceFile)); - - Textures = new TextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); - Textures.AddStore(new RawTextureLoaderStore(new OnlineStore())); - dependencies.Cache(Textures); - - var tracks = new ResourceStore(Resources); - tracks.AddStore(new NamespacedResourceStore(Resources, @"Tracks")); - tracks.AddStore(new OnlineStore()); - - var samples = new ResourceStore(Resources); - samples.AddStore(new NamespacedResourceStore(Resources, @"Samples")); - samples.AddStore(new OnlineStore()); - - Audio = new AudioManager(tracks, samples) { EventScheduler = Scheduler }; - dependencies.Cache(Audio); - - Host.RegisterThread(Audio.Thread); - - //attach our bindables to the audio subsystem. - config.BindWith(FrameworkSetting.AudioDevice, Audio.AudioDevice); - config.BindWith(FrameworkSetting.VolumeUniversal, Audio.Volume); - config.BindWith(FrameworkSetting.VolumeEffect, Audio.VolumeSample); - config.BindWith(FrameworkSetting.VolumeMusic, Audio.VolumeTrack); - - Shaders = new ShaderManager(new NamespacedResourceStore(Resources, @"Shaders")); - dependencies.Cache(Shaders); - - Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/OpenSans")) - { - ScaleAdjust = 100 - }; - dependencies.Cache(Fonts); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - LoadComponentAsync(performanceContainer = new PerformanceOverlay(Host.Threads.Reverse()) - { - Margin = new MarginPadding(5), - Direction = FillDirection.Vertical, - Spacing = new Vector2(10, 10), - AutoSizeAxes = Axes.Both, - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Depth = float.MinValue - }, AddInternal); - - addDebugTools(); - } - - /// - /// Whether the Game environment is active (in the foreground). - /// - public bool IsActive - { - get { return isActive; } - private set - { - if (value == isActive) - return; - isActive = value; - - if (isActive) - OnActivated(); - else - OnDeactivated(); - } - } - - protected FrameStatisticsMode FrameStatisticsMode - { - get { return performanceContainer.State; } - set { performanceContainer.State = value; } - } - - public bool OnPressed(FrameworkAction action) - { - switch (action) - { - case FrameworkAction.CycleFrameStatistics: - switch (FrameStatisticsMode) - { - case FrameStatisticsMode.None: - FrameStatisticsMode = FrameStatisticsMode.Minimal; - break; - case FrameStatisticsMode.Minimal: - FrameStatisticsMode = FrameStatisticsMode.Full; - break; - case FrameStatisticsMode.Full: - FrameStatisticsMode = FrameStatisticsMode.None; - break; - } - return true; - case FrameworkAction.ToggleDrawVisualiser: - DrawVisualiser.ToggleVisibility(); - return true; - case FrameworkAction.ToggleLogOverlay: - logOverlay.ToggleVisibility(); - return true; - case FrameworkAction.ToggleFullscreen: - Window?.CycleMode(); - return true; - } - - return false; - } - - public bool OnReleased(FrameworkAction action) => false; - - public void Exit() - { - Host.Exit(); - } - - protected virtual void OnActivated() - { - } - - protected virtual void OnDeactivated() - { - } - - protected virtual bool OnExiting() - { - return false; - } - - /// - /// Called before a frame cycle has started (Update and Draw). - /// - protected virtual void PreFrame() - { - } - - /// - /// Called after a frame cycle has been completed (Update and Draw). - /// - protected virtual void PostFrame() - { - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - Audio?.Dispose(); - Audio = null; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Visualisation; +using osu.Framework.Input; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Input.Bindings; +using OpenTK; +using GameWindow = osu.Framework.Platform.GameWindow; + +namespace osu.Framework +{ + public abstract class Game : Container, IKeyBindingHandler + { + public GameWindow Window => Host?.Window; + + public ResourceStore Resources; + + public TextureStore Textures; + + /// + /// This should point to the main resource dll file. If not specified, it will use resources embedded in your executable. + /// + protected virtual string MainResourceFile => Host.FullPath; + + protected GameHost Host { get; private set; } + + private bool isActive; + + public AudioManager Audio; + + public ShaderManager Shaders; + + public FontStore Fonts; + + private readonly Container content; + private PerformanceOverlay performanceContainer; + internal DrawVisualiser DrawVisualiser; + + private LogOverlay logOverlay; + + protected override Container Content => content; + + protected Game() + { + RelativeSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + }); + } + + private void addDebugTools() + { + LoadComponentAsync(DrawVisualiser = new DrawVisualiser + { + Depth = float.MinValue / 2, + }, AddInternal); + + LoadComponentAsync(logOverlay = new LogOverlay + { + Depth = float.MinValue / 2, + }, AddInternal); + } + + /// + /// As Load is run post host creation, you can override this method to alter properties of the host before it makes itself visible to the user. + /// + /// + public virtual void SetHost(GameHost host) + { + Host = host; + host.Exiting += OnExiting; + host.Activated += () => IsActive = true; + host.Deactivated += () => IsActive = false; + } + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + [BackgroundDependencyLoader] + private void load(FrameworkConfigManager config) + { + Resources = new ResourceStore(); + Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(@"osu.Framework.dll"), @"Resources")); + Resources.AddStore(new DllResourceStore(MainResourceFile)); + + Textures = new TextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); + Textures.AddStore(new RawTextureLoaderStore(new OnlineStore())); + dependencies.Cache(Textures); + + var tracks = new ResourceStore(Resources); + tracks.AddStore(new NamespacedResourceStore(Resources, @"Tracks")); + tracks.AddStore(new OnlineStore()); + + var samples = new ResourceStore(Resources); + samples.AddStore(new NamespacedResourceStore(Resources, @"Samples")); + samples.AddStore(new OnlineStore()); + + Audio = new AudioManager(tracks, samples) { EventScheduler = Scheduler }; + dependencies.Cache(Audio); + + Host.RegisterThread(Audio.Thread); + + //attach our bindables to the audio subsystem. + config.BindWith(FrameworkSetting.AudioDevice, Audio.AudioDevice); + config.BindWith(FrameworkSetting.VolumeUniversal, Audio.Volume); + config.BindWith(FrameworkSetting.VolumeEffect, Audio.VolumeSample); + config.BindWith(FrameworkSetting.VolumeMusic, Audio.VolumeTrack); + + Shaders = new ShaderManager(new NamespacedResourceStore(Resources, @"Shaders")); + dependencies.Cache(Shaders); + + Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/OpenSans")) + { + ScaleAdjust = 100 + }; + dependencies.Cache(Fonts); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LoadComponentAsync(performanceContainer = new PerformanceOverlay(Host.Threads.Reverse()) + { + Margin = new MarginPadding(5), + Direction = FillDirection.Vertical, + Spacing = new Vector2(10, 10), + AutoSizeAxes = Axes.Both, + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Depth = float.MinValue + }, AddInternal); + + addDebugTools(); + } + + /// + /// Whether the Game environment is active (in the foreground). + /// + public bool IsActive + { + get { return isActive; } + private set + { + if (value == isActive) + return; + isActive = value; + + if (isActive) + OnActivated(); + else + OnDeactivated(); + } + } + + protected FrameStatisticsMode FrameStatisticsMode + { + get { return performanceContainer.State; } + set { performanceContainer.State = value; } + } + + public bool OnPressed(FrameworkAction action) + { + switch (action) + { + case FrameworkAction.CycleFrameStatistics: + switch (FrameStatisticsMode) + { + case FrameStatisticsMode.None: + FrameStatisticsMode = FrameStatisticsMode.Minimal; + break; + case FrameStatisticsMode.Minimal: + FrameStatisticsMode = FrameStatisticsMode.Full; + break; + case FrameStatisticsMode.Full: + FrameStatisticsMode = FrameStatisticsMode.None; + break; + } + return true; + case FrameworkAction.ToggleDrawVisualiser: + DrawVisualiser.ToggleVisibility(); + return true; + case FrameworkAction.ToggleLogOverlay: + logOverlay.ToggleVisibility(); + return true; + case FrameworkAction.ToggleFullscreen: + Window?.CycleMode(); + return true; + } + + return false; + } + + public bool OnReleased(FrameworkAction action) => false; + + public void Exit() + { + Host.Exit(); + } + + protected virtual void OnActivated() + { + } + + protected virtual void OnDeactivated() + { + } + + protected virtual bool OnExiting() + { + return false; + } + + /// + /// Called before a frame cycle has started (Update and Draw). + /// + protected virtual void PreFrame() + { + } + + /// + /// Called after a frame cycle has been completed (Update and Draw). + /// + protected virtual void PostFrame() + { + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Audio?.Dispose(); + Audio = null; + } + } +} diff --git a/osu.Framework/Graphics/Animations/Animation.cs b/osu.Framework/Graphics/Animations/Animation.cs index becc6139b..6a98fe983 100644 --- a/osu.Framework/Graphics/Animations/Animation.cs +++ b/osu.Framework/Graphics/Animations/Animation.cs @@ -1,153 +1,153 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Animations -{ - /// - /// Represents a generic, frame-based animation. Inherit this class if you need custom animations. - /// - /// The type of content in the frames of the animation. - public abstract class Animation : Container, IAnimation - { - /// - /// The duration in milliseconds of a newly added frame, if no duration is explicitly specified when adding the frame. - /// - public double DefaultFrameLength = 1000.0 / 60.0; - - private readonly List> frameData; - private int currentFrameIndex; - - private double currentFrameTime; - - /// - /// The number of frames this animation has. - /// - public int FrameCount => frameData.Count; - - /// - /// True if the animation is playing, false otherwise. - /// - public bool IsPlaying { get; set; } - - /// - /// True if the animation should start over from the first frame after finishing. False if it should stop playing and keep displaying the last frame when finishing. - /// - public bool Repeat { get; set; } - - - protected Animation() - { - AutoSizeAxes = Axes.Both; - - frameData = new List>(); - IsPlaying = true; - Repeat = true; - } - - /// - /// Displays the frame with the given zero-based frame index. - /// - /// The zero-based index of the frame to display. - public void GotoFrame(int frameIndex) - { - if (frameIndex < 0) - frameIndex = 0; - else if (frameIndex >= frameData.Count) - frameIndex = frameData.Count - 1; - - currentFrameIndex = frameIndex; - displayFrame(currentFrameIndex); - } - - /// - /// Adds a new frame with the given content and display duration (in milliseconds) to this animation. - /// - /// The content of the new frame. - /// The duration the new frame should be displayed for. - public void AddFrame(T content, double? displayDuration = null) - { - AddFrame(new FrameData - { - Duration = displayDuration ?? DefaultFrameLength, // 60 fps by default - Content = content - }); - } - - public void AddFrame(FrameData frame) - { - frameData.Add(frame); - OnFrameAdded(frame.Content, frame.Duration); - - if (frameData.Count == 1) - displayFrame(0); - } - - /// - /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for 1/60th of a second. - /// - /// The contents to use for creating new frames. - public void AddFrames(IEnumerable contents) - { - foreach (var t in contents) - AddFrame(t); - } - - /// - /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for the given number of milliseconds. - /// - /// The contents and display durations to use for creating new frames. - public void AddFrames(IEnumerable> frames) - { - foreach (var t in frames) - AddFrame(t.Content, t.Duration); - } - - private void displayFrame(int index) => DisplayFrame(frameData[index].Content); - - /// - /// Displays the given contents. - /// - /// This method will only be called after has been called at least once. - /// The content that will be displayed. - protected abstract void DisplayFrame(T content); - - /// - /// Called whenever a new frame was added to this animation. - /// - /// The content of the new frame. - /// The display duration of the new frame. - protected virtual void OnFrameAdded(T content, double displayDuration) { } - - protected override void Update() - { - base.Update(); - - if (IsPlaying && frameData.Count > 0) - { - currentFrameTime += Time.Elapsed; - while (currentFrameTime > frameData[currentFrameIndex].Duration) - { - currentFrameTime -= frameData[currentFrameIndex].Duration; - ++currentFrameIndex; - if (currentFrameIndex >= frameData.Count) - { - if (Repeat) - { - currentFrameIndex = 0; - } - else - { - currentFrameIndex = frameData.Count - 1; - IsPlaying = false; - break; - } - } - } - displayFrame(currentFrameIndex); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Animations +{ + /// + /// Represents a generic, frame-based animation. Inherit this class if you need custom animations. + /// + /// The type of content in the frames of the animation. + public abstract class Animation : Container, IAnimation + { + /// + /// The duration in milliseconds of a newly added frame, if no duration is explicitly specified when adding the frame. + /// + public double DefaultFrameLength = 1000.0 / 60.0; + + private readonly List> frameData; + private int currentFrameIndex; + + private double currentFrameTime; + + /// + /// The number of frames this animation has. + /// + public int FrameCount => frameData.Count; + + /// + /// True if the animation is playing, false otherwise. + /// + public bool IsPlaying { get; set; } + + /// + /// True if the animation should start over from the first frame after finishing. False if it should stop playing and keep displaying the last frame when finishing. + /// + public bool Repeat { get; set; } + + + protected Animation() + { + AutoSizeAxes = Axes.Both; + + frameData = new List>(); + IsPlaying = true; + Repeat = true; + } + + /// + /// Displays the frame with the given zero-based frame index. + /// + /// The zero-based index of the frame to display. + public void GotoFrame(int frameIndex) + { + if (frameIndex < 0) + frameIndex = 0; + else if (frameIndex >= frameData.Count) + frameIndex = frameData.Count - 1; + + currentFrameIndex = frameIndex; + displayFrame(currentFrameIndex); + } + + /// + /// Adds a new frame with the given content and display duration (in milliseconds) to this animation. + /// + /// The content of the new frame. + /// The duration the new frame should be displayed for. + public void AddFrame(T content, double? displayDuration = null) + { + AddFrame(new FrameData + { + Duration = displayDuration ?? DefaultFrameLength, // 60 fps by default + Content = content + }); + } + + public void AddFrame(FrameData frame) + { + frameData.Add(frame); + OnFrameAdded(frame.Content, frame.Duration); + + if (frameData.Count == 1) + displayFrame(0); + } + + /// + /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for 1/60th of a second. + /// + /// The contents to use for creating new frames. + public void AddFrames(IEnumerable contents) + { + foreach (var t in contents) + AddFrame(t); + } + + /// + /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for the given number of milliseconds. + /// + /// The contents and display durations to use for creating new frames. + public void AddFrames(IEnumerable> frames) + { + foreach (var t in frames) + AddFrame(t.Content, t.Duration); + } + + private void displayFrame(int index) => DisplayFrame(frameData[index].Content); + + /// + /// Displays the given contents. + /// + /// This method will only be called after has been called at least once. + /// The content that will be displayed. + protected abstract void DisplayFrame(T content); + + /// + /// Called whenever a new frame was added to this animation. + /// + /// The content of the new frame. + /// The display duration of the new frame. + protected virtual void OnFrameAdded(T content, double displayDuration) { } + + protected override void Update() + { + base.Update(); + + if (IsPlaying && frameData.Count > 0) + { + currentFrameTime += Time.Elapsed; + while (currentFrameTime > frameData[currentFrameIndex].Duration) + { + currentFrameTime -= frameData[currentFrameIndex].Duration; + ++currentFrameIndex; + if (currentFrameIndex >= frameData.Count) + { + if (Repeat) + { + currentFrameIndex = 0; + } + else + { + currentFrameIndex = frameData.Count - 1; + IsPlaying = false; + break; + } + } + } + displayFrame(currentFrameIndex); + } + } + } +} diff --git a/osu.Framework/Graphics/Animations/AnimationExtensions.cs b/osu.Framework/Graphics/Animations/AnimationExtensions.cs index eb520bc16..13c6b12a0 100644 --- a/osu.Framework/Graphics/Animations/AnimationExtensions.cs +++ b/osu.Framework/Graphics/Animations/AnimationExtensions.cs @@ -1,51 +1,51 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Animations -{ - /// - /// This class holds various extension methods for the interface. - /// - public static class AnimationExtensions - { - /// - /// Displays the frame with the given zero-based frame index and stops the animation at that frame. - /// - /// The animation that should seek the frame and stop playing. - /// The zero-based index of the frame to display. - public static void GotoAndStop(this IAnimation animation, int frameIndex) - { - animation.GotoFrame(frameIndex); - animation.IsPlaying = false; - } - - /// - /// Displays the frame with the given zero-based frame index and plays the animation from that frame. - /// - /// The animation that should seek the frame and start playing. - /// The zero-based index of the frame to display. - public static void GotoAndPlay(this IAnimation animation, int frameIndex) - { - animation.GotoFrame(frameIndex); - animation.IsPlaying = true; - } - - /// - /// Resumes playing the animation. - /// - /// The animation to play. - public static void Play(this IAnimation animation) => animation.IsPlaying = true; - - /// - /// Stops playing the animation. - /// - /// The animation to stop playing. - public static void Stop(this IAnimation animation) => animation.IsPlaying = false; - - /// - /// Restarts the animation. - /// - /// The animation to restart. - public static void Restart(this IAnimation animation) => animation.GotoAndPlay(0); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Animations +{ + /// + /// This class holds various extension methods for the interface. + /// + public static class AnimationExtensions + { + /// + /// Displays the frame with the given zero-based frame index and stops the animation at that frame. + /// + /// The animation that should seek the frame and stop playing. + /// The zero-based index of the frame to display. + public static void GotoAndStop(this IAnimation animation, int frameIndex) + { + animation.GotoFrame(frameIndex); + animation.IsPlaying = false; + } + + /// + /// Displays the frame with the given zero-based frame index and plays the animation from that frame. + /// + /// The animation that should seek the frame and start playing. + /// The zero-based index of the frame to display. + public static void GotoAndPlay(this IAnimation animation, int frameIndex) + { + animation.GotoFrame(frameIndex); + animation.IsPlaying = true; + } + + /// + /// Resumes playing the animation. + /// + /// The animation to play. + public static void Play(this IAnimation animation) => animation.IsPlaying = true; + + /// + /// Stops playing the animation. + /// + /// The animation to stop playing. + public static void Stop(this IAnimation animation) => animation.IsPlaying = false; + + /// + /// Restarts the animation. + /// + /// The animation to restart. + public static void Restart(this IAnimation animation) => animation.GotoAndPlay(0); + } +} diff --git a/osu.Framework/Graphics/Animations/DrawableAnimation.cs b/osu.Framework/Graphics/Animations/DrawableAnimation.cs index 69f229549..2045874f6 100644 --- a/osu.Framework/Graphics/Animations/DrawableAnimation.cs +++ b/osu.Framework/Graphics/Animations/DrawableAnimation.cs @@ -1,17 +1,17 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Animations -{ - /// - /// An animation that switches the displayed drawable when a new frame is displayed. - /// - public class DrawableAnimation : Animation - { - protected override void DisplayFrame(Drawable content) - { - Clear(false); - Add(content); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Animations +{ + /// + /// An animation that switches the displayed drawable when a new frame is displayed. + /// + public class DrawableAnimation : Animation + { + protected override void DisplayFrame(Drawable content) + { + Clear(false); + Add(content); + } + } +} diff --git a/osu.Framework/Graphics/Animations/FrameData.cs b/osu.Framework/Graphics/Animations/FrameData.cs index 21471f596..5cab16194 100644 --- a/osu.Framework/Graphics/Animations/FrameData.cs +++ b/osu.Framework/Graphics/Animations/FrameData.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Animations -{ - /// - /// Represents all data necessary to describe a single frame of an . - /// - /// The type of animation the frame data is for. - public struct FrameData - { - /// - /// The contents to display for the frame. - /// - public T Content { get; set; } - - /// - /// The duration to display the frame for. - /// - public double Duration { get; set; } - - /// - /// Constructs new frame data with the given content and duration. - /// - /// The content of the frame. - /// The duration the frame will be displayed for. - public FrameData(T content, double duration) - { - Content = content; - Duration = duration; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Animations +{ + /// + /// Represents all data necessary to describe a single frame of an . + /// + /// The type of animation the frame data is for. + public struct FrameData + { + /// + /// The contents to display for the frame. + /// + public T Content { get; set; } + + /// + /// The duration to display the frame for. + /// + public double Duration { get; set; } + + /// + /// Constructs new frame data with the given content and duration. + /// + /// The content of the frame. + /// The duration the frame will be displayed for. + public FrameData(T content, double duration) + { + Content = content; + Duration = duration; + } + } +} diff --git a/osu.Framework/Graphics/Animations/IAnimation.cs b/osu.Framework/Graphics/Animations/IAnimation.cs index 20ed9bb47..0902acfe4 100644 --- a/osu.Framework/Graphics/Animations/IAnimation.cs +++ b/osu.Framework/Graphics/Animations/IAnimation.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - - -namespace osu.Framework.Graphics.Animations -{ - /// - /// Represents a generic, frame-based animation. - /// - public interface IAnimation - { - /// - /// The number of frames this animation has. - /// - int FrameCount { get; } - - /// - /// True if the animation is playing, false otherwise. - /// - bool IsPlaying { get; set; } - - /// - /// True if the animation should start over from the first frame after finishing. False if it should stop playing and keep displaying the last frame when finishing. - /// - bool Repeat { get; set; } - - /// - /// Displays the frame with the given zero-based frame index. - /// - /// The zero-based index of the frame to display. - void GotoFrame(int frameIndex); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + + +namespace osu.Framework.Graphics.Animations +{ + /// + /// Represents a generic, frame-based animation. + /// + public interface IAnimation + { + /// + /// The number of frames this animation has. + /// + int FrameCount { get; } + + /// + /// True if the animation is playing, false otherwise. + /// + bool IsPlaying { get; set; } + + /// + /// True if the animation should start over from the first frame after finishing. False if it should stop playing and keep displaying the last frame when finishing. + /// + bool Repeat { get; set; } + + /// + /// Displays the frame with the given zero-based frame index. + /// + /// The zero-based index of the frame to display. + void GotoFrame(int frameIndex); + } +} diff --git a/osu.Framework/Graphics/Animations/TextureAnimation.cs b/osu.Framework/Graphics/Animations/TextureAnimation.cs index a08605e43..ebe006496 100644 --- a/osu.Framework/Graphics/Animations/TextureAnimation.cs +++ b/osu.Framework/Graphics/Animations/TextureAnimation.cs @@ -1,71 +1,71 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; - -namespace osu.Framework.Graphics.Animations -{ - /// - /// An animation that switches the displayed texture when a new frame is displayed. - /// - public class TextureAnimation : Animation - { - private readonly Sprite textureHolder; - private Vector2 maxTextureSize = Vector2.Zero; - - public TextureAnimation() - { - Child = textureHolder = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - - protected override void OnFrameAdded(Texture content, double displayDuration) - { - base.OnFrameAdded(content, displayDuration); - - maxTextureSize = Vector2.ComponentMax(new Vector2(content?.DisplayWidth ?? 0, content?.DisplayHeight ?? 0), maxTextureSize); - } - - protected override void DisplayFrame(Texture content) - { - textureHolder.Texture = content; - updateTextureHolderSize(); - } - - protected override void OnSizingChanged() - { - base.OnSizingChanged(); - - if (textureHolder == null) - return; - - textureHolder.RelativeSizeAxes = ~AutoSizeAxes; - updateTextureHolderSize(); - } - - private void updateTextureHolderSize() - { - var newSize = Vector2.Zero; - var content = textureHolder.Texture; - if (content == null) - return; - - if ((AutoSizeAxes & Axes.X) != 0) - newSize.X = content.DisplayWidth; - else - newSize.X = content.DisplayWidth / maxTextureSize.X; - - if ((AutoSizeAxes & Axes.Y) != 0) - newSize.Y = content.DisplayHeight; - else - newSize.Y = content.DisplayHeight / maxTextureSize.Y; - - textureHolder.Size = newSize; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Framework.Graphics.Animations +{ + /// + /// An animation that switches the displayed texture when a new frame is displayed. + /// + public class TextureAnimation : Animation + { + private readonly Sprite textureHolder; + private Vector2 maxTextureSize = Vector2.Zero; + + public TextureAnimation() + { + Child = textureHolder = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void OnFrameAdded(Texture content, double displayDuration) + { + base.OnFrameAdded(content, displayDuration); + + maxTextureSize = Vector2.ComponentMax(new Vector2(content?.DisplayWidth ?? 0, content?.DisplayHeight ?? 0), maxTextureSize); + } + + protected override void DisplayFrame(Texture content) + { + textureHolder.Texture = content; + updateTextureHolderSize(); + } + + protected override void OnSizingChanged() + { + base.OnSizingChanged(); + + if (textureHolder == null) + return; + + textureHolder.RelativeSizeAxes = ~AutoSizeAxes; + updateTextureHolderSize(); + } + + private void updateTextureHolderSize() + { + var newSize = Vector2.Zero; + var content = textureHolder.Texture; + if (content == null) + return; + + if ((AutoSizeAxes & Axes.X) != 0) + newSize.X = content.DisplayWidth; + else + newSize.X = content.DisplayWidth / maxTextureSize.X; + + if ((AutoSizeAxes & Axes.Y) != 0) + newSize.Y = content.DisplayHeight; + else + newSize.Y = content.DisplayHeight / maxTextureSize.Y; + + textureHolder.Size = newSize; + } + } +} diff --git a/osu.Framework/Graphics/Audio/WaveformGraph.cs b/osu.Framework/Graphics/Audio/WaveformGraph.cs index 6bb100eb2..289f5df73 100644 --- a/osu.Framework/Graphics/Audio/WaveformGraph.cs +++ b/osu.Framework/Graphics/Audio/WaveformGraph.cs @@ -1,224 +1,224 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Threading; -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using OpenTK; -using osu.Framework.Graphics.OpenGL; - -namespace osu.Framework.Graphics.Audio -{ - /// - /// Visualises the waveform for an audio stream. - /// - public class WaveformGraph : Drawable - { - private Shader shader; - private readonly Texture texture; - - public WaveformGraph() - { - texture = Texture.WhitePixel; - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); - } - - private float resolution = 1; - /// - /// Gets or sets the amount of 's displayed relative to . - /// - public float Resolution - { - get { return resolution; } - set - { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value)); - - if (resolution == value) - return; - resolution = value; - - cancelGeneration(); - } - } - - private Waveform waveform; - /// - /// The to display. - /// - public Waveform Waveform - { - get { return waveform; } - set - { - if (waveform == value) - return; - - waveform = value; - - cancelGeneration(); - } - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - var result = base.Invalidate(invalidation, source, shallPropagate); - - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - cancelGeneration(); - - return result; - } - - protected override void Update() - { - base.Update(); - - if (cancelSource == null) - generate(); - } - - private CancellationTokenSource cancelSource = new CancellationTokenSource(); - - private Waveform generatedWaveform; - - private void generate() - { - if (Waveform == null) - return; - - cancelSource = new CancellationTokenSource(); - - Waveform.GenerateResampledAsync((int)Math.Max(0, Math.Ceiling(DrawWidth * Scale.X) * Resolution), cancelSource.Token).ContinueWith(w => - { - generatedWaveform = w.Result; - Schedule(() => Invalidate(Invalidation.DrawNode)); - }, cancelSource.Token); - } - - private void cancelGeneration() - { - cancelSource?.Cancel(); - cancelSource?.Dispose(); - cancelSource = null; - } - - private readonly WaveformDrawNodeSharedData sharedData = new WaveformDrawNodeSharedData(); - protected override DrawNode CreateDrawNode() => new WaveformDrawNode(); - protected override void ApplyDrawNode(DrawNode node) - { - var n = (WaveformDrawNode)node; - - n.Shader = shader; - n.Texture = texture; - n.DrawSize = DrawSize; - n.Shared = sharedData; - n.Points = generatedWaveform?.GetPoints(); - n.Channels = generatedWaveform?.GetChannels() ?? 0; - - base.ApplyDrawNode(node); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - cancelGeneration(); - } - - private class WaveformDrawNodeSharedData - { - public readonly QuadBatch VertexBatch = new QuadBatch(1000, 10); - } - - private class WaveformDrawNode : DrawNode - { - public Shader Shader; - public Texture Texture; - - public WaveformDrawNodeSharedData Shared; - - public IReadOnlyList Points; - public Vector2 DrawSize; - public int Channels; - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - if (Points == null || Points.Count == 0) - return; - - Shader.Bind(); - Texture.TextureGL.Bind(); - - Vector2 localInflationAmount = new Vector2(0, 1) * DrawInfo.MatrixInverse.ExtractScale().Xy; - - // We're dealing with a _large_ number of points, so we need to optimise the quadToDraw * drawInfo.Matrix multiplications below - // for points that are going to be masked out anyway. This allows for higher resolution graphs at larger scales with virtually no performance loss. - // Since the points are generated in the local coordinate space, we need to convert the screen space masking quad coordinates into the local coordinate space - RectangleF localMaskingRectangle = (Quad.FromRectangle(GLWrapper.CurrentMaskingInfo.ScreenSpaceAABB) * DrawInfo.MatrixInverse).AABBFloat; - - float separation = DrawSize.X / (Points.Count - 1); - - for (int i = 0; i < Points.Count - 1; i++) - { - float leftX = i * separation; - float rightX = (i + 1) * separation; - - if (rightX < localMaskingRectangle.Left) - continue; - if (leftX > localMaskingRectangle.Right) - break; // X is always increasing - - ColourInfo colour = DrawInfo.Colour; - Quad quadToDraw; - - switch (Channels) - { - default: - case 2: - { - float height = DrawSize.Y / 2; - quadToDraw = new Quad( - new Vector2(leftX, height - Points[i].Amplitude[0] * height), - new Vector2(rightX, height - Points[i + 1].Amplitude[0] * height), - new Vector2(leftX, height + Points[i].Amplitude[1] * height), - new Vector2(rightX, height + Points[i + 1].Amplitude[1] * height) - ); - } - break; - case 1: - { - quadToDraw = new Quad( - new Vector2(leftX, DrawSize.Y - Points[i].Amplitude[0] * DrawSize.Y), - new Vector2(rightX, DrawSize.Y - Points[i + 1].Amplitude[0] * DrawSize.Y), - new Vector2(leftX, DrawSize.Y), - new Vector2(rightX, DrawSize.Y) - ); - break; - } - } - - quadToDraw *= DrawInfo.Matrix; - Texture.DrawQuad(quadToDraw, colour, null, Shared.VertexBatch.AddAction, Vector2.Divide(localInflationAmount, quadToDraw.Size)); - } - - Shader.Unbind(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using OpenTK; +using osu.Framework.Graphics.OpenGL; + +namespace osu.Framework.Graphics.Audio +{ + /// + /// Visualises the waveform for an audio stream. + /// + public class WaveformGraph : Drawable + { + private Shader shader; + private readonly Texture texture; + + public WaveformGraph() + { + texture = Texture.WhitePixel; + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + } + + private float resolution = 1; + /// + /// Gets or sets the amount of 's displayed relative to . + /// + public float Resolution + { + get { return resolution; } + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + if (resolution == value) + return; + resolution = value; + + cancelGeneration(); + } + } + + private Waveform waveform; + /// + /// The to display. + /// + public Waveform Waveform + { + get { return waveform; } + set + { + if (waveform == value) + return; + + waveform = value; + + cancelGeneration(); + } + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + var result = base.Invalidate(invalidation, source, shallPropagate); + + if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) + cancelGeneration(); + + return result; + } + + protected override void Update() + { + base.Update(); + + if (cancelSource == null) + generate(); + } + + private CancellationTokenSource cancelSource = new CancellationTokenSource(); + + private Waveform generatedWaveform; + + private void generate() + { + if (Waveform == null) + return; + + cancelSource = new CancellationTokenSource(); + + Waveform.GenerateResampledAsync((int)Math.Max(0, Math.Ceiling(DrawWidth * Scale.X) * Resolution), cancelSource.Token).ContinueWith(w => + { + generatedWaveform = w.Result; + Schedule(() => Invalidate(Invalidation.DrawNode)); + }, cancelSource.Token); + } + + private void cancelGeneration() + { + cancelSource?.Cancel(); + cancelSource?.Dispose(); + cancelSource = null; + } + + private readonly WaveformDrawNodeSharedData sharedData = new WaveformDrawNodeSharedData(); + protected override DrawNode CreateDrawNode() => new WaveformDrawNode(); + protected override void ApplyDrawNode(DrawNode node) + { + var n = (WaveformDrawNode)node; + + n.Shader = shader; + n.Texture = texture; + n.DrawSize = DrawSize; + n.Shared = sharedData; + n.Points = generatedWaveform?.GetPoints(); + n.Channels = generatedWaveform?.GetChannels() ?? 0; + + base.ApplyDrawNode(node); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + cancelGeneration(); + } + + private class WaveformDrawNodeSharedData + { + public readonly QuadBatch VertexBatch = new QuadBatch(1000, 10); + } + + private class WaveformDrawNode : DrawNode + { + public Shader Shader; + public Texture Texture; + + public WaveformDrawNodeSharedData Shared; + + public IReadOnlyList Points; + public Vector2 DrawSize; + public int Channels; + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + if (Points == null || Points.Count == 0) + return; + + Shader.Bind(); + Texture.TextureGL.Bind(); + + Vector2 localInflationAmount = new Vector2(0, 1) * DrawInfo.MatrixInverse.ExtractScale().Xy; + + // We're dealing with a _large_ number of points, so we need to optimise the quadToDraw * drawInfo.Matrix multiplications below + // for points that are going to be masked out anyway. This allows for higher resolution graphs at larger scales with virtually no performance loss. + // Since the points are generated in the local coordinate space, we need to convert the screen space masking quad coordinates into the local coordinate space + RectangleF localMaskingRectangle = (Quad.FromRectangle(GLWrapper.CurrentMaskingInfo.ScreenSpaceAABB) * DrawInfo.MatrixInverse).AABBFloat; + + float separation = DrawSize.X / (Points.Count - 1); + + for (int i = 0; i < Points.Count - 1; i++) + { + float leftX = i * separation; + float rightX = (i + 1) * separation; + + if (rightX < localMaskingRectangle.Left) + continue; + if (leftX > localMaskingRectangle.Right) + break; // X is always increasing + + ColourInfo colour = DrawInfo.Colour; + Quad quadToDraw; + + switch (Channels) + { + default: + case 2: + { + float height = DrawSize.Y / 2; + quadToDraw = new Quad( + new Vector2(leftX, height - Points[i].Amplitude[0] * height), + new Vector2(rightX, height - Points[i + 1].Amplitude[0] * height), + new Vector2(leftX, height + Points[i].Amplitude[1] * height), + new Vector2(rightX, height + Points[i + 1].Amplitude[1] * height) + ); + } + break; + case 1: + { + quadToDraw = new Quad( + new Vector2(leftX, DrawSize.Y - Points[i].Amplitude[0] * DrawSize.Y), + new Vector2(rightX, DrawSize.Y - Points[i + 1].Amplitude[0] * DrawSize.Y), + new Vector2(leftX, DrawSize.Y), + new Vector2(rightX, DrawSize.Y) + ); + break; + } + } + + quadToDraw *= DrawInfo.Matrix; + Texture.DrawQuad(quadToDraw, colour, null, Shared.VertexBatch.AddAction, Vector2.Divide(localInflationAmount, quadToDraw.Size)); + } + + Shader.Unbind(); + } + } + } +} diff --git a/osu.Framework/Graphics/Batches/IVertexBatch.cs b/osu.Framework/Graphics/Batches/IVertexBatch.cs index 355e50e59..90b204d13 100644 --- a/osu.Framework/Graphics/Batches/IVertexBatch.cs +++ b/osu.Framework/Graphics/Batches/IVertexBatch.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Batches -{ - public interface IVertexBatch - { - int Draw(); - - void ResetCounters(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Batches +{ + public interface IVertexBatch + { + int Draw(); + + void ResetCounters(); + } +} diff --git a/osu.Framework/Graphics/Batches/LinearBatch.cs b/osu.Framework/Graphics/Batches/LinearBatch.cs index c7e66b150..6af48fcad 100644 --- a/osu.Framework/Graphics/Batches/LinearBatch.cs +++ b/osu.Framework/Graphics/Batches/LinearBatch.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Buffers; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.Batches -{ - public class LinearBatch : VertexBatch - where T : struct, IEquatable, IVertex - { - private readonly PrimitiveType type; - - public LinearBatch(int size, int maxBuffers, PrimitiveType type) - : base(size, maxBuffers) - { - this.type = type; - } - - protected override VertexBuffer CreateVertexBuffer() => new LinearVertexBuffer(Size, type, BufferUsageHint.DynamicDraw); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Buffers; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.Batches +{ + public class LinearBatch : VertexBatch + where T : struct, IEquatable, IVertex + { + private readonly PrimitiveType type; + + public LinearBatch(int size, int maxBuffers, PrimitiveType type) + : base(size, maxBuffers) + { + this.type = type; + } + + protected override VertexBuffer CreateVertexBuffer() => new LinearVertexBuffer(Size, type, BufferUsageHint.DynamicDraw); + } +} diff --git a/osu.Framework/Graphics/Batches/QuadBatch.cs b/osu.Framework/Graphics/Batches/QuadBatch.cs index 2a0afc830..39e5496d9 100644 --- a/osu.Framework/Graphics/Batches/QuadBatch.cs +++ b/osu.Framework/Graphics/Batches/QuadBatch.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Buffers; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.Batches -{ - public class QuadBatch : VertexBatch - where T : struct, IEquatable, IVertex - { - public QuadBatch(int size, int maxBuffers) - : base(size, maxBuffers) - { - } - - protected override VertexBuffer CreateVertexBuffer() => new QuadVertexBuffer(Size, BufferUsageHint.DynamicDraw); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Buffers; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.Batches +{ + public class QuadBatch : VertexBatch + where T : struct, IEquatable, IVertex + { + public QuadBatch(int size, int maxBuffers) + : base(size, maxBuffers) + { + } + + protected override VertexBuffer CreateVertexBuffer() => new QuadVertexBuffer(Size, BufferUsageHint.DynamicDraw); + } +} diff --git a/osu.Framework/Graphics/Batches/VertexBatch.cs b/osu.Framework/Graphics/Batches/VertexBatch.cs index 1599da2f4..02cc8c882 100644 --- a/osu.Framework/Graphics/Batches/VertexBatch.cs +++ b/osu.Framework/Graphics/Batches/VertexBatch.cs @@ -1,143 +1,143 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.OpenGL.Buffers; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Statistics; - -namespace osu.Framework.Graphics.Batches -{ - public abstract class VertexBatch : IVertexBatch, IDisposable - where T : struct, IEquatable, IVertex - { - public List> VertexBuffers = new List>(); - - /// - /// The number of vertices in each VertexBuffer. - /// - public int Size { get; } - - private int changeBeginIndex = -1; - private int changeEndIndex = -1; - - private int currentIndex; - private int currentVertex; - private int lastVertex; - - private readonly int maxBuffers; - - private VertexBuffer currentVertexBuffer => VertexBuffers[currentIndex]; - - protected VertexBatch(int bufferSize, int maxBuffers) - { - // Vertex buffers of size 0 don't make any sense. Let's not blindly hope for good behavior of OpenGL. - Trace.Assert(bufferSize > 0); - - Size = bufferSize; - this.maxBuffers = maxBuffers; - - AddAction = Add; - } - - #region Disposal - - ~VertexBatch() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - if (disposing) - foreach (VertexBuffer vbo in VertexBuffers) - vbo.Dispose(); - } - - #endregion - - public void ResetCounters() - { - changeBeginIndex = -1; - currentIndex = 0; - currentVertex = 0; - lastVertex = 0; - } - - protected abstract VertexBuffer CreateVertexBuffer(); - - /// - /// Adds a vertex to this . - /// - /// The vertex to add. - public void Add(T v) - { - GLWrapper.SetActiveBatch(this); - - while (currentIndex >= VertexBuffers.Count) - VertexBuffers.Add(CreateVertexBuffer()); - - VertexBuffer vertexBuffer = currentVertexBuffer; - - if (!vertexBuffer.Vertices[currentVertex].Equals(v)) - { - if (changeBeginIndex == -1) - changeBeginIndex = currentVertex; - - changeEndIndex = currentVertex + 1; - } - - vertexBuffer.Vertices[currentVertex] = v; - ++currentVertex; - - if (currentVertex >= vertexBuffer.Vertices.Length) - { - Draw(); - FrameStatistics.Increment(StatisticsCounterType.VBufOverflow); - lastVertex = currentVertex = 0; - } - } - - /// - /// Adds a vertex to this . - /// This is a cached delegate of that should be used in memory-critical locations such as s. - /// - public readonly Action AddAction; - - public int Draw() - { - if (currentVertex == lastVertex) - return 0; - - VertexBuffer vertexBuffer = currentVertexBuffer; - if (changeBeginIndex >= 0) - vertexBuffer.UpdateRange(changeBeginIndex, changeEndIndex); - - vertexBuffer.DrawRange(lastVertex, currentVertex); - - int count = currentVertex - lastVertex; - - // When using multiple buffers we advance to the next one with every draw to prevent contention on the same buffer with future vertex updates. - //TODO: let us know if we exceed and roll over to zero here. - currentIndex = (currentIndex + 1) % maxBuffers; - currentVertex = 0; - - lastVertex = currentVertex; - changeBeginIndex = -1; - - FrameStatistics.Increment(StatisticsCounterType.DrawCalls); - FrameStatistics.Add(StatisticsCounterType.VerticesDraw, count); - - return count; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.OpenGL.Buffers; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Statistics; + +namespace osu.Framework.Graphics.Batches +{ + public abstract class VertexBatch : IVertexBatch, IDisposable + where T : struct, IEquatable, IVertex + { + public List> VertexBuffers = new List>(); + + /// + /// The number of vertices in each VertexBuffer. + /// + public int Size { get; } + + private int changeBeginIndex = -1; + private int changeEndIndex = -1; + + private int currentIndex; + private int currentVertex; + private int lastVertex; + + private readonly int maxBuffers; + + private VertexBuffer currentVertexBuffer => VertexBuffers[currentIndex]; + + protected VertexBatch(int bufferSize, int maxBuffers) + { + // Vertex buffers of size 0 don't make any sense. Let's not blindly hope for good behavior of OpenGL. + Trace.Assert(bufferSize > 0); + + Size = bufferSize; + this.maxBuffers = maxBuffers; + + AddAction = Add; + } + + #region Disposal + + ~VertexBatch() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing) + foreach (VertexBuffer vbo in VertexBuffers) + vbo.Dispose(); + } + + #endregion + + public void ResetCounters() + { + changeBeginIndex = -1; + currentIndex = 0; + currentVertex = 0; + lastVertex = 0; + } + + protected abstract VertexBuffer CreateVertexBuffer(); + + /// + /// Adds a vertex to this . + /// + /// The vertex to add. + public void Add(T v) + { + GLWrapper.SetActiveBatch(this); + + while (currentIndex >= VertexBuffers.Count) + VertexBuffers.Add(CreateVertexBuffer()); + + VertexBuffer vertexBuffer = currentVertexBuffer; + + if (!vertexBuffer.Vertices[currentVertex].Equals(v)) + { + if (changeBeginIndex == -1) + changeBeginIndex = currentVertex; + + changeEndIndex = currentVertex + 1; + } + + vertexBuffer.Vertices[currentVertex] = v; + ++currentVertex; + + if (currentVertex >= vertexBuffer.Vertices.Length) + { + Draw(); + FrameStatistics.Increment(StatisticsCounterType.VBufOverflow); + lastVertex = currentVertex = 0; + } + } + + /// + /// Adds a vertex to this . + /// This is a cached delegate of that should be used in memory-critical locations such as s. + /// + public readonly Action AddAction; + + public int Draw() + { + if (currentVertex == lastVertex) + return 0; + + VertexBuffer vertexBuffer = currentVertexBuffer; + if (changeBeginIndex >= 0) + vertexBuffer.UpdateRange(changeBeginIndex, changeEndIndex); + + vertexBuffer.DrawRange(lastVertex, currentVertex); + + int count = currentVertex - lastVertex; + + // When using multiple buffers we advance to the next one with every draw to prevent contention on the same buffer with future vertex updates. + //TODO: let us know if we exceed and roll over to zero here. + currentIndex = (currentIndex + 1) % maxBuffers; + currentVertex = 0; + + lastVertex = currentVertex; + changeBeginIndex = -1; + + FrameStatistics.Increment(StatisticsCounterType.DrawCalls); + FrameStatistics.Add(StatisticsCounterType.VerticesDraw, count); + + return count; + } + } +} diff --git a/osu.Framework/Graphics/BlendingInfo.cs b/osu.Framework/Graphics/BlendingInfo.cs index c17816885..62e5f8a10 100644 --- a/osu.Framework/Graphics/BlendingInfo.cs +++ b/osu.Framework/Graphics/BlendingInfo.cs @@ -1,83 +1,83 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics -{ - public struct BlendingInfo - { - public BlendingFactorSrc Source; - public BlendingFactorDest Destination; - public BlendingFactorSrc SourceAlpha; - public BlendingFactorDest DestinationAlpha; - - public BlendEquationMode RGBEquation; - public BlendEquationMode AlphaEquation; - - public BlendingInfo(BlendingParameters parameters) - { - switch (parameters.Mode) - { - case BlendingMode.Inherit: - case BlendingMode.Mixture: - Source = BlendingFactorSrc.SrcAlpha; - Destination = BlendingFactorDest.OneMinusSrcAlpha; - SourceAlpha = BlendingFactorSrc.One; - DestinationAlpha = BlendingFactorDest.One; - break; - case BlendingMode.Additive: - Source = BlendingFactorSrc.SrcAlpha; - Destination = BlendingFactorDest.One; - SourceAlpha = BlendingFactorSrc.One; - DestinationAlpha = BlendingFactorDest.One; - break; - default: - Source = BlendingFactorSrc.One; - Destination = BlendingFactorDest.Zero; - SourceAlpha = BlendingFactorSrc.One; - DestinationAlpha = BlendingFactorDest.Zero; - break; - } - - RGBEquation = translateEquation(parameters.RGBEquation); - AlphaEquation = translateEquation(parameters.AlphaEquation); - - } - - private static BlendEquationMode translateEquation(BlendingEquation blendingEquation) - { - switch (blendingEquation) - { - default: - case BlendingEquation.Inherit: - case BlendingEquation.Add: - return BlendEquationMode.FuncAdd; - case BlendingEquation.Min: - return BlendEquationMode.Min; - case BlendingEquation.Max: - return BlendEquationMode.Max; - case BlendingEquation.Subtract: - return BlendEquationMode.FuncSubtract; - case BlendingEquation.ReverseSubtract: - return BlendEquationMode.FuncReverseSubtract; - } - } - - public bool IsDisabled => - Source == BlendingFactorSrc.One - && Destination == BlendingFactorDest.Zero - && SourceAlpha == BlendingFactorSrc.One - && DestinationAlpha == BlendingFactorDest.Zero - && RGBEquation == BlendEquationMode.FuncAdd - && AlphaEquation == BlendEquationMode.FuncAdd; - - public bool Equals(BlendingInfo other) => - other.Source == Source - && other.Destination == Destination - && other.SourceAlpha == SourceAlpha - && other.DestinationAlpha == DestinationAlpha - && other.RGBEquation == RGBEquation - && other.AlphaEquation == AlphaEquation; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics +{ + public struct BlendingInfo + { + public BlendingFactorSrc Source; + public BlendingFactorDest Destination; + public BlendingFactorSrc SourceAlpha; + public BlendingFactorDest DestinationAlpha; + + public BlendEquationMode RGBEquation; + public BlendEquationMode AlphaEquation; + + public BlendingInfo(BlendingParameters parameters) + { + switch (parameters.Mode) + { + case BlendingMode.Inherit: + case BlendingMode.Mixture: + Source = BlendingFactorSrc.SrcAlpha; + Destination = BlendingFactorDest.OneMinusSrcAlpha; + SourceAlpha = BlendingFactorSrc.One; + DestinationAlpha = BlendingFactorDest.One; + break; + case BlendingMode.Additive: + Source = BlendingFactorSrc.SrcAlpha; + Destination = BlendingFactorDest.One; + SourceAlpha = BlendingFactorSrc.One; + DestinationAlpha = BlendingFactorDest.One; + break; + default: + Source = BlendingFactorSrc.One; + Destination = BlendingFactorDest.Zero; + SourceAlpha = BlendingFactorSrc.One; + DestinationAlpha = BlendingFactorDest.Zero; + break; + } + + RGBEquation = translateEquation(parameters.RGBEquation); + AlphaEquation = translateEquation(parameters.AlphaEquation); + + } + + private static BlendEquationMode translateEquation(BlendingEquation blendingEquation) + { + switch (blendingEquation) + { + default: + case BlendingEquation.Inherit: + case BlendingEquation.Add: + return BlendEquationMode.FuncAdd; + case BlendingEquation.Min: + return BlendEquationMode.Min; + case BlendingEquation.Max: + return BlendEquationMode.Max; + case BlendingEquation.Subtract: + return BlendEquationMode.FuncSubtract; + case BlendingEquation.ReverseSubtract: + return BlendEquationMode.FuncReverseSubtract; + } + } + + public bool IsDisabled => + Source == BlendingFactorSrc.One + && Destination == BlendingFactorDest.Zero + && SourceAlpha == BlendingFactorSrc.One + && DestinationAlpha == BlendingFactorDest.Zero + && RGBEquation == BlendEquationMode.FuncAdd + && AlphaEquation == BlendEquationMode.FuncAdd; + + public bool Equals(BlendingInfo other) => + other.Source == Source + && other.Destination == Destination + && other.SourceAlpha == SourceAlpha + && other.DestinationAlpha == DestinationAlpha + && other.RGBEquation == RGBEquation + && other.AlphaEquation == AlphaEquation; + } +} diff --git a/osu.Framework/Graphics/BlendingParameters.cs b/osu.Framework/Graphics/BlendingParameters.cs index 2d090cbec..681b2b8c6 100644 --- a/osu.Framework/Graphics/BlendingParameters.cs +++ b/osu.Framework/Graphics/BlendingParameters.cs @@ -1,83 +1,83 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics -{ - /// - /// Contains information about how an should be blended into its destination. - /// - public struct BlendingParameters - { - /// - /// Gets or sets to use. - /// - public BlendingMode Mode; - - /// - /// Gets or sets the to use for the RGB components of the blend. - /// - public BlendingEquation RGBEquation; - - /// - /// Gets or sets the to use for the alpha component of the blend. - /// - public BlendingEquation AlphaEquation; - - public static implicit operator BlendingParameters(BlendingMode blendingMode) => new BlendingParameters { Mode = blendingMode }; - public static implicit operator BlendingParameters(BlendingEquation blendingEquation) => new BlendingParameters - { - RGBEquation = blendingEquation, - AlphaEquation = blendingEquation - }; - - public bool Equals(BlendingParameters other) => other.Mode == Mode && other.RGBEquation == RGBEquation && other.AlphaEquation == AlphaEquation; - } - - public enum BlendingMode - { - /// - /// Inherits from parent. - /// - Inherit = 0, - /// - /// Mixes with existing colour by a factor of the colour's alpha. - /// - Mixture, - /// - /// Purely additive (by a factor of the colour's alpha) blending. - /// - Additive, - /// - /// No alpha blending whatsoever. - /// - None, - } - - public enum BlendingEquation - { - /// - /// Inherits from parent. - /// - Inherit = 0, - /// - /// Adds the source and destination colours. - /// - Add, - /// - /// Chooses the minimum of each component of the source and destination colours. - /// - Min, - /// - /// Chooses the maximum of each component of the source and destination colours. - /// - Max, - /// - /// Subtracts the destination colour from the source colour. - /// - Subtract, - /// - /// Subtracts the source colour from the destination colour. - /// - ReverseSubtract, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics +{ + /// + /// Contains information about how an should be blended into its destination. + /// + public struct BlendingParameters + { + /// + /// Gets or sets to use. + /// + public BlendingMode Mode; + + /// + /// Gets or sets the to use for the RGB components of the blend. + /// + public BlendingEquation RGBEquation; + + /// + /// Gets or sets the to use for the alpha component of the blend. + /// + public BlendingEquation AlphaEquation; + + public static implicit operator BlendingParameters(BlendingMode blendingMode) => new BlendingParameters { Mode = blendingMode }; + public static implicit operator BlendingParameters(BlendingEquation blendingEquation) => new BlendingParameters + { + RGBEquation = blendingEquation, + AlphaEquation = blendingEquation + }; + + public bool Equals(BlendingParameters other) => other.Mode == Mode && other.RGBEquation == RGBEquation && other.AlphaEquation == AlphaEquation; + } + + public enum BlendingMode + { + /// + /// Inherits from parent. + /// + Inherit = 0, + /// + /// Mixes with existing colour by a factor of the colour's alpha. + /// + Mixture, + /// + /// Purely additive (by a factor of the colour's alpha) blending. + /// + Additive, + /// + /// No alpha blending whatsoever. + /// + None, + } + + public enum BlendingEquation + { + /// + /// Inherits from parent. + /// + Inherit = 0, + /// + /// Adds the source and destination colours. + /// + Add, + /// + /// Chooses the minimum of each component of the source and destination colours. + /// + Min, + /// + /// Chooses the maximum of each component of the source and destination colours. + /// + Max, + /// + /// Subtracts the destination colour from the source colour. + /// + Subtract, + /// + /// Subtracts the source colour from the destination colour. + /// + ReverseSubtract, + } +} diff --git a/osu.Framework/Graphics/Colour/ColourInfo.cs b/osu.Framework/Graphics/Colour/ColourInfo.cs index 8d46100fb..eefe54958 100644 --- a/osu.Framework/Graphics/Colour/ColourInfo.cs +++ b/osu.Framework/Graphics/Colour/ColourInfo.cs @@ -1,214 +1,214 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; -using osu.Framework.Graphics.Primitives; -using System.Diagnostics; -using OpenTK.Graphics; - -namespace osu.Framework.Graphics.Colour -{ - /// - /// ColourInfo contains information about the colours of all 4 vertices of a quad. - /// These colours are always stored in linear space. - /// - public struct ColourInfo : IEquatable - { - public SRGBColour TopLeft; - public SRGBColour BottomLeft; - public SRGBColour TopRight; - public SRGBColour BottomRight; - public bool HasSingleColour; - - /// - /// Creates a ColourInfo with a single linear colour assigned to all vertices. - /// - /// The single linear colour to be assigned to all vertices. - /// The created ColourInfo. - public static ColourInfo SingleColour(SRGBColour colour) - { - ColourInfo result = new ColourInfo(); - result.TopLeft = result.BottomLeft = result.TopRight = result.BottomRight = colour; - result.HasSingleColour = true; - return result; - } - - /// - /// Creates a ColourInfo with a horizontal gradient. - /// - /// The left colour of the gradient. - /// The right colour of the gradient. - /// The created ColourInfo. - public static ColourInfo GradientHorizontal(SRGBColour c1, SRGBColour c2) - { - ColourInfo result = new ColourInfo(); - result.TopLeft = result.BottomLeft = c1; - result.TopRight = result.BottomRight = c2; - result.HasSingleColour = false; - return result; - } - - /// - /// Creates a ColourInfo with a horizontal gradient. - /// - /// The top colour of the gradient. - /// The bottom colour of the gradient. - /// The created ColourInfo. - public static ColourInfo GradientVertical(SRGBColour c1, SRGBColour c2) - { - ColourInfo result = new ColourInfo(); - result.TopLeft = result.TopRight = c1; - result.BottomLeft = result.BottomRight = c2; - result.HasSingleColour = false; - return result; - } - - private SRGBColour singleColour - { - get - { - if (!HasSingleColour) - throw new InvalidOperationException("Attempted to read single colour from multi-colour ColourInfo."); - return TopLeft; - } - - set - { - TopLeft = BottomLeft = TopRight = BottomRight = value; - HasSingleColour = true; - } - } - - public SRGBColour Interpolate(Vector2 interp) => SRGBColour.FromVector( - (1 - interp.Y) * ((1 - interp.X) * TopLeft.ToVector() + interp.X * TopRight.ToVector()) + - interp.Y * ((1 - interp.X) * BottomLeft.ToVector() + interp.X * BottomRight.ToVector())); - - public void ApplyChild(ColourInfo childColour) - { - if (!HasSingleColour) - { - ApplyChild(childColour, new Quad(0, 0, 1, 1)); - return; - } - - if (childColour.HasSingleColour) - singleColour *= childColour.singleColour; - else - { - HasSingleColour = false; - BottomLeft = childColour.BottomLeft * TopLeft; - TopRight = childColour.TopRight * TopLeft; - BottomRight = childColour.BottomRight * TopLeft; - - // Need to assign TopLeft last to keep correctness. - TopLeft = childColour.TopLeft * TopLeft; - } - } - - public void ApplyChild(ColourInfo childColour, Quad interp) - { - Trace.Assert(!HasSingleColour); - - SRGBColour newTopLeft = Interpolate(interp.TopLeft) * childColour.TopLeft; - SRGBColour newTopRight = Interpolate(interp.TopRight) * childColour.TopRight; - SRGBColour newBottomLeft = Interpolate(interp.BottomLeft) * childColour.BottomLeft; - SRGBColour newBottomRight = Interpolate(interp.BottomRight) * childColour.BottomRight; - - TopLeft = newTopLeft; - TopRight = newTopRight; - BottomLeft = newBottomLeft; - BottomRight = newBottomRight; - } - - /// - /// Created a new ColourInfo with the alpha value of the colours of all vertices - /// multiplied by a given alpha parameter. - /// - /// The alpha parameter to multiply the alpha values of all vertices with. - /// The new ColourInfo. - public ColourInfo MultiplyAlpha(float alpha) - { - if (alpha == 1.0) - return this; - - ColourInfo result = this; - result.TopLeft.MultiplyAlpha(alpha); - - if (HasSingleColour) - result.BottomLeft = result.TopRight = result.BottomRight = result.TopLeft; - else - { - result.BottomLeft.MultiplyAlpha(alpha); - result.TopRight.MultiplyAlpha(alpha); - result.BottomRight.MultiplyAlpha(alpha); - } - - return result; - } - - public bool Equals(ColourInfo other) - { - if (!HasSingleColour) - { - if (other.HasSingleColour) - return false; - - return - TopLeft.Equals(other.TopLeft) && - TopRight.Equals(other.TopRight) && - BottomLeft.Equals(other.BottomLeft) && - BottomRight.Equals(other.BottomRight); - } - - return other.HasSingleColour && TopLeft.Equals(other.TopLeft); - } - - public bool Equals(SRGBColour other) - { - return HasSingleColour && TopLeft.Equals(other); - } - - /// - /// The average colour of all corners. - /// - public SRGBColour AverageColour - { - get - { - if (HasSingleColour) - return TopLeft; - - return SRGBColour.FromVector( - (TopLeft.ToVector() + TopRight.ToVector() + BottomLeft.ToVector() + BottomRight.ToVector()) / 4); - } - } - - /// - /// The maximum alpha value of all four corners. - /// - public float MaxAlpha - { - get - { - float max = TopLeft.Linear.A; - if (TopRight.Linear.A < max) max = TopRight.Linear.A; - if (BottomLeft.Linear.A < max) max = BottomLeft.Linear.A; - if (BottomRight.Linear.A < max) max = BottomRight.Linear.A; - - return max; - } - } - - public override string ToString() => - HasSingleColour ? - $@"{TopLeft} (Single)" : - $@"{TopLeft}, {TopRight}, {BottomLeft}, {BottomRight}"; - - public static implicit operator ColourInfo(SRGBColour colour) => SingleColour(colour); - public static implicit operator SRGBColour(ColourInfo colour) => colour.singleColour; - - public static implicit operator ColourInfo(Color4 colour) => (SRGBColour)colour; - public static implicit operator Color4(ColourInfo colour) => (SRGBColour)colour; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; +using osu.Framework.Graphics.Primitives; +using System.Diagnostics; +using OpenTK.Graphics; + +namespace osu.Framework.Graphics.Colour +{ + /// + /// ColourInfo contains information about the colours of all 4 vertices of a quad. + /// These colours are always stored in linear space. + /// + public struct ColourInfo : IEquatable + { + public SRGBColour TopLeft; + public SRGBColour BottomLeft; + public SRGBColour TopRight; + public SRGBColour BottomRight; + public bool HasSingleColour; + + /// + /// Creates a ColourInfo with a single linear colour assigned to all vertices. + /// + /// The single linear colour to be assigned to all vertices. + /// The created ColourInfo. + public static ColourInfo SingleColour(SRGBColour colour) + { + ColourInfo result = new ColourInfo(); + result.TopLeft = result.BottomLeft = result.TopRight = result.BottomRight = colour; + result.HasSingleColour = true; + return result; + } + + /// + /// Creates a ColourInfo with a horizontal gradient. + /// + /// The left colour of the gradient. + /// The right colour of the gradient. + /// The created ColourInfo. + public static ColourInfo GradientHorizontal(SRGBColour c1, SRGBColour c2) + { + ColourInfo result = new ColourInfo(); + result.TopLeft = result.BottomLeft = c1; + result.TopRight = result.BottomRight = c2; + result.HasSingleColour = false; + return result; + } + + /// + /// Creates a ColourInfo with a horizontal gradient. + /// + /// The top colour of the gradient. + /// The bottom colour of the gradient. + /// The created ColourInfo. + public static ColourInfo GradientVertical(SRGBColour c1, SRGBColour c2) + { + ColourInfo result = new ColourInfo(); + result.TopLeft = result.TopRight = c1; + result.BottomLeft = result.BottomRight = c2; + result.HasSingleColour = false; + return result; + } + + private SRGBColour singleColour + { + get + { + if (!HasSingleColour) + throw new InvalidOperationException("Attempted to read single colour from multi-colour ColourInfo."); + return TopLeft; + } + + set + { + TopLeft = BottomLeft = TopRight = BottomRight = value; + HasSingleColour = true; + } + } + + public SRGBColour Interpolate(Vector2 interp) => SRGBColour.FromVector( + (1 - interp.Y) * ((1 - interp.X) * TopLeft.ToVector() + interp.X * TopRight.ToVector()) + + interp.Y * ((1 - interp.X) * BottomLeft.ToVector() + interp.X * BottomRight.ToVector())); + + public void ApplyChild(ColourInfo childColour) + { + if (!HasSingleColour) + { + ApplyChild(childColour, new Quad(0, 0, 1, 1)); + return; + } + + if (childColour.HasSingleColour) + singleColour *= childColour.singleColour; + else + { + HasSingleColour = false; + BottomLeft = childColour.BottomLeft * TopLeft; + TopRight = childColour.TopRight * TopLeft; + BottomRight = childColour.BottomRight * TopLeft; + + // Need to assign TopLeft last to keep correctness. + TopLeft = childColour.TopLeft * TopLeft; + } + } + + public void ApplyChild(ColourInfo childColour, Quad interp) + { + Trace.Assert(!HasSingleColour); + + SRGBColour newTopLeft = Interpolate(interp.TopLeft) * childColour.TopLeft; + SRGBColour newTopRight = Interpolate(interp.TopRight) * childColour.TopRight; + SRGBColour newBottomLeft = Interpolate(interp.BottomLeft) * childColour.BottomLeft; + SRGBColour newBottomRight = Interpolate(interp.BottomRight) * childColour.BottomRight; + + TopLeft = newTopLeft; + TopRight = newTopRight; + BottomLeft = newBottomLeft; + BottomRight = newBottomRight; + } + + /// + /// Created a new ColourInfo with the alpha value of the colours of all vertices + /// multiplied by a given alpha parameter. + /// + /// The alpha parameter to multiply the alpha values of all vertices with. + /// The new ColourInfo. + public ColourInfo MultiplyAlpha(float alpha) + { + if (alpha == 1.0) + return this; + + ColourInfo result = this; + result.TopLeft.MultiplyAlpha(alpha); + + if (HasSingleColour) + result.BottomLeft = result.TopRight = result.BottomRight = result.TopLeft; + else + { + result.BottomLeft.MultiplyAlpha(alpha); + result.TopRight.MultiplyAlpha(alpha); + result.BottomRight.MultiplyAlpha(alpha); + } + + return result; + } + + public bool Equals(ColourInfo other) + { + if (!HasSingleColour) + { + if (other.HasSingleColour) + return false; + + return + TopLeft.Equals(other.TopLeft) && + TopRight.Equals(other.TopRight) && + BottomLeft.Equals(other.BottomLeft) && + BottomRight.Equals(other.BottomRight); + } + + return other.HasSingleColour && TopLeft.Equals(other.TopLeft); + } + + public bool Equals(SRGBColour other) + { + return HasSingleColour && TopLeft.Equals(other); + } + + /// + /// The average colour of all corners. + /// + public SRGBColour AverageColour + { + get + { + if (HasSingleColour) + return TopLeft; + + return SRGBColour.FromVector( + (TopLeft.ToVector() + TopRight.ToVector() + BottomLeft.ToVector() + BottomRight.ToVector()) / 4); + } + } + + /// + /// The maximum alpha value of all four corners. + /// + public float MaxAlpha + { + get + { + float max = TopLeft.Linear.A; + if (TopRight.Linear.A < max) max = TopRight.Linear.A; + if (BottomLeft.Linear.A < max) max = BottomLeft.Linear.A; + if (BottomRight.Linear.A < max) max = BottomRight.Linear.A; + + return max; + } + } + + public override string ToString() => + HasSingleColour ? + $@"{TopLeft} (Single)" : + $@"{TopLeft}, {TopRight}, {BottomLeft}, {BottomRight}"; + + public static implicit operator ColourInfo(SRGBColour colour) => SingleColour(colour); + public static implicit operator SRGBColour(ColourInfo colour) => colour.singleColour; + + public static implicit operator ColourInfo(Color4 colour) => (SRGBColour)colour; + public static implicit operator Color4(ColourInfo colour) => (SRGBColour)colour; + } +} diff --git a/osu.Framework/Graphics/Colour/SRGBColour.cs b/osu.Framework/Graphics/Colour/SRGBColour.cs index 58d0295a3..27cfdb558 100644 --- a/osu.Framework/Graphics/Colour/SRGBColour.cs +++ b/osu.Framework/Graphics/Colour/SRGBColour.cs @@ -1,71 +1,71 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using System; - -namespace osu.Framework.Graphics.Colour -{ - /// - /// A wrapper struct around Color4 that takes care of converting between sRGB and linear colour spaces. - /// Internally this struct stores the colour in linear space, which is exposed by the Linear member. - /// This struct implicitly converts to sRGB space Color4 values (i.e. it can be assigned and implicitly cast) - /// to sRGB Color4. - /// - public struct SRGBColour : IEquatable - { - public Color4 Linear; - - public static implicit operator SRGBColour(Color4 value) => new SRGBColour { Linear = value.ToLinear() }; - public static implicit operator Color4(SRGBColour value) => value.Linear.ToSRGB(); - - /// - /// Multiplies 2 colours in linear colour space. - /// - /// First factor. - /// Second factor. - /// Product of first and second. - public static SRGBColour operator *(SRGBColour first, SRGBColour second) => new SRGBColour - { - Linear = new Color4( - first.Linear.R * second.Linear.R, - first.Linear.G * second.Linear.G, - first.Linear.B * second.Linear.B, - first.Linear.A * second.Linear.A), - }; - - public static SRGBColour operator *(SRGBColour first, float second) => new SRGBColour - { - Linear = new Color4( - first.Linear.R * second, - first.Linear.G * second, - first.Linear.B * second, - first.Linear.A * second), - }; - - public static SRGBColour operator /(SRGBColour first, float second) => first * (1 / second); - - public static SRGBColour operator +(SRGBColour first, SRGBColour second) => new SRGBColour - { - Linear = new Color4( - first.Linear.R + second.Linear.R, - first.Linear.G + second.Linear.G, - first.Linear.B + second.Linear.B, - first.Linear.A + second.Linear.A), - }; - - public Vector4 ToVector() => new Vector4(Linear.R, Linear.G, Linear.B, Linear.A); - public static SRGBColour FromVector(Vector4 v) => new SRGBColour { Linear = new Color4(v.X, v.Y, v.Z, v.W) }; - - /// - /// Multiplies the alpha value of this colour by the given alpha factor. - /// - /// The alpha factor to multiply with. - public void MultiplyAlpha(float alpha) => Linear.A *= alpha; - - public bool Equals(SRGBColour other) => Linear.Equals(other.Linear); - public override string ToString() => Linear.ToString(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using System; + +namespace osu.Framework.Graphics.Colour +{ + /// + /// A wrapper struct around Color4 that takes care of converting between sRGB and linear colour spaces. + /// Internally this struct stores the colour in linear space, which is exposed by the Linear member. + /// This struct implicitly converts to sRGB space Color4 values (i.e. it can be assigned and implicitly cast) + /// to sRGB Color4. + /// + public struct SRGBColour : IEquatable + { + public Color4 Linear; + + public static implicit operator SRGBColour(Color4 value) => new SRGBColour { Linear = value.ToLinear() }; + public static implicit operator Color4(SRGBColour value) => value.Linear.ToSRGB(); + + /// + /// Multiplies 2 colours in linear colour space. + /// + /// First factor. + /// Second factor. + /// Product of first and second. + public static SRGBColour operator *(SRGBColour first, SRGBColour second) => new SRGBColour + { + Linear = new Color4( + first.Linear.R * second.Linear.R, + first.Linear.G * second.Linear.G, + first.Linear.B * second.Linear.B, + first.Linear.A * second.Linear.A), + }; + + public static SRGBColour operator *(SRGBColour first, float second) => new SRGBColour + { + Linear = new Color4( + first.Linear.R * second, + first.Linear.G * second, + first.Linear.B * second, + first.Linear.A * second), + }; + + public static SRGBColour operator /(SRGBColour first, float second) => first * (1 / second); + + public static SRGBColour operator +(SRGBColour first, SRGBColour second) => new SRGBColour + { + Linear = new Color4( + first.Linear.R + second.Linear.R, + first.Linear.G + second.Linear.G, + first.Linear.B + second.Linear.B, + first.Linear.A + second.Linear.A), + }; + + public Vector4 ToVector() => new Vector4(Linear.R, Linear.G, Linear.B, Linear.A); + public static SRGBColour FromVector(Vector4 v) => new SRGBColour { Linear = new Color4(v.X, v.Y, v.Z, v.W) }; + + /// + /// Multiplies the alpha value of this colour by the given alpha factor. + /// + /// The alpha factor to multiply with. + public void MultiplyAlpha(float alpha) => Linear.A *= alpha; + + public bool Equals(SRGBColour other) => Linear.Equals(other.Linear); + public override string ToString() => Linear.ToString(); + } +} diff --git a/osu.Framework/Graphics/Component.cs b/osu.Framework/Graphics/Component.cs index c0500b398..44a8999df 100644 --- a/osu.Framework/Graphics/Component.cs +++ b/osu.Framework/Graphics/Component.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics -{ - /// - /// An updateable component that can be inserted into the draw hierarchy. - /// This is currently used as a marker for cases where nothing more than load, update, lifetime support and hierarchy presence are required. - /// Eventually this will be fleshed out (and the inheritance will be reversed to Drawable : Component). - /// - public abstract class Component : Drawable - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics +{ + /// + /// An updateable component that can be inserted into the draw hierarchy. + /// This is currently used as a marker for cases where nothing more than load, update, lifetime support and hierarchy presence are required. + /// Eventually this will be fleshed out (and the inheritance will be reversed to Drawable : Component). + /// + public abstract class Component : Drawable + { + } +} diff --git a/osu.Framework/Graphics/Containers/BufferedContainer.cs b/osu.Framework/Graphics/Containers/BufferedContainer.cs index 5f2d7f5f5..8f9b179cd 100644 --- a/osu.Framework/Graphics/Containers/BufferedContainer.cs +++ b/osu.Framework/Graphics/Containers/BufferedContainer.cs @@ -1,408 +1,408 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Buffers; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.MathUtils; -using osu.Framework.Threading; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container that renders its children to an internal framebuffer, and then - /// blits the framebuffer to the screen, instead of directly rendering the children - /// to the screen. This allows otherwise impossible effects to be applied to the - /// appearance of the container at the cost of performance. Such effects include - /// uniform fading of children, blur, and other post-processing effects. - /// If all children are of a specific non- type, use the - /// generic version . - /// - public class BufferedContainer : BufferedContainer - { - }; - - /// - /// A container that renders its children to an internal framebuffer, and then - /// blits the framebuffer to the screen, instead of directly rendering the children - /// to the screen. This allows otherwise impossible effects to be applied to the - /// appearance of the container at the cost of performance. Such effects include - /// uniform fading of children, blur, and other post-processing effects. - /// - public class BufferedContainer : Container, IBufferedContainer - where T : Drawable - { - private bool drawOriginal; - - /// - /// If true the original buffered children will be drawn a second time on top of any effect (e.g. blur). - /// - public bool DrawOriginal - { - get { return drawOriginal; } - - set - { - if (drawOriginal == value) - return; - - drawOriginal = value; - ForceRedraw(); - } - } - - - private Vector2 blurSigma = Vector2.Zero; - - /// - /// Controls the amount of blurring in two orthogonal directions (X and Y if - /// is zero). - /// Blur is parametrized by a gaussian image filter. This property controls - /// the standard deviation (sigma) of the gaussian kernel. - /// - public Vector2 BlurSigma - { - get { return blurSigma; } - set - { - if (blurSigma == value) - return; - - blurSigma = value; - ForceRedraw(); - } - } - - private float blurRotation; - - /// - /// Rotates the blur kernel clockwise. In degrees. Has no effect if - /// has the same magnitude in both directions. - /// - public float BlurRotation - { - get { return blurRotation; } - set - { - if (blurRotation == value) - return; - - blurRotation = value; - ForceRedraw(); - } - } - - private bool pixelSnapping; - - /// - /// Whether the framebuffer's position is snapped to the nearest pixel when blitting. - /// Since the framebuffer's texels have the same size as pixels, this amounts to setting - /// the texture filtering mode to "nearest". - /// - public bool PixelSnapping - { - get { return pixelSnapping; } - set - { - if (frameBuffers[0].IsInitialized || frameBuffers[1].IsInitialized) - throw new InvalidOperationException("May only set PixelSnapping before FrameBuffers are initialized (i.e. before the first draw)."); - pixelSnapping = value; - } - } - - private ColourInfo effectColour = Color4.White; - - /// - /// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is . - /// Does not affect the original which is drawn when is true. - /// - public ColourInfo EffectColour - { - get { return effectColour; } - - set - { - if (effectColour.Equals(value)) - return; - - effectColour = value; - Invalidate(Invalidation.DrawNode); - } - } - - private BlendingParameters effectBlending; - - /// - /// The to use after applying all effects. Default is . - /// inherits the blending mode of the original, i.e. is used. - /// Does not affect the original which is drawn when is true. - /// - public BlendingParameters EffectBlending - { - get { return effectBlending; } - - set - { - if (effectBlending.Equals(value)) - return; - - effectBlending = value; - Invalidate(Invalidation.DrawNode); - } - } - - private EffectPlacement effectPlacement; - - /// - /// Whether the buffered effect should be drawn behind or in front of the original. - /// Behind by default. Does not have any effect if is false. - /// - public EffectPlacement EffectPlacement - { - get { return effectPlacement; } - - set - { - if (effectPlacement == value) - return; - - effectPlacement = value; - Invalidate(Invalidation.DrawNode); - } - } - - private Color4 backgroundColour = new Color4(0, 0, 0, 0); - - /// - /// The background colour of the framebuffer. Transparent black by default. - /// - public Color4 BackgroundColour - { - get { return backgroundColour; } - - set - { - if (backgroundColour == value) - return; - - backgroundColour = value; - ForceRedraw(); - } - } - - private Shader blurShader; - - /// - /// Whether the rendered framebuffer shall be cached until is called - /// or the size of the container (i.e. framebuffer) changes. - /// If false, then the framebuffer is re-rendered before it is blitted to the screen; equivalent - /// to calling every frame. - /// - public bool CacheDrawnFrameBuffer; - - /// - /// Forces a redraw of the framebuffer before it is blitted the next time. - /// Only relevant if is true. - /// - public void ForceRedraw() => Invalidate(Invalidation.DrawNode); - - /// - /// We need 3 frame buffers such that we can accumulate post-processing effects in a - /// ping-pong fashion going back and forth (reading from one buffer, writing into the other). - /// - private readonly FrameBuffer[] frameBuffers = new FrameBuffer[3]; - - /// - /// In order to signal the draw thread to re-draw the buffered container we version it. - /// Our own version (update) keeps track of which version we are on, whereas the - /// drawVersion keeps track of the version the draw thread is on. - /// When forcing a redraw we increment updateVersion, pass it into each new drawnode - /// and the draw thread will realize its drawVersion is lagging behind, thus redrawing. - /// - private long updateVersion; - - /// - /// We also want to keep track of updates to our children, as we can bypass these updates - /// when our output is in a cached state. - /// - private long childrenUpdateVersion; - - private readonly AtomicCounter drawVersion = new AtomicCounter(); - - private readonly QuadBatch quadBatch = new QuadBatch(1, 3); - - private readonly List attachedFormats = new List(); - - protected override bool CanBeFlattened => false; - - /// - /// Constructs an empty buffered container. - /// - public BufferedContainer() - { - for (int i = 0; i < frameBuffers.Length; ++i) - frameBuffers[i] = new FrameBuffer(); - - // The initial draw cannot be cached, and thus we need to initialize - // with a forced draw. - ForceRedraw(); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - if (blurShader == null) - blurShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); - } - - protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(); - - protected override void ApplyDrawNode(DrawNode node) - { - BufferedContainerDrawNode n = (BufferedContainerDrawNode)node; - - n.ScreenSpaceDrawRectangle = ScreenSpaceDrawQuad.AABBFloat; - n.Batch = quadBatch; - n.FrameBuffers = frameBuffers; - n.Formats = new List(attachedFormats); - n.FilteringMode = pixelSnapping ? All.Nearest : All.Linear; - - n.DrawVersion = drawVersion; - n.UpdateVersion = updateVersion; - n.BackgroundColour = backgroundColour; - - BlendingParameters localEffectBlending = EffectBlending; - if (localEffectBlending.Mode == BlendingMode.Inherit) - localEffectBlending.Mode = Blending.Mode; - - if (localEffectBlending.RGBEquation == BlendingEquation.Inherit) - localEffectBlending.RGBEquation = Blending.RGBEquation; - - if (localEffectBlending.AlphaEquation == BlendingEquation.Inherit) - localEffectBlending.AlphaEquation = Blending.AlphaEquation; - - n.EffectColour = effectColour; - n.EffectBlending = localEffectBlending; - n.EffectPlacement = effectPlacement; - - n.DrawOriginal = drawOriginal; - n.BlurSigma = blurSigma; - n.BlurRadius = new Vector2I(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y)); - n.BlurRotation = blurRotation; - n.BlurShader = blurShader; - - base.ApplyDrawNode(node); - - // Our own draw node should contain our correct color, hence we have - // to undo our overridden DrawInfo getter here. - n.DrawInfo.Colour = base.DrawInfo.Colour; - } - - /// - /// Attach an additional component to the framebuffer. Such a component can e.g. - /// be a depth component, such that the framebuffer can hold fragment depth information. - /// - public void Attach(RenderbufferInternalFormat format) - { - if (attachedFormats.Exists(f => f == format)) - return; - - attachedFormats.Add(format); - } - - protected override RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => ScreenSpaceDrawQuad.AABBFloat; // Make sure children never get masked away - - private Vector2 lastScreenSpacePos; - private bool checkScrenSpaceSize; - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawNode) > 0) - ++updateVersion; - - // We actually only care about Invalidation.MiscGeometry | Invalidation.DrawInfo, but must match the blanket invalidation logic in Drawable.Invalidate - if ((invalidation & (Invalidation.Colour | Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0) - checkScrenSpaceSize = true; - - return base.Invalidate(invalidation, source, shallPropagate); - } - - protected override void Update() - { - // Invalidate drawn frame buffer every frame. - if (!CacheDrawnFrameBuffer) - ForceRedraw(); - else if (checkScrenSpaceSize) - { - var quad = ScreenSpaceDrawQuad.AABBFloat; - - if (!Precision.AlmostEquals(quad.Width, lastScreenSpacePos.X) || !Precision.AlmostEquals(quad.Height, lastScreenSpacePos.Y)) - { - ++updateVersion; - lastScreenSpacePos = new Vector2(quad.Width, quad.Height); - } - - checkScrenSpaceSize = false; - } - - base.Update(); - } - - private readonly long[] drawNodeVersions = new long[3]; - private long currentDrawNode; - - internal override bool AddChildDrawNodes => drawNodeVersions[currentDrawNode] != updateVersion; - - internal override DrawNode GenerateDrawNodeSubtree(int treeIndex) - { - currentDrawNode = treeIndex; - var node = base.GenerateDrawNodeSubtree(treeIndex); - drawNodeVersions[currentDrawNode] = childrenUpdateVersion = updateVersion; - return node; - } - - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && childrenUpdateVersion != updateVersion; - - public override DrawInfo DrawInfo - { - get - { - DrawInfo result = base.DrawInfo; - - // When drawing our children to the frame buffer we do not - // want their colour to be polluted by their parent (us!) - // since our own color will be applied on top when we render - // from the frame buffer to the back buffer later on. - result.Colour = ColourInfo.SingleColour(Color4.White); - return result; - } - } - - //protected override void Dispose(bool isDisposing) - //{ - // right now we are relying on the finalizer for correct disposal. - // correct method would be to schedule these to update thread and - // then to the draw thread. - - // foreach (FrameBuffer frameBuffer in frameBuffers) - // frameBuffer.Dispose(); - - // base.Dispose(isDisposing); - //} - } - - public enum EffectPlacement - { - Behind, - InFront, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Buffers; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.MathUtils; +using osu.Framework.Threading; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container that renders its children to an internal framebuffer, and then + /// blits the framebuffer to the screen, instead of directly rendering the children + /// to the screen. This allows otherwise impossible effects to be applied to the + /// appearance of the container at the cost of performance. Such effects include + /// uniform fading of children, blur, and other post-processing effects. + /// If all children are of a specific non- type, use the + /// generic version . + /// + public class BufferedContainer : BufferedContainer + { + }; + + /// + /// A container that renders its children to an internal framebuffer, and then + /// blits the framebuffer to the screen, instead of directly rendering the children + /// to the screen. This allows otherwise impossible effects to be applied to the + /// appearance of the container at the cost of performance. Such effects include + /// uniform fading of children, blur, and other post-processing effects. + /// + public class BufferedContainer : Container, IBufferedContainer + where T : Drawable + { + private bool drawOriginal; + + /// + /// If true the original buffered children will be drawn a second time on top of any effect (e.g. blur). + /// + public bool DrawOriginal + { + get { return drawOriginal; } + + set + { + if (drawOriginal == value) + return; + + drawOriginal = value; + ForceRedraw(); + } + } + + + private Vector2 blurSigma = Vector2.Zero; + + /// + /// Controls the amount of blurring in two orthogonal directions (X and Y if + /// is zero). + /// Blur is parametrized by a gaussian image filter. This property controls + /// the standard deviation (sigma) of the gaussian kernel. + /// + public Vector2 BlurSigma + { + get { return blurSigma; } + set + { + if (blurSigma == value) + return; + + blurSigma = value; + ForceRedraw(); + } + } + + private float blurRotation; + + /// + /// Rotates the blur kernel clockwise. In degrees. Has no effect if + /// has the same magnitude in both directions. + /// + public float BlurRotation + { + get { return blurRotation; } + set + { + if (blurRotation == value) + return; + + blurRotation = value; + ForceRedraw(); + } + } + + private bool pixelSnapping; + + /// + /// Whether the framebuffer's position is snapped to the nearest pixel when blitting. + /// Since the framebuffer's texels have the same size as pixels, this amounts to setting + /// the texture filtering mode to "nearest". + /// + public bool PixelSnapping + { + get { return pixelSnapping; } + set + { + if (frameBuffers[0].IsInitialized || frameBuffers[1].IsInitialized) + throw new InvalidOperationException("May only set PixelSnapping before FrameBuffers are initialized (i.e. before the first draw)."); + pixelSnapping = value; + } + } + + private ColourInfo effectColour = Color4.White; + + /// + /// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is . + /// Does not affect the original which is drawn when is true. + /// + public ColourInfo EffectColour + { + get { return effectColour; } + + set + { + if (effectColour.Equals(value)) + return; + + effectColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private BlendingParameters effectBlending; + + /// + /// The to use after applying all effects. Default is . + /// inherits the blending mode of the original, i.e. is used. + /// Does not affect the original which is drawn when is true. + /// + public BlendingParameters EffectBlending + { + get { return effectBlending; } + + set + { + if (effectBlending.Equals(value)) + return; + + effectBlending = value; + Invalidate(Invalidation.DrawNode); + } + } + + private EffectPlacement effectPlacement; + + /// + /// Whether the buffered effect should be drawn behind or in front of the original. + /// Behind by default. Does not have any effect if is false. + /// + public EffectPlacement EffectPlacement + { + get { return effectPlacement; } + + set + { + if (effectPlacement == value) + return; + + effectPlacement = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Color4 backgroundColour = new Color4(0, 0, 0, 0); + + /// + /// The background colour of the framebuffer. Transparent black by default. + /// + public Color4 BackgroundColour + { + get { return backgroundColour; } + + set + { + if (backgroundColour == value) + return; + + backgroundColour = value; + ForceRedraw(); + } + } + + private Shader blurShader; + + /// + /// Whether the rendered framebuffer shall be cached until is called + /// or the size of the container (i.e. framebuffer) changes. + /// If false, then the framebuffer is re-rendered before it is blitted to the screen; equivalent + /// to calling every frame. + /// + public bool CacheDrawnFrameBuffer; + + /// + /// Forces a redraw of the framebuffer before it is blitted the next time. + /// Only relevant if is true. + /// + public void ForceRedraw() => Invalidate(Invalidation.DrawNode); + + /// + /// We need 3 frame buffers such that we can accumulate post-processing effects in a + /// ping-pong fashion going back and forth (reading from one buffer, writing into the other). + /// + private readonly FrameBuffer[] frameBuffers = new FrameBuffer[3]; + + /// + /// In order to signal the draw thread to re-draw the buffered container we version it. + /// Our own version (update) keeps track of which version we are on, whereas the + /// drawVersion keeps track of the version the draw thread is on. + /// When forcing a redraw we increment updateVersion, pass it into each new drawnode + /// and the draw thread will realize its drawVersion is lagging behind, thus redrawing. + /// + private long updateVersion; + + /// + /// We also want to keep track of updates to our children, as we can bypass these updates + /// when our output is in a cached state. + /// + private long childrenUpdateVersion; + + private readonly AtomicCounter drawVersion = new AtomicCounter(); + + private readonly QuadBatch quadBatch = new QuadBatch(1, 3); + + private readonly List attachedFormats = new List(); + + protected override bool CanBeFlattened => false; + + /// + /// Constructs an empty buffered container. + /// + public BufferedContainer() + { + for (int i = 0; i < frameBuffers.Length; ++i) + frameBuffers[i] = new FrameBuffer(); + + // The initial draw cannot be cached, and thus we need to initialize + // with a forced draw. + ForceRedraw(); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + if (blurShader == null) + blurShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); + } + + protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(); + + protected override void ApplyDrawNode(DrawNode node) + { + BufferedContainerDrawNode n = (BufferedContainerDrawNode)node; + + n.ScreenSpaceDrawRectangle = ScreenSpaceDrawQuad.AABBFloat; + n.Batch = quadBatch; + n.FrameBuffers = frameBuffers; + n.Formats = new List(attachedFormats); + n.FilteringMode = pixelSnapping ? All.Nearest : All.Linear; + + n.DrawVersion = drawVersion; + n.UpdateVersion = updateVersion; + n.BackgroundColour = backgroundColour; + + BlendingParameters localEffectBlending = EffectBlending; + if (localEffectBlending.Mode == BlendingMode.Inherit) + localEffectBlending.Mode = Blending.Mode; + + if (localEffectBlending.RGBEquation == BlendingEquation.Inherit) + localEffectBlending.RGBEquation = Blending.RGBEquation; + + if (localEffectBlending.AlphaEquation == BlendingEquation.Inherit) + localEffectBlending.AlphaEquation = Blending.AlphaEquation; + + n.EffectColour = effectColour; + n.EffectBlending = localEffectBlending; + n.EffectPlacement = effectPlacement; + + n.DrawOriginal = drawOriginal; + n.BlurSigma = blurSigma; + n.BlurRadius = new Vector2I(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y)); + n.BlurRotation = blurRotation; + n.BlurShader = blurShader; + + base.ApplyDrawNode(node); + + // Our own draw node should contain our correct color, hence we have + // to undo our overridden DrawInfo getter here. + n.DrawInfo.Colour = base.DrawInfo.Colour; + } + + /// + /// Attach an additional component to the framebuffer. Such a component can e.g. + /// be a depth component, such that the framebuffer can hold fragment depth information. + /// + public void Attach(RenderbufferInternalFormat format) + { + if (attachedFormats.Exists(f => f == format)) + return; + + attachedFormats.Add(format); + } + + protected override RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => ScreenSpaceDrawQuad.AABBFloat; // Make sure children never get masked away + + private Vector2 lastScreenSpacePos; + private bool checkScrenSpaceSize; + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawNode) > 0) + ++updateVersion; + + // We actually only care about Invalidation.MiscGeometry | Invalidation.DrawInfo, but must match the blanket invalidation logic in Drawable.Invalidate + if ((invalidation & (Invalidation.Colour | Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0) + checkScrenSpaceSize = true; + + return base.Invalidate(invalidation, source, shallPropagate); + } + + protected override void Update() + { + // Invalidate drawn frame buffer every frame. + if (!CacheDrawnFrameBuffer) + ForceRedraw(); + else if (checkScrenSpaceSize) + { + var quad = ScreenSpaceDrawQuad.AABBFloat; + + if (!Precision.AlmostEquals(quad.Width, lastScreenSpacePos.X) || !Precision.AlmostEquals(quad.Height, lastScreenSpacePos.Y)) + { + ++updateVersion; + lastScreenSpacePos = new Vector2(quad.Width, quad.Height); + } + + checkScrenSpaceSize = false; + } + + base.Update(); + } + + private readonly long[] drawNodeVersions = new long[3]; + private long currentDrawNode; + + internal override bool AddChildDrawNodes => drawNodeVersions[currentDrawNode] != updateVersion; + + internal override DrawNode GenerateDrawNodeSubtree(int treeIndex) + { + currentDrawNode = treeIndex; + var node = base.GenerateDrawNodeSubtree(treeIndex); + drawNodeVersions[currentDrawNode] = childrenUpdateVersion = updateVersion; + return node; + } + + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && childrenUpdateVersion != updateVersion; + + public override DrawInfo DrawInfo + { + get + { + DrawInfo result = base.DrawInfo; + + // When drawing our children to the frame buffer we do not + // want their colour to be polluted by their parent (us!) + // since our own color will be applied on top when we render + // from the frame buffer to the back buffer later on. + result.Colour = ColourInfo.SingleColour(Color4.White); + return result; + } + } + + //protected override void Dispose(bool isDisposing) + //{ + // right now we are relying on the finalizer for correct disposal. + // correct method would be to schedule these to update thread and + // then to the draw thread. + + // foreach (FrameBuffer frameBuffer in frameBuffers) + // frameBuffer.Dispose(); + + // base.Dispose(isDisposing); + //} + } + + public enum EffectPlacement + { + Behind, + InFront, + } +} diff --git a/osu.Framework/Graphics/Containers/BufferedContainerDrawNode.cs b/osu.Framework/Graphics/Containers/BufferedContainerDrawNode.cs index 2e023f435..d9e0f097b 100644 --- a/osu.Framework/Graphics/Containers/BufferedContainerDrawNode.cs +++ b/osu.Framework/Graphics/Containers/BufferedContainerDrawNode.cs @@ -1,228 +1,228 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.OpenGL.Buffers; -using OpenTK; -using OpenTK.Graphics.ES30; -using OpenTK.Graphics; -using osu.Framework.Threading; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Shaders; -using System; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using System.Diagnostics; - -namespace osu.Framework.Graphics.Containers -{ - public class BufferedContainerDrawNode : CompositeDrawNode - { - public FrameBuffer[] FrameBuffers; - - public bool DrawOriginal; - public Color4 BackgroundColour; - public ColourInfo EffectColour; - public BlendingParameters EffectBlending; - public EffectPlacement EffectPlacement; - - public Vector2 BlurSigma; - public Vector2I BlurRadius; - public float BlurRotation; - - public Shader BlurShader; - - public AtomicCounter DrawVersion; - public long UpdateVersion = -1; - - public RectangleF ScreenSpaceDrawRectangle; - public QuadBatch Batch; - public List Formats; - public All FilteringMode; - - private InvokeOnDisposal establishFrameBufferViewport(Vector2 roundedSize) - { - // Disable masking for generating the frame buffer since masking will be re-applied - // when actually drawing later on anyways. This allows more information to be captured - // in the frame buffer and helps with cached buffers being re-used. - RectangleI screenSpaceMaskingRect = new RectangleI((int)Math.Floor(ScreenSpaceDrawRectangle.X), (int)Math.Floor(ScreenSpaceDrawRectangle.Y), (int)roundedSize.X + 1, (int)roundedSize.Y + 1); - - GLWrapper.PushMaskingInfo(new MaskingInfo - { - ScreenSpaceAABB = screenSpaceMaskingRect, - MaskingRect = ScreenSpaceDrawRectangle, - ToMaskingSpace = Matrix3.Identity, - BlendRange = 1, - AlphaExponent = 1, - }, true); - - // Match viewport to FrameBuffer such that we don't draw unnecessary pixels. - GLWrapper.PushViewport(new RectangleI(0, 0, (int)roundedSize.X, (int)roundedSize.Y)); - - return new InvokeOnDisposal(delegate - { - GLWrapper.PopViewport(); - GLWrapper.PopMaskingInfo(); - }); - } - - private InvokeOnDisposal bindFrameBuffer(FrameBuffer frameBuffer, Vector2 requestedSize) - { - if (!frameBuffer.IsInitialized) - frameBuffer.Initialize(true, FilteringMode); - - // These additional render buffers are only required if e.g. depth - // or stencil information needs to also be stored somewhere. - foreach (var f in Formats) - frameBuffer.Attach(f); - - // This setter will also take care of allocating a texture of appropriate size within the framebuffer. - frameBuffer.Size = requestedSize; - - frameBuffer.Bind(); - - return new InvokeOnDisposal(frameBuffer.Unbind); - } - - private void drawFrameBufferToBackBuffer(FrameBuffer frameBuffer, RectangleF drawRectangle, ColourInfo colourInfo) - { - // The strange Y coordinate and Height are a result of OpenGL coordinate systems having Y grow upwards and not downwards. - RectangleF textureRect = new RectangleF(0, frameBuffer.Texture.Height, frameBuffer.Texture.Width, -frameBuffer.Texture.Height); - if (frameBuffer.Texture.Bind()) - // Color was already applied by base.Draw(); no need to re-apply. Thus we use White here. - frameBuffer.Texture.DrawQuad(drawRectangle, textureRect, colourInfo); - } - - private void drawChildren(Action vertexAction, Vector2 frameBufferSize) - { - // Fill the frame buffer with drawn children - using (bindFrameBuffer(currentFrameBuffer, frameBufferSize)) - { - // We need to draw children as if they were zero-based to the top-left of the texture. - // We can do this by adding a translation component to our (orthogonal) projection matrix. - GLWrapper.PushOrtho(ScreenSpaceDrawRectangle); - - GLWrapper.ClearColour(BackgroundColour); - base.Draw(vertexAction); - - GLWrapper.PopOrtho(); - } - } - - private void drawBlurredFrameBuffer(int kernelRadius, float sigma, float blurRotation) - { - FrameBuffer source = currentFrameBuffer; - FrameBuffer target = advanceFrameBuffer(); - - GLWrapper.SetBlend(new BlendingInfo - { - Source = BlendingFactorSrc.One, - Destination = BlendingFactorDest.Zero, - SourceAlpha = BlendingFactorSrc.One, - DestinationAlpha = BlendingFactorDest.Zero, - }); - - using (bindFrameBuffer(target, source.Size)) - { - BlurShader.GetUniform(@"g_Radius").Value = kernelRadius; - BlurShader.GetUniform(@"g_Sigma").Value = sigma; - BlurShader.GetUniform(@"g_TexSize").Value = source.Size; - - float radians = -MathHelper.DegreesToRadians(blurRotation); - BlurShader.GetUniform(@"g_BlurDirection").Value = new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)); - - BlurShader.Bind(); - drawFrameBufferToBackBuffer(source, new RectangleF(0, 0, source.Texture.Width, source.Texture.Height), ColourInfo.SingleColour(Color4.White)); - BlurShader.Unbind(); - } - } - - private int currentFrameBufferIndex; - private FrameBuffer currentFrameBuffer => FrameBuffers[currentFrameBufferIndex]; - private FrameBuffer advanceFrameBuffer() => FrameBuffers[currentFrameBufferIndex = (currentFrameBufferIndex + 1) % 2]; - - /// - /// Makes sure the first frame buffer is always the one we want to draw from. - /// This saves us the need to sync the draw indices across draw node trees - /// since the FrameBuffers array is already shared. - /// - private void finalizeFrameBuffer() - { - if (currentFrameBufferIndex != 0) - { - Trace.Assert(currentFrameBufferIndex == 1, - $"Only the first two framebuffers should be the last to be written to at the end of {nameof(Draw)}."); - - FrameBuffer temp = FrameBuffers[0]; - FrameBuffers[0] = FrameBuffers[1]; - FrameBuffers[1] = temp; - - currentFrameBufferIndex = 0; - } - } - - // Our effects will be drawn into framebuffers 0 and 1. If we want to preserve the originally - // drawn children we need to put them in a separate buffer; in this case buffer 2. Otherwise, - // we do not want to allocate a third buffer for nothing and hence we start with 0. - private int originalIndex => DrawOriginal && (BlurRadius.X > 0 || BlurRadius.Y > 0) ? 2 : 0; - - public override void Draw(Action vertexAction) - { - currentFrameBufferIndex = originalIndex; - - Vector2 frameBufferSize = new Vector2((float)Math.Ceiling(ScreenSpaceDrawRectangle.Width), (float)Math.Ceiling(ScreenSpaceDrawRectangle.Height)); - if (UpdateVersion > DrawVersion.Value) - { - DrawVersion.Value = UpdateVersion; - - using (establishFrameBufferViewport(frameBufferSize)) - { - drawChildren(vertexAction, frameBufferSize); - - // Blur post-processing in case a blur radius is defined. - if (BlurRadius.X > 0 || BlurRadius.Y > 0) - { - GL.Disable(EnableCap.ScissorTest); - - if (BlurRadius.X > 0) drawBlurredFrameBuffer(BlurRadius.X, BlurSigma.X, BlurRotation); - if (BlurRadius.Y > 0) drawBlurredFrameBuffer(BlurRadius.Y, BlurSigma.Y, BlurRotation + 90); - - GL.Enable(EnableCap.ScissorTest); - } - } - - finalizeFrameBuffer(); - } - - RectangleF drawRectangle = FilteringMode == All.Nearest - ? new RectangleF(ScreenSpaceDrawRectangle.X, ScreenSpaceDrawRectangle.Y, frameBufferSize.X, frameBufferSize.Y) - : ScreenSpaceDrawRectangle; - - Shader.Bind(); - - if (DrawOriginal && EffectPlacement == EffectPlacement.InFront) - { - GLWrapper.SetBlend(DrawInfo.Blending); - drawFrameBufferToBackBuffer(FrameBuffers[originalIndex], drawRectangle, DrawInfo.Colour); - } - - // Blit the final framebuffer to screen. - GLWrapper.SetBlend(new BlendingInfo(EffectBlending)); - - ColourInfo effectColour = DrawInfo.Colour; - effectColour.ApplyChild(EffectColour); - drawFrameBufferToBackBuffer(FrameBuffers[0], drawRectangle, effectColour); - - if (DrawOriginal && EffectPlacement == EffectPlacement.Behind) - { - GLWrapper.SetBlend(DrawInfo.Blending); - drawFrameBufferToBackBuffer(FrameBuffers[originalIndex], drawRectangle, DrawInfo.Colour); - } - - Shader.Unbind(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.OpenGL.Buffers; +using OpenTK; +using OpenTK.Graphics.ES30; +using OpenTK.Graphics; +using osu.Framework.Threading; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Shaders; +using System; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; +using System.Diagnostics; + +namespace osu.Framework.Graphics.Containers +{ + public class BufferedContainerDrawNode : CompositeDrawNode + { + public FrameBuffer[] FrameBuffers; + + public bool DrawOriginal; + public Color4 BackgroundColour; + public ColourInfo EffectColour; + public BlendingParameters EffectBlending; + public EffectPlacement EffectPlacement; + + public Vector2 BlurSigma; + public Vector2I BlurRadius; + public float BlurRotation; + + public Shader BlurShader; + + public AtomicCounter DrawVersion; + public long UpdateVersion = -1; + + public RectangleF ScreenSpaceDrawRectangle; + public QuadBatch Batch; + public List Formats; + public All FilteringMode; + + private InvokeOnDisposal establishFrameBufferViewport(Vector2 roundedSize) + { + // Disable masking for generating the frame buffer since masking will be re-applied + // when actually drawing later on anyways. This allows more information to be captured + // in the frame buffer and helps with cached buffers being re-used. + RectangleI screenSpaceMaskingRect = new RectangleI((int)Math.Floor(ScreenSpaceDrawRectangle.X), (int)Math.Floor(ScreenSpaceDrawRectangle.Y), (int)roundedSize.X + 1, (int)roundedSize.Y + 1); + + GLWrapper.PushMaskingInfo(new MaskingInfo + { + ScreenSpaceAABB = screenSpaceMaskingRect, + MaskingRect = ScreenSpaceDrawRectangle, + ToMaskingSpace = Matrix3.Identity, + BlendRange = 1, + AlphaExponent = 1, + }, true); + + // Match viewport to FrameBuffer such that we don't draw unnecessary pixels. + GLWrapper.PushViewport(new RectangleI(0, 0, (int)roundedSize.X, (int)roundedSize.Y)); + + return new InvokeOnDisposal(delegate + { + GLWrapper.PopViewport(); + GLWrapper.PopMaskingInfo(); + }); + } + + private InvokeOnDisposal bindFrameBuffer(FrameBuffer frameBuffer, Vector2 requestedSize) + { + if (!frameBuffer.IsInitialized) + frameBuffer.Initialize(true, FilteringMode); + + // These additional render buffers are only required if e.g. depth + // or stencil information needs to also be stored somewhere. + foreach (var f in Formats) + frameBuffer.Attach(f); + + // This setter will also take care of allocating a texture of appropriate size within the framebuffer. + frameBuffer.Size = requestedSize; + + frameBuffer.Bind(); + + return new InvokeOnDisposal(frameBuffer.Unbind); + } + + private void drawFrameBufferToBackBuffer(FrameBuffer frameBuffer, RectangleF drawRectangle, ColourInfo colourInfo) + { + // The strange Y coordinate and Height are a result of OpenGL coordinate systems having Y grow upwards and not downwards. + RectangleF textureRect = new RectangleF(0, frameBuffer.Texture.Height, frameBuffer.Texture.Width, -frameBuffer.Texture.Height); + if (frameBuffer.Texture.Bind()) + // Color was already applied by base.Draw(); no need to re-apply. Thus we use White here. + frameBuffer.Texture.DrawQuad(drawRectangle, textureRect, colourInfo); + } + + private void drawChildren(Action vertexAction, Vector2 frameBufferSize) + { + // Fill the frame buffer with drawn children + using (bindFrameBuffer(currentFrameBuffer, frameBufferSize)) + { + // We need to draw children as if they were zero-based to the top-left of the texture. + // We can do this by adding a translation component to our (orthogonal) projection matrix. + GLWrapper.PushOrtho(ScreenSpaceDrawRectangle); + + GLWrapper.ClearColour(BackgroundColour); + base.Draw(vertexAction); + + GLWrapper.PopOrtho(); + } + } + + private void drawBlurredFrameBuffer(int kernelRadius, float sigma, float blurRotation) + { + FrameBuffer source = currentFrameBuffer; + FrameBuffer target = advanceFrameBuffer(); + + GLWrapper.SetBlend(new BlendingInfo + { + Source = BlendingFactorSrc.One, + Destination = BlendingFactorDest.Zero, + SourceAlpha = BlendingFactorSrc.One, + DestinationAlpha = BlendingFactorDest.Zero, + }); + + using (bindFrameBuffer(target, source.Size)) + { + BlurShader.GetUniform(@"g_Radius").Value = kernelRadius; + BlurShader.GetUniform(@"g_Sigma").Value = sigma; + BlurShader.GetUniform(@"g_TexSize").Value = source.Size; + + float radians = -MathHelper.DegreesToRadians(blurRotation); + BlurShader.GetUniform(@"g_BlurDirection").Value = new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)); + + BlurShader.Bind(); + drawFrameBufferToBackBuffer(source, new RectangleF(0, 0, source.Texture.Width, source.Texture.Height), ColourInfo.SingleColour(Color4.White)); + BlurShader.Unbind(); + } + } + + private int currentFrameBufferIndex; + private FrameBuffer currentFrameBuffer => FrameBuffers[currentFrameBufferIndex]; + private FrameBuffer advanceFrameBuffer() => FrameBuffers[currentFrameBufferIndex = (currentFrameBufferIndex + 1) % 2]; + + /// + /// Makes sure the first frame buffer is always the one we want to draw from. + /// This saves us the need to sync the draw indices across draw node trees + /// since the FrameBuffers array is already shared. + /// + private void finalizeFrameBuffer() + { + if (currentFrameBufferIndex != 0) + { + Trace.Assert(currentFrameBufferIndex == 1, + $"Only the first two framebuffers should be the last to be written to at the end of {nameof(Draw)}."); + + FrameBuffer temp = FrameBuffers[0]; + FrameBuffers[0] = FrameBuffers[1]; + FrameBuffers[1] = temp; + + currentFrameBufferIndex = 0; + } + } + + // Our effects will be drawn into framebuffers 0 and 1. If we want to preserve the originally + // drawn children we need to put them in a separate buffer; in this case buffer 2. Otherwise, + // we do not want to allocate a third buffer for nothing and hence we start with 0. + private int originalIndex => DrawOriginal && (BlurRadius.X > 0 || BlurRadius.Y > 0) ? 2 : 0; + + public override void Draw(Action vertexAction) + { + currentFrameBufferIndex = originalIndex; + + Vector2 frameBufferSize = new Vector2((float)Math.Ceiling(ScreenSpaceDrawRectangle.Width), (float)Math.Ceiling(ScreenSpaceDrawRectangle.Height)); + if (UpdateVersion > DrawVersion.Value) + { + DrawVersion.Value = UpdateVersion; + + using (establishFrameBufferViewport(frameBufferSize)) + { + drawChildren(vertexAction, frameBufferSize); + + // Blur post-processing in case a blur radius is defined. + if (BlurRadius.X > 0 || BlurRadius.Y > 0) + { + GL.Disable(EnableCap.ScissorTest); + + if (BlurRadius.X > 0) drawBlurredFrameBuffer(BlurRadius.X, BlurSigma.X, BlurRotation); + if (BlurRadius.Y > 0) drawBlurredFrameBuffer(BlurRadius.Y, BlurSigma.Y, BlurRotation + 90); + + GL.Enable(EnableCap.ScissorTest); + } + } + + finalizeFrameBuffer(); + } + + RectangleF drawRectangle = FilteringMode == All.Nearest + ? new RectangleF(ScreenSpaceDrawRectangle.X, ScreenSpaceDrawRectangle.Y, frameBufferSize.X, frameBufferSize.Y) + : ScreenSpaceDrawRectangle; + + Shader.Bind(); + + if (DrawOriginal && EffectPlacement == EffectPlacement.InFront) + { + GLWrapper.SetBlend(DrawInfo.Blending); + drawFrameBufferToBackBuffer(FrameBuffers[originalIndex], drawRectangle, DrawInfo.Colour); + } + + // Blit the final framebuffer to screen. + GLWrapper.SetBlend(new BlendingInfo(EffectBlending)); + + ColourInfo effectColour = DrawInfo.Colour; + effectColour.ApplyChild(EffectColour); + drawFrameBufferToBackBuffer(FrameBuffers[0], drawRectangle, effectColour); + + if (DrawOriginal && EffectPlacement == EffectPlacement.Behind) + { + GLWrapper.SetBlend(DrawInfo.Blending); + drawFrameBufferToBackBuffer(FrameBuffers[originalIndex], drawRectangle, DrawInfo.Colour); + } + + Shader.Unbind(); + } + } +} diff --git a/osu.Framework/Graphics/Containers/CircularContainer.cs b/osu.Framework/Graphics/Containers/CircularContainer.cs index 4a0b3f6ec..ae7f00918 100644 --- a/osu.Framework/Graphics/Containers/CircularContainer.cs +++ b/osu.Framework/Graphics/Containers/CircularContainer.cs @@ -1,37 +1,37 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Caching; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container which is rounded (via automatic corner-radius) on the shortest edge. - /// - public class CircularContainer : Container - { - private Cached cornerRadius = new Cached(); - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - bool result = base.Invalidate(invalidation, source, shallPropagate); - - if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit)) > 0) - cornerRadius.Invalidate(); - - return result; - } - - protected override void UpdateAfterAutoSize() - { - base.UpdateAfterAutoSize(); - - if (!cornerRadius.IsValid) - { - CornerRadius = Math.Min(DrawSize.X, DrawSize.Y) / 2f; - cornerRadius.Validate(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Caching; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which is rounded (via automatic corner-radius) on the shortest edge. + /// + public class CircularContainer : Container + { + private Cached cornerRadius = new Cached(); + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + bool result = base.Invalidate(invalidation, source, shallPropagate); + + if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit)) > 0) + cornerRadius.Invalidate(); + + return result; + } + + protected override void UpdateAfterAutoSize() + { + base.UpdateAfterAutoSize(); + + if (!cornerRadius.IsValid) + { + CornerRadius = Math.Min(DrawSize.X, DrawSize.Y) / 2f; + cornerRadius.Validate(); + } + } + } +} diff --git a/osu.Framework/Graphics/Containers/ClickableContainer.cs b/osu.Framework/Graphics/Containers/ClickableContainer.cs index c7865473e..14ccdeee1 100644 --- a/osu.Framework/Graphics/Containers/ClickableContainer.cs +++ b/osu.Framework/Graphics/Containers/ClickableContainer.cs @@ -1,37 +1,37 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Configuration; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.Containers -{ - public class ClickableContainer : Container - { - private Action action; - - public Action Action - { - get - { - return action; - } - - set - { - action = value; - Enabled.Value = action != null; - } - } - - public readonly BindableBool Enabled = new BindableBool(); - - protected override bool OnClick(InputState state) - { - if (Enabled.Value) - Action?.Invoke(); - return true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.Containers +{ + public class ClickableContainer : Container + { + private Action action; + + public Action Action + { + get + { + return action; + } + + set + { + action = value; + Enabled.Value = action != null; + } + } + + public readonly BindableBool Enabled = new BindableBool(); + + protected override bool OnClick(InputState state) + { + if (Enabled.Value) + Action?.Invoke(); + return true; + } + } +} diff --git a/osu.Framework/Graphics/Containers/CompositeDrawNode.cs b/osu.Framework/Graphics/Containers/CompositeDrawNode.cs index b5e99dff8..2aa869f32 100644 --- a/osu.Framework/Graphics/Containers/CompositeDrawNode.cs +++ b/osu.Framework/Graphics/Containers/CompositeDrawNode.cs @@ -1,221 +1,221 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Batches; -using OpenTK; -using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Colour; -using System; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// Types of edge effects that can be applied to s. - /// - public enum EdgeEffectType - { - None, - Glow, - Shadow, - } - - /// - /// Parametrizes the appearance of an edge effect. - /// - public struct EdgeEffectParameters : IEquatable - { - /// - /// Colour of the edge effect. - /// - public SRGBColour Colour; - - /// - /// Positional offset applied to the edge effect. - /// Useful for off-center shadows. - /// - public Vector2 Offset; - - /// - /// The type of the edge effect. - /// - public EdgeEffectType Type; - - /// - /// How round the edge effect should appear. Adds to the - /// of the corresponding . Not to confuse with the . - /// - public float Roundness; - - /// - /// How "thick" the edge effect is around the . In other words: At what distance - /// from the 's border the edge effect becomes fully invisible. - /// - public float Radius; - - /// - /// Whether the inside of the EdgeEffect rectangle should be empty. - /// - public bool Hollow; - - public bool Equals(EdgeEffectParameters other) => - Colour.Equals(other.Colour) && - Offset == other.Offset && - Type == other.Type && - Roundness == other.Roundness && - Radius == other.Radius; - - public override string ToString() => Type != EdgeEffectType.None ? $@"{Radius} {Type}EdgeEffect" : @"EdgeEffect (Disabled)"; - } - - /// - /// Shared data between all s corresponding to the same - /// . - /// - public class CompositeDrawNodeSharedData - { - /// - /// The vertex batch used for rendering. - /// - public QuadBatch VertexBatch; - - /// - /// Whether we always want to use our own vertex batch for our corresponding - /// . If false, then we may get rendered with some other - /// shared vertex batch. - /// - public bool ForceOwnVertexBatch; - } - - /// - /// A draw node responsible for rendering a and the - /// s of its children. - /// - public class CompositeDrawNode : DrawNode - { - /// - /// The s of the children of our . - /// - public List Children; - - /// - /// Information about how masking of children should be carried out. - /// - public MaskingInfo? MaskingInfo; - - /// - /// The screen-space version of . - /// Used as cache of screen-space masking quads computed in previous frames. - /// Assign null to reset. - /// - public Quad? ScreenSpaceMaskingQuad; - - /// - /// Information about how the edge effect should be rendered. - /// - public EdgeEffectParameters EdgeEffect; - - /// - /// Shared data between all s corresponding to the same - /// . - /// - public CompositeDrawNodeSharedData Shared; - - /// - /// The shader to be used for rendering the edge effect. - /// - public Shader Shader; - - private void drawEdgeEffect() - { - if (MaskingInfo == null || EdgeEffect.Type == EdgeEffectType.None || EdgeEffect.Radius <= 0.0f || EdgeEffect.Colour.Linear.A <= 0.0f) - return; - - RectangleF effectRect = MaskingInfo.Value.MaskingRect.Inflate(EdgeEffect.Radius).Offset(EdgeEffect.Offset); - if (!ScreenSpaceMaskingQuad.HasValue) - ScreenSpaceMaskingQuad = Quad.FromRectangle(effectRect) * DrawInfo.Matrix; - - MaskingInfo edgeEffectMaskingInfo = MaskingInfo.Value; - edgeEffectMaskingInfo.MaskingRect = effectRect; - edgeEffectMaskingInfo.ScreenSpaceAABB = ScreenSpaceMaskingQuad.Value.AABB; - edgeEffectMaskingInfo.CornerRadius += EdgeEffect.Radius + EdgeEffect.Roundness; - edgeEffectMaskingInfo.BorderThickness = 0; - // HACK HACK HACK. We abuse blend range to give us the linear alpha gradient of - // the edge effect along its radius using the same rounded-corners shader. - edgeEffectMaskingInfo.BlendRange = EdgeEffect.Radius; - edgeEffectMaskingInfo.AlphaExponent = 2; - edgeEffectMaskingInfo.Hollow = EdgeEffect.Hollow; - - GLWrapper.PushMaskingInfo(edgeEffectMaskingInfo); - - GLWrapper.SetBlend(new BlendingInfo(EdgeEffect.Type == EdgeEffectType.Glow ? BlendingMode.Additive : BlendingMode.Mixture)); - - Shader.Bind(); - - ColourInfo colour = ColourInfo.SingleColour(EdgeEffect.Colour); - colour.TopLeft.MultiplyAlpha(DrawInfo.Colour.TopLeft.Linear.A); - colour.BottomLeft.MultiplyAlpha(DrawInfo.Colour.BottomLeft.Linear.A); - colour.TopRight.MultiplyAlpha(DrawInfo.Colour.TopRight.Linear.A); - colour.BottomRight.MultiplyAlpha(DrawInfo.Colour.BottomRight.Linear.A); - - Texture.WhitePixel.DrawQuad( - ScreenSpaceMaskingQuad.Value, - colour, null, null, null, - // HACK HACK HACK. We re-use the unused vertex blend range to store the original - // masking blend range when rendering edge effects. This is needed for smooth inner edges - // with a hollow edge effect. - new Vector2(MaskingInfo.Value.BlendRange)); - - Shader.Unbind(); - - GLWrapper.PopMaskingInfo(); - } - - private const int min_amount_children_to_warrant_batch = 5; - - private bool mayHaveOwnVertexBatch(int amountChildren) => Shared.ForceOwnVertexBatch || amountChildren >= min_amount_children_to_warrant_batch; - - private void updateVertexBatch() - { - if (Children == null) - return; - - // This logic got roughly copied from the old osu! code base. These constants seem to have worked well so far. - int clampedAmountChildren = MathHelper.Clamp(Children.Count, 1, 1000); - if (mayHaveOwnVertexBatch(clampedAmountChildren) && (Shared.VertexBatch == null || Shared.VertexBatch.Size < clampedAmountChildren)) - Shared.VertexBatch = new QuadBatch(clampedAmountChildren * 2, 500); - } - - public override void Draw(Action vertexAction) - { - updateVertexBatch(); - - // Prefer to use own vertex batch instead of the parent-owned one. - if (Shared.VertexBatch != null) - vertexAction = Shared.VertexBatch.AddAction; - - base.Draw(vertexAction); - - drawEdgeEffect(); - if (MaskingInfo != null) - { - MaskingInfo info = MaskingInfo.Value; - if (info.BorderThickness > 0) - info.BorderColour *= DrawInfo.Colour.AverageColour; - - GLWrapper.PushMaskingInfo(info); - } - - if (Children != null) - foreach (DrawNode child in Children) - child.Draw(vertexAction); - - if (MaskingInfo != null) - GLWrapper.PopMaskingInfo(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Batches; +using OpenTK; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Colour; +using System; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// Types of edge effects that can be applied to s. + /// + public enum EdgeEffectType + { + None, + Glow, + Shadow, + } + + /// + /// Parametrizes the appearance of an edge effect. + /// + public struct EdgeEffectParameters : IEquatable + { + /// + /// Colour of the edge effect. + /// + public SRGBColour Colour; + + /// + /// Positional offset applied to the edge effect. + /// Useful for off-center shadows. + /// + public Vector2 Offset; + + /// + /// The type of the edge effect. + /// + public EdgeEffectType Type; + + /// + /// How round the edge effect should appear. Adds to the + /// of the corresponding . Not to confuse with the . + /// + public float Roundness; + + /// + /// How "thick" the edge effect is around the . In other words: At what distance + /// from the 's border the edge effect becomes fully invisible. + /// + public float Radius; + + /// + /// Whether the inside of the EdgeEffect rectangle should be empty. + /// + public bool Hollow; + + public bool Equals(EdgeEffectParameters other) => + Colour.Equals(other.Colour) && + Offset == other.Offset && + Type == other.Type && + Roundness == other.Roundness && + Radius == other.Radius; + + public override string ToString() => Type != EdgeEffectType.None ? $@"{Radius} {Type}EdgeEffect" : @"EdgeEffect (Disabled)"; + } + + /// + /// Shared data between all s corresponding to the same + /// . + /// + public class CompositeDrawNodeSharedData + { + /// + /// The vertex batch used for rendering. + /// + public QuadBatch VertexBatch; + + /// + /// Whether we always want to use our own vertex batch for our corresponding + /// . If false, then we may get rendered with some other + /// shared vertex batch. + /// + public bool ForceOwnVertexBatch; + } + + /// + /// A draw node responsible for rendering a and the + /// s of its children. + /// + public class CompositeDrawNode : DrawNode + { + /// + /// The s of the children of our . + /// + public List Children; + + /// + /// Information about how masking of children should be carried out. + /// + public MaskingInfo? MaskingInfo; + + /// + /// The screen-space version of . + /// Used as cache of screen-space masking quads computed in previous frames. + /// Assign null to reset. + /// + public Quad? ScreenSpaceMaskingQuad; + + /// + /// Information about how the edge effect should be rendered. + /// + public EdgeEffectParameters EdgeEffect; + + /// + /// Shared data between all s corresponding to the same + /// . + /// + public CompositeDrawNodeSharedData Shared; + + /// + /// The shader to be used for rendering the edge effect. + /// + public Shader Shader; + + private void drawEdgeEffect() + { + if (MaskingInfo == null || EdgeEffect.Type == EdgeEffectType.None || EdgeEffect.Radius <= 0.0f || EdgeEffect.Colour.Linear.A <= 0.0f) + return; + + RectangleF effectRect = MaskingInfo.Value.MaskingRect.Inflate(EdgeEffect.Radius).Offset(EdgeEffect.Offset); + if (!ScreenSpaceMaskingQuad.HasValue) + ScreenSpaceMaskingQuad = Quad.FromRectangle(effectRect) * DrawInfo.Matrix; + + MaskingInfo edgeEffectMaskingInfo = MaskingInfo.Value; + edgeEffectMaskingInfo.MaskingRect = effectRect; + edgeEffectMaskingInfo.ScreenSpaceAABB = ScreenSpaceMaskingQuad.Value.AABB; + edgeEffectMaskingInfo.CornerRadius += EdgeEffect.Radius + EdgeEffect.Roundness; + edgeEffectMaskingInfo.BorderThickness = 0; + // HACK HACK HACK. We abuse blend range to give us the linear alpha gradient of + // the edge effect along its radius using the same rounded-corners shader. + edgeEffectMaskingInfo.BlendRange = EdgeEffect.Radius; + edgeEffectMaskingInfo.AlphaExponent = 2; + edgeEffectMaskingInfo.Hollow = EdgeEffect.Hollow; + + GLWrapper.PushMaskingInfo(edgeEffectMaskingInfo); + + GLWrapper.SetBlend(new BlendingInfo(EdgeEffect.Type == EdgeEffectType.Glow ? BlendingMode.Additive : BlendingMode.Mixture)); + + Shader.Bind(); + + ColourInfo colour = ColourInfo.SingleColour(EdgeEffect.Colour); + colour.TopLeft.MultiplyAlpha(DrawInfo.Colour.TopLeft.Linear.A); + colour.BottomLeft.MultiplyAlpha(DrawInfo.Colour.BottomLeft.Linear.A); + colour.TopRight.MultiplyAlpha(DrawInfo.Colour.TopRight.Linear.A); + colour.BottomRight.MultiplyAlpha(DrawInfo.Colour.BottomRight.Linear.A); + + Texture.WhitePixel.DrawQuad( + ScreenSpaceMaskingQuad.Value, + colour, null, null, null, + // HACK HACK HACK. We re-use the unused vertex blend range to store the original + // masking blend range when rendering edge effects. This is needed for smooth inner edges + // with a hollow edge effect. + new Vector2(MaskingInfo.Value.BlendRange)); + + Shader.Unbind(); + + GLWrapper.PopMaskingInfo(); + } + + private const int min_amount_children_to_warrant_batch = 5; + + private bool mayHaveOwnVertexBatch(int amountChildren) => Shared.ForceOwnVertexBatch || amountChildren >= min_amount_children_to_warrant_batch; + + private void updateVertexBatch() + { + if (Children == null) + return; + + // This logic got roughly copied from the old osu! code base. These constants seem to have worked well so far. + int clampedAmountChildren = MathHelper.Clamp(Children.Count, 1, 1000); + if (mayHaveOwnVertexBatch(clampedAmountChildren) && (Shared.VertexBatch == null || Shared.VertexBatch.Size < clampedAmountChildren)) + Shared.VertexBatch = new QuadBatch(clampedAmountChildren * 2, 500); + } + + public override void Draw(Action vertexAction) + { + updateVertexBatch(); + + // Prefer to use own vertex batch instead of the parent-owned one. + if (Shared.VertexBatch != null) + vertexAction = Shared.VertexBatch.AddAction; + + base.Draw(vertexAction); + + drawEdgeEffect(); + if (MaskingInfo != null) + { + MaskingInfo info = MaskingInfo.Value; + if (info.BorderThickness > 0) + info.BorderColour *= DrawInfo.Colour.AverageColour; + + GLWrapper.PushMaskingInfo(info); + } + + if (Children != null) + foreach (DrawNode child in Children) + child.Draw(vertexAction); + + if (MaskingInfo != null) + GLWrapper.PopMaskingInfo(); + } + } +} diff --git a/osu.Framework/Graphics/Containers/CompositeDrawable.cs b/osu.Framework/Graphics/Containers/CompositeDrawable.cs index 6eece7df6..eb9a79378 100644 --- a/osu.Framework/Graphics/Containers/CompositeDrawable.cs +++ b/osu.Framework/Graphics/Containers/CompositeDrawable.cs @@ -1,1404 +1,1404 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Lists; -using System.Collections.Generic; -using System; -using System.Diagnostics; -using OpenTK; -using osu.Framework.Graphics.OpenGL; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Colour; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Timing; -using osu.Framework.Caching; -using osu.Framework.Threading; -using osu.Framework.Statistics; -using System.Threading.Tasks; -using osu.Framework.Graphics.Primitives; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A drawable consisting of a composite of child drawables which are - /// manages by the composite object itself. Transformations applied to - /// a are also applied to its children. - /// Additionally, s support various effects, such as masking, edge effect, - /// padding, and automatic sizing depending on their children. - /// - public abstract class CompositeDrawable : Drawable - { - #region Contruction and disposal - - /// - /// Contructs a that stores children. - /// - protected CompositeDrawable() - { - internalChildren = new SortedList(new ChildComparer(this)); - aliveInternalChildren = new SortedList(new ChildComparer(this)); - } - - private Game game; - - /// - /// Loads a future child or grand-child of this asyncronously. - /// and are inherited from this . - /// - /// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level, - /// consider using - /// - /// The type of the future future child or grand-child to be loaded. - /// The type of the future future child or grand-child to be loaded. - /// Callback to be invoked on the update thread after loading is complete. - /// The task which is used for loading and callbacks. - protected Task LoadComponentAsync(TLoadable component, Action onLoaded = null) where TLoadable : Drawable - { - if (game == null) - throw new InvalidOperationException($"May not invoke {nameof(LoadComponentAsync)} prior to this {nameof(CompositeDrawable)} being loaded."); - - return component.LoadAsync(game, this, () => onLoaded?.Invoke(component)); - } - - [BackgroundDependencyLoader(true)] - private void load(Game game, ShaderManager shaders) - { - this.game = game; - - if (shader == null) - shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); - - // We are in a potentially async context, so let's aggressively load all our children - // regardless of their alive state. this also gives children a clock so they can be checked - // for their correct alive state in the case LifetimeStart is set to a definite value. - internalChildren.ForEach(loadChild); - - // Let's also perform an update on our children's life to add any alive children. - UpdateChildrenLife(); - } - - private void loadChild(Drawable child) - { - child.Load(Clock, Dependencies); - child.Parent = this; - } - - protected override void LoadComplete() - { - schedulerAfterChildren?.SetCurrentThread(MainThread); - base.LoadComplete(); - } - - protected override void Dispose(bool isDisposing) - { - InternalChildren?.ForEach(c => c.Dispose()); - - OnAutoSize = null; - schedulerAfterChildren = null; - - base.Dispose(isDisposing); - } - - #endregion - - #region Children management - - /// - /// Invoked when a child has entered . - /// - internal event Action ChildBecameAlive; - - /// - /// Invoked when a child has left . - /// - internal event Action ChildDied; - - /// - /// Gets or sets the only child in . - /// - protected internal Drawable InternalChild - { - get - { - if (InternalChildren.Count != 1) - throw new InvalidOperationException($"{nameof(InternalChild)} is only available when there's only 1 in {nameof(InternalChildren)}!"); - - return InternalChildren[0]; - } - set - { - ClearInternal(); - AddInternal(value); - } - } - - protected class ChildComparer : IComparer - { - private readonly CompositeDrawable owner; - - public ChildComparer(CompositeDrawable owner) - { - this.owner = owner; - } - - public int Compare(Drawable x, Drawable y) => owner.Compare(x, y); - } - - /// - /// Compares two to determine their sorting. - /// - /// The first child to compare. - /// The second child to compare. - /// -1 if comes before , and 1 otherwise. - protected virtual int Compare(Drawable x, Drawable y) - { - if (x == null) throw new ArgumentNullException(nameof(x)); - if (y == null) throw new ArgumentNullException(nameof(y)); - - int i = y.Depth.CompareTo(x.Depth); - if (i != 0) return i; - return x.ChildID.CompareTo(y.ChildID); - } - - /// - /// Helper method comparing children by their depth first, and then by their reversed child ID. - /// - /// The first child to compare. - /// The second child to compare. - /// -1 if comes before , and 1 otherwise. - protected int CompareReverseChildID(Drawable x, Drawable y) - { - if (x == null) throw new ArgumentNullException(nameof(x)); - if (y == null) throw new ArgumentNullException(nameof(y)); - - int i = y.Depth.CompareTo(x.Depth); - if (i != 0) return i; - return y.ChildID.CompareTo(x.ChildID); - } - - private readonly SortedList internalChildren; - /// - /// This list of children. Assigning to this property will dispose all existing children of this . - /// - protected internal IReadOnlyList InternalChildren - { - get { return internalChildren; } - set { InternalChildrenEnumerable = value; } - } - - /// - /// Replaces all internal children of this with the elements contained in the enumerable. - /// - protected internal IEnumerable InternalChildrenEnumerable - { - set - { - ClearInternal(); - AddRangeInternal(value); - } - } - - private readonly SortedList aliveInternalChildren; - protected internal IReadOnlyList AliveInternalChildren => aliveInternalChildren; - - /// - /// The index of a given child within . - /// - /// - /// If the child is found, its index. Otherwise, the negated index it would obtain - /// if it were added to . - /// - protected internal int IndexOfInternal(Drawable drawable) => internalChildren.IndexOf(drawable); - - /// - /// Checks whether a given child is contained within . - /// - protected internal bool ContainsInternal(Drawable drawable) => IndexOfInternal(drawable) >= 0; - - /// - /// Removes a given child from this . - /// - /// The to be removed. - /// False if was not a child of this and true otherwise. - protected internal virtual bool RemoveInternal(Drawable drawable) - { - if (drawable == null) - throw new ArgumentNullException(nameof(drawable)); - - int index = IndexOfInternal(drawable); - if (index < 0) - return false; - - internalChildren.RemoveAt(index); - if (drawable.IsAlive) - { - aliveInternalChildren.Remove(drawable); - ChildDied?.Invoke(drawable); - } - - if (drawable.LoadState >= LoadState.Ready && drawable.Parent != this) - throw new InvalidOperationException($@"Removed a drawable ({drawable}) whose parent was not this ({this}), but {drawable.Parent}."); - - drawable.Parent = null; - drawable.IsAlive = false; - - if (AutoSizeAxes != Axes.None) - InvalidateFromChild(Invalidation.RequiredParentSizeToFit); - - return true; - } - - /// - /// Clear all of . - /// - /// - /// Whether removed children should also get disposed. - /// Disposal will be recursive. - /// - protected internal virtual void ClearInternal(bool disposeChildren = true) - { - if (internalChildren.Count == 0) return; - - foreach (Drawable t in internalChildren) - { - if (t.IsAlive) - ChildDied?.Invoke(t); - - t.IsAlive = false; - - if (disposeChildren) - { - //cascade disposal - (t as CompositeDrawable)?.ClearInternal(); - t.Dispose(); - } - else - t.Parent = null; - - Trace.Assert(t.Parent == null); - } - - internalChildren.Clear(); - aliveInternalChildren.Clear(); - - if (AutoSizeAxes != Axes.None) - InvalidateFromChild(Invalidation.RequiredParentSizeToFit); - } - - /// - /// Used to assign a monotonically increasing ID to children as they are added. This member is - /// incremented whenever a child is added. - /// - private ulong currentChildID; - - /// - /// Adds a child to . - /// - protected internal virtual void AddInternal(Drawable drawable) - { - if (drawable == null) - throw new ArgumentNullException(nameof(drawable), $"null {nameof(Drawable)}s may not be added to {nameof(CompositeDrawable)}."); - - if (drawable == this) - throw new InvalidOperationException($"{nameof(CompositeDrawable)} may not be added to itself."); - - // If the drawable's ChildId is not zero, then it was added to another parent even if it wasn't loaded - if (drawable.ChildID != 0) - throw new InvalidOperationException("May not add a drawable to multiple containers."); - - drawable.ChildID = ++currentChildID; - drawable.RemoveCompletedTransforms = RemoveCompletedTransforms; - - if (drawable.LoadState >= LoadState.Ready) - drawable.Parent = this; - // If we're already loaded, we can eagerly allow children to be loaded - else if (LoadState >= LoadState.Loading) - loadChild(drawable); - - internalChildren.Add(drawable); - checkChildLife(drawable); - - if (AutoSizeAxes != Axes.None) - InvalidateFromChild(Invalidation.RequiredParentSizeToFit); - } - - /// - /// Adds a range of children to . This is equivalent to calling - /// on each element of the range in order. - /// - protected internal void AddRangeInternal(IEnumerable range) - { - foreach (Drawable d in range) - AddInternal(d); - } - - /// - /// Changes the depth of an internal child. This affects ordering of . - /// - /// The child whose depth is to be changed. - /// The new depth value to be set. - protected internal void ChangeInternalChildDepth(Drawable child, float newDepth) - { - if (child.Depth == newDepth) return; - - var index = IndexOfInternal(child); - if (index < 0) - throw new InvalidOperationException($"Can not change depth of drawable which is not contained within this {nameof(CompositeDrawable)}."); - - internalChildren.RemoveAt(index); - var aliveIndex = aliveInternalChildren.IndexOf(child); - if (aliveIndex >= 0) // remove if found - aliveInternalChildren.RemoveAt(aliveIndex); - - var chId = child.ChildID; - child.ChildID = 0; // ensure Depth-change does not throw an exception - child.Depth = newDepth; - child.ChildID = chId; - - internalChildren.Add(child); - if (aliveIndex >= 0) // re-add if it used to be in aliveInternalChildren - aliveInternalChildren.Add(child); - } - - #endregion - - #region Updating (per-frame periodic) - - private Scheduler schedulerAfterChildren; - - protected Scheduler SchedulerAfterChildren => schedulerAfterChildren ?? (schedulerAfterChildren = new Scheduler(MainThread, Clock)); - - /// - /// Updates the life status of according to their - /// property. - /// - /// True iff the life status of at least one child changed. - protected virtual bool UpdateChildrenLife() - { - bool anyAliveChanged = false; - - // checkChildLife may remove a child from internalChildren. In order to not skip children, - // we keep track of the original amount children to apply an offset to the iterator - int originalCount = internalChildren.Count; - for (int i = 0; i < internalChildren.Count; i++) - anyAliveChanged |= checkChildLife(internalChildren[i + internalChildren.Count - originalCount]); - - if (anyAliveChanged) - childrenSizeDependencies.Invalidate(); - - return anyAliveChanged; - } - - /// - /// Checks whether the alive state of a child has changed processes it. This will add or remove - /// the child from depending on its alive state. - /// - /// The child to check. - /// Whether the child's alive state has changed. - private bool checkChildLife(Drawable child) - { - Debug.Assert(internalChildren.Contains(child), "Can only check and react to the life of our own children."); - - // Can not have alive children if we are not loaded. - if (LoadState < LoadState.Ready) - return false; - - bool changed = false; - - if (child.ShouldBeAlive) - { - if (!child.IsAlive) - { - loadChild(child); - - if (child.LoadState >= LoadState.Ready) - { - aliveInternalChildren.Add(child); - ChildBecameAlive?.Invoke(child); - child.IsAlive = true; - changed = true; - } - } - } - else - { - if (child.IsAlive) - { - aliveInternalChildren.Remove(child); - ChildDied?.Invoke(child); - child.IsAlive = false; - changed = true; - } - - if (child.RemoveWhenNotAlive) - { - RemoveInternal(child); - - if (child.DisposeOnDeathRemoval) - child.Dispose(); - } - } - - return changed; - } - - internal override void UpdateClock(IFrameBasedClock clock) - { - if (Clock == clock) - return; - - base.UpdateClock(clock); - foreach (Drawable child in internalChildren) - child.UpdateClock(Clock); - - schedulerAfterChildren?.UpdateClock(Clock); - } - - /// - /// Specifies whether this requires an update of its children. - /// If the return value is false, then children are not updated and - /// is not called. - /// - protected virtual bool RequiresChildrenUpdate => !IsMaskedAway || !childrenSizeDependencies.IsValid; - - public override bool UpdateSubTree() - { - if (!base.UpdateSubTree()) return false; - - // We update our children's life even if we are invisible. - // Note, that this does not propagate down and may need - // generalization in the future. - UpdateChildrenLife(); - - // If we are not present then there is never a reason to check - // for children, as they should never affect our present status. - if (!IsPresent || !RequiresChildrenUpdate) return false; - - UpdateAfterChildrenLife(); - - // We iterate by index to gain performance - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < aliveInternalChildren.Count; ++i) - { - Drawable c = aliveInternalChildren[i]; - Debug.Assert(c.LoadState >= LoadState.Ready); - c.UpdateSubTree(); - } - - if (schedulerAfterChildren != null) - { - int amountScheduledTasks = schedulerAfterChildren.Update(); - FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks); - } - - UpdateAfterChildren(); - - updateChildrenSizeDependencies(); - UpdateAfterAutoSize(); - return true; - } - - /// - /// Updates all masking calculations for this and its . - /// This occurs post- to ensure that all updates have taken place. - /// - /// The parent that triggered this update on this . - /// The that defines the masking bounds. - /// Whether masking calculations have taken place. - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) - { - if (!base.UpdateSubTreeMasking(source, maskingBounds)) - return false; - - if (IsMaskedAway) - return true; - - if (aliveInternalChildren.Count == 0) - return true; - - if (RequiresChildrenUpdate) - { - var childMaskingBounds = ComputeChildMaskingBounds(maskingBounds); - - - // We iterate by index to gain performance - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < aliveInternalChildren.Count; i++) - aliveInternalChildren[i].UpdateSubTreeMasking(this, childMaskingBounds); - } - - return true; - } - - protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) - { - if (!CanBeFlattened) - return base.ComputeIsMaskedAway(maskingBounds); - - // The masking check is overly expensive (requires creation of ScreenSpaceDrawQuad) - // when only few children exist. - return aliveInternalChildren.Count >= amount_children_required_for_masking_check && base.ComputeIsMaskedAway(maskingBounds); - } - - /// - /// Computes the to be used as the masking bounds for all . - /// - /// The that defines the masking bounds for this . - /// The to be used as the masking bounds for . - protected virtual RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => Masking ? RectangleF.Intersect(maskingBounds, ScreenSpaceDrawQuad.AABBFloat) : maskingBounds; - - /// - /// Invoked after and state checks have taken place, - /// but before is invoked for all . - /// This occurs after has been invoked on this - /// - protected virtual void UpdateAfterChildrenLife() - { - } - - /// - /// An opportunity to update state once-per-frame after has been called - /// for all . - /// This is invoked prior to any autosize calculations of this . - /// - protected virtual void UpdateAfterChildren() - { - } - - /// - /// Invoked after all autosize calculations have taken place. - /// - protected virtual void UpdateAfterAutoSize() - { - } - - #endregion - - #region Invalidation - - /// - /// Informs this that a child has been invalidated. - /// - /// The type of invalidation applied to the child. - public virtual void InvalidateFromChild(Invalidation invalidation) - { - //Colour captures potential changes in IsPresent. If this ever becomes a bottleneck, - //Invalidation could be further separated into presence changes. - if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) - childrenSizeDependencies.Invalidate(); - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if (!base.Invalidate(invalidation, source, shallPropagate)) - return false; - - if (!shallPropagate) return true; - - // This way of looping turns out to be slightly faster than a foreach - // or directly indexing a SortedList. This part of the code is often - // hot, so an optimization like this makes sense here. - SortedList current = internalChildren; - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < current.Count; ++i) - { - Drawable c = current[i]; - Debug.Assert(c != source); - - Invalidation childInvalidation = invalidation; - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - childInvalidation |= Invalidation.DrawInfo; - - // Other geometry things like rotation, shearing, etc don't affect child properties. - childInvalidation &= ~Invalidation.MiscGeometry; - - // Relative positioning can however affect child geometry - // ReSharper disable once PossibleNullReferenceException - if (c.RelativePositionAxes != Axes.None && (invalidation & Invalidation.DrawSize) > 0) - childInvalidation |= Invalidation.MiscGeometry; - - // No draw size changes if relative size axes does not propagate it downward. - if (c.RelativeSizeAxes == Axes.None) - childInvalidation &= ~Invalidation.DrawSize; - - c.Invalidate(childInvalidation, this); - } - - return true; - } - - #endregion - - #region DrawNode - - private readonly CompositeDrawNodeSharedData compositeDrawNodeSharedData = new CompositeDrawNodeSharedData(); - private Shader shader; - - protected override DrawNode CreateDrawNode() => new CompositeDrawNode(); - - protected override void ApplyDrawNode(DrawNode node) - { - CompositeDrawNode n = (CompositeDrawNode)node; - - if (!Masking && (BorderThickness != 0.0f || EdgeEffect.Type != EdgeEffectType.None)) - throw new InvalidOperationException("Can not have border effects/edge effects if masking is disabled."); - - Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); - - n.MaskingInfo = !Masking - ? (MaskingInfo?)null - : new MaskingInfo - { - ScreenSpaceAABB = ScreenSpaceDrawQuad.AABB, - MaskingRect = DrawRectangle, - ToMaskingSpace = DrawInfo.MatrixInverse, - CornerRadius = CornerRadius, - BorderThickness = BorderThickness, - BorderColour = BorderColour, - // We are setting the linear blend range to the approximate size of a _pixel_ here. - // This results in the optimal trade-off between crispness and smoothness of the - // edges of the masked region according to sampling theory. - BlendRange = MaskingSmoothness * (scale.X + scale.Y) / 2, - AlphaExponent = 1, - }; - - n.EdgeEffect = EdgeEffect; - - n.ScreenSpaceMaskingQuad = null; - n.Shared = compositeDrawNodeSharedData; - - n.Shader = shader; - - base.ApplyDrawNode(node); - } - - protected virtual bool CanBeFlattened => !Masking; - - private const int amount_children_required_for_masking_check = 2; - - /// - /// This function adds all children's s to a target List, flattening the children of certain types - /// of subtrees for optimization purposes. - /// - /// The index of the currently in-use DrawNode tree. - /// The running index into the target List. - /// The whose children's DrawNodes to add. - /// The target list to fill with DrawNodes. - private static void addFromComposite(int treeIndex, ref int j, CompositeDrawable parentComposite, List target) - { - SortedList current = parentComposite.aliveInternalChildren; - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < current.Count; ++i) - { - Drawable drawable = current[i]; - - // If we are proxied somewhere, then we want to be drawn at the proxy's location - // in the scene graph, rather than at our own location, thus no draw nodes for us. - if (drawable.HasProxy) - continue; - - // Take drawable.Original until drawable.Original == drawable - while (drawable != (drawable = drawable.Original)) - { - } - - if (!drawable.IsPresent) - continue; - - CompositeDrawable composite = drawable as CompositeDrawable; - if (composite?.CanBeFlattened == true) - { - if (!composite.IsMaskedAway) - addFromComposite(treeIndex, ref j, composite, target); - - continue; - } - - if (drawable.IsMaskedAway) - continue; - - DrawNode next = drawable.GenerateDrawNodeSubtree(treeIndex); - if (next == null) - continue; - - if (j < target.Count) - target[j] = next; - else - target.Add(next); - - j++; - } - } - - internal virtual bool AddChildDrawNodes => true; - - internal override DrawNode GenerateDrawNodeSubtree(int treeIndex) - { - // No need for a draw node at all if there are no children and we are not glowing. - if (aliveInternalChildren.Count == 0 && CanBeFlattened) - return null; - - CompositeDrawNode cNode = base.GenerateDrawNodeSubtree(treeIndex) as CompositeDrawNode; - if (cNode == null) - return null; - - if (cNode.Children == null) - cNode.Children = new List(aliveInternalChildren.Count); - - if (AddChildDrawNodes) - { - List target = cNode.Children; - - int j = 0; - addFromComposite(treeIndex, ref j, this, target); - - if (j < target.Count) - target.RemoveRange(j, target.Count - j); - } - - return cNode; - } - - #endregion - - #region Transforms - - /// - /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. - /// - /// This value is passed down to children. - /// - /// - public override bool RemoveCompletedTransforms - { - get { return base.RemoveCompletedTransforms; } - internal set - { - if (base.RemoveCompletedTransforms == value) - return; - base.RemoveCompletedTransforms = value; - - foreach (var c in internalChildren) - c.RemoveCompletedTransforms = RemoveCompletedTransforms; - } - } - - public override void ApplyTransformsAt(double time, bool propagateChildren = false) - { - base.ApplyTransformsAt(time, propagateChildren); - - if (!propagateChildren) - return; - - foreach (var c in internalChildren) - c.ApplyTransformsAt(time, true); - } - - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) - { - base.ClearTransformsAfter(time, propagateChildren, targetMember); - - if (!propagateChildren) - return; - - foreach (var c in internalChildren) - c.ClearTransformsAfter(time, true, targetMember); - } - - internal override void AddDelay(double duration, bool propagateChildren = false) - { - if (duration == 0) - return; - - base.AddDelay(duration, propagateChildren); - - if (propagateChildren) - foreach (var c in internalChildren) - c.AddDelay(duration, true); - } - - protected ScheduledDelegate ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay); - - public override InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false) - { - var baseDisposalAction = base.BeginAbsoluteSequence(newTransformStartTime, recursive); - if (!recursive) - return baseDisposalAction; - - List disposalActions = new List(internalChildren.Count + 1) { baseDisposalAction }; - foreach (var c in internalChildren) - disposalActions.Add(c.BeginAbsoluteSequence(newTransformStartTime, true)); - - return new InvokeOnDisposal(() => - { - foreach (var a in disposalActions) - a.Dispose(); - }); - } - - public override void FinishTransforms(bool propagateChildren = false, string targetMember = null) - { - base.FinishTransforms(propagateChildren, targetMember); - - if (propagateChildren) - foreach (var c in internalChildren) - c.FinishTransforms(true, targetMember); - } - - /// - /// Helper function for creating and adding a that fades the current . - /// - protected TransformSequence FadeEdgeEffectTo(float newAlpha, double duration = 0, Easing easing = Easing.None) - { - Color4 targetColour = EdgeEffect.Colour; - targetColour.A = newAlpha; - return FadeEdgeEffectTo(targetColour, duration, easing); - } - - /// - /// Helper function for creating and adding a that fades the current . - /// - protected TransformSequence FadeEdgeEffectTo(Color4 newColour, double duration = 0, Easing easing = Easing.None) - { - var effect = EdgeEffect; - effect.Colour = newColour; - return TweenEdgeEffectTo(effect, duration, easing); - } - - /// - /// Helper function for creating and adding a that tweens the current . - /// - protected TransformSequence TweenEdgeEffectTo(EdgeEffectParameters newParams, double duration = 0, Easing easing = Easing.None) => - this.TransformTo(nameof(EdgeEffect), newParams, duration, easing); - - #endregion - - #region Interaction / Input - - // Required to pass through input to children by default. - // TODO: Evaluate effects of this on performance and address. - public override bool HandleKeyboardInput => true; - public override bool HandleMouseInput => true; - - public override bool Contains(Vector2 screenSpacePos) - { - float cRadius = CornerRadius; - - // Select a cheaper contains method when we don't need rounded edges. - if (cRadius == 0.0f) - return base.Contains(screenSpacePos); - return DrawRectangle.Shrink(cRadius).DistanceSquared(ToLocalSpace(screenSpacePos)) <= cRadius * cRadius; - } - - internal override bool BuildKeyboardInputQueue(List queue) - { - if (!base.BuildKeyboardInputQueue(queue)) - return false; - - // We iterate by index to gain performance - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < aliveInternalChildren.Count; ++i) - aliveInternalChildren[i].BuildKeyboardInputQueue(queue); - - return true; - } - - internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) - { - if (!base.BuildMouseInputQueue(screenSpaceMousePos, queue) && (!CanReceiveMouseInput || Masking)) - return false; - - // We iterate by index to gain performance - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < aliveInternalChildren.Count; ++i) - aliveInternalChildren[i].BuildMouseInputQueue(screenSpaceMousePos, queue); - - return true; - } - - #endregion - - #region Masking and related effects (e.g. round corners) - - private bool masking; - - /// - /// If enabled, only the portion of children that falls within this 's - /// shape is drawn to the screen. - /// - public bool Masking - { - get { return masking; } - protected set - { - if (masking == value) - return; - - masking = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float maskingSmoothness = 1; - - /// - /// Determines over how many pixels the alpha component smoothly fades out. - /// Only has an effect when is true. - /// - public float MaskingSmoothness - { - get { return maskingSmoothness; } - protected set - { - //must be above zero to avoid a div-by-zero in the shader logic. - value = Math.Max(0.01f, value); - - if (maskingSmoothness == value) - return; - - maskingSmoothness = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float cornerRadius; - - /// - /// Determines how large of a radius is masked away around the corners. - /// Only has an effect when is true. - /// - public float CornerRadius - { - get { return cornerRadius; } - protected set - { - if (cornerRadius == value) - return; - - cornerRadius = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float borderThickness; - - /// - /// Determines how thick of a border to draw around the inside of the masked region. - /// Only has an effect when is true. - /// The border only is drawn on top of children using a sprite shader. - /// - /// - /// Drawing borders is optimized heavily into our sprite shaders. As a consequence - /// borders are only drawn correctly on top of quad-shaped children using our sprite - /// shaders. - /// - public float BorderThickness - { - get { return borderThickness; } - protected set - { - if (borderThickness == value) - return; - - borderThickness = value; - Invalidate(Invalidation.DrawNode); - } - } - - private SRGBColour borderColour = Color4.Black; - - /// - /// Determines the color of the border controlled by . - /// Only has an effect when is true. - /// - public SRGBColour BorderColour - { - get { return borderColour; } - protected set - { - if (borderColour.Equals(value)) - return; - - borderColour = value; - Invalidate(Invalidation.DrawNode); - } - } - - private EdgeEffectParameters edgeEffect; - - /// - /// Determines an edge effect of this . - /// Edge effects are e.g. glow or a shadow. - /// Only has an effect when is true. - /// - public EdgeEffectParameters EdgeEffect - { - get { return edgeEffect; } - protected set - { - if (edgeEffect.Equals(value)) - return; - - edgeEffect = value; - Invalidate(Invalidation.DrawNode); - } - } - - #endregion - - #region Sizing - - public override RectangleF BoundingBox - { - get - { - float cRadius = CornerRadius; - if (cRadius == 0.0f) - return base.BoundingBox; - - RectangleF drawRect = LayoutRectangle.Shrink(cRadius); - - // Inflate bounding box in parent space by the half-size of the bounding box of the - // ellipse obtained by transforming the unit circle into parent space. - Vector2 offset = ToParentSpace(Vector2.Zero); - Vector2 u = ToParentSpace(new Vector2(cRadius, 0)) - offset; - Vector2 v = ToParentSpace(new Vector2(0, cRadius)) - offset; - Vector2 inflation = new Vector2((float)Math.Sqrt(u.X * u.X + v.X * v.X), (float)Math.Sqrt(u.Y * u.Y + v.Y * v.Y)); - - RectangleF result = ToParentSpace(drawRect).AABBFloat.Inflate(inflation); - // The above algorithm will return incorrect results if the rounded corners are not fully visible. - // To limit bad behavior we at least enforce here, that the bounding box with rounded corners - // is never larger than the bounding box without. - if (DrawSize.X < CornerRadius * 2 || DrawSize.Y < CornerRadius * 2) - result.Intersect(base.BoundingBox); - - return result; - } - } - - private MarginPadding padding; - - /// - /// Shrinks the space children may occupy within this - /// by the specified amount on each side. - /// - public MarginPadding Padding - { - get { return padding; } - protected set - { - if (padding.Equals(value)) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}."); - - padding = value; - - foreach (Drawable c in internalChildren) - c.Invalidate(c.InvalidationFromParentSize); - } - } - - /// - /// The size of the coordinate space revealed to . - /// Captures the effect of e.g. . - /// - public Vector2 ChildSize => DrawSize - new Vector2(Padding.TotalHorizontal, Padding.TotalVertical); - - /// - /// Positional offset applied to . - /// Captures the effect of e.g. . - /// - public Vector2 ChildOffset => new Vector2(Padding.Left, Padding.Top); - - private Vector2 relativeChildSize = Vector2.One; - - /// - /// The size of the relative position/size coordinate space of children of this . - /// Children positioned at this size will appear as if they were positioned at = in this . - /// - public Vector2 RelativeChildSize - { - get { return relativeChildSize; } - protected set - { - if (relativeChildSize == value) - return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be finite, but is {value}."); - if (value.X == 0 || value.Y == 0) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be non-zero, but is {value}."); - - relativeChildSize = value; - - foreach (Drawable c in internalChildren) - c.Invalidate(c.InvalidationFromParentSize); - } - } - - private Vector2 relativeChildOffset = Vector2.Zero; - - /// - /// The offset of the relative position/size coordinate space of children of this . - /// Children positioned at this offset will appear as if they were positioned at = in this . - /// - public Vector2 RelativeChildOffset - { - get { return relativeChildOffset; } - protected set - { - if (relativeChildOffset == value) - return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildOffset)} must be finite, but is {value}."); - - relativeChildOffset = value; - - foreach (Drawable c in internalChildren) - c.Invalidate(c.InvalidationFromParentSize & ~Invalidation.DrawSize); - } - } - - /// - /// Conversion factor from relative to absolute coordinates in our space. - /// - public Vector2 RelativeToAbsoluteFactor => Vector2.Divide(ChildSize, RelativeChildSize); - - /// - /// Tweens the of this . - /// - /// The coordinate space to tween to. - /// The tween duration. - /// The tween easing. - protected TransformSequence TransformRelativeChildSizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) => - this.TransformTo(nameof(RelativeChildSize), newSize, duration, easing); - - /// - /// Tweens the of this . - /// - /// The coordinate space to tween to. - /// The tween duration. - /// The tween easing. - protected TransformSequence TransformRelativeChildOffsetTo(Vector2 newOffset, double duration = 0, Easing easing = Easing.None) => - this.TransformTo(nameof(RelativeChildOffset), newOffset, duration, easing); - - public override Axes RelativeSizeAxes - { - get { return base.RelativeSizeAxes; } - set - { - if ((AutoSizeAxes & value) != 0) - throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); - - base.RelativeSizeAxes = value; - } - } - - private Axes autoSizeAxes; - - /// - /// Controls which are automatically sized w.r.t. . - /// Children's are ignored for automatic sizing. - /// Most notably, and of children - /// do not affect automatic sizing to avoid circular size dependencies. - /// It is not allowed to manually set (or / ) - /// on any which are automatically sized. - /// - public virtual Axes AutoSizeAxes - { - get { return autoSizeAxes; } - protected set - { - if (value == autoSizeAxes) - return; - - if ((RelativeSizeAxes & value) != 0) - throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); - - autoSizeAxes = value; - childrenSizeDependencies.Invalidate(); - OnSizingChanged(); - } - } - - /// - /// The duration which automatic sizing should take. If zero, then it is instantaneous. - /// Otherwise, this is equivalent to applying an automatic size via a resize transform. - /// - public float AutoSizeDuration { get; protected set; } - - /// - /// The type of easing which should be used for smooth automatic sizing when - /// is non-zero. - /// - public Easing AutoSizeEasing { get; protected set; } - - /// - /// THIS EVENT PURELY EXISTS FOR THE SCENE GRAPH VISUALIZER. DO NOT USE. - /// This event will fire after our is updated from autosizing. - /// - internal event Action OnAutoSize; - - private Cached childrenSizeDependencies = new Cached(); - - public override float Width - { - get - { - if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && (AutoSizeAxes & Axes.X) > 0) - updateChildrenSizeDependencies(); - return base.Width; - } - - set - { - if ((AutoSizeAxes & Axes.X) != 0) - throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); - base.Width = value; - } - } - - public override float Height - { - get - { - if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && (AutoSizeAxes & Axes.Y) > 0) - updateChildrenSizeDependencies(); - return base.Height; - } - - set - { - if ((AutoSizeAxes & Axes.Y) != 0) - throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); - base.Height = value; - } - } - - private bool isComputingChildrenSizeDependencies; - - public override Vector2 Size - { - get - { - if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && AutoSizeAxes != Axes.None) - updateChildrenSizeDependencies(); - return base.Size; - } - - set - { - if ((AutoSizeAxes & Axes.Both) != 0) - throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); - base.Size = value; - } - } - - private Vector2 computeAutoSize() - { - MarginPadding originalPadding = Padding; - MarginPadding originalMargin = Margin; - - try - { - Padding = new MarginPadding(); - Margin = new MarginPadding(); - - if (AutoSizeAxes == Axes.None) return DrawSize; - - Vector2 maxBoundSize = Vector2.Zero; - - // Find the maximum width/height of children - foreach (Drawable c in AliveInternalChildren) - { - if (!c.IsPresent) - continue; - - Vector2 cBound = c.RequiredParentSizeToFit; - - if ((c.BypassAutoSizeAxes & Axes.X) == 0) - maxBoundSize.X = Math.Max(maxBoundSize.X, cBound.X); - - if ((c.BypassAutoSizeAxes & Axes.Y) == 0) - maxBoundSize.Y = Math.Max(maxBoundSize.Y, cBound.Y); - } - - if ((AutoSizeAxes & Axes.X) == 0) - maxBoundSize.X = DrawSize.X; - if ((AutoSizeAxes & Axes.Y) == 0) - maxBoundSize.Y = DrawSize.Y; - - return new Vector2(maxBoundSize.X, maxBoundSize.Y); - } - finally - { - Padding = originalPadding; - Margin = originalMargin; - } - } - - private void updateAutoSize() - { - if (AutoSizeAxes == Axes.None) - return; - - Vector2 b = computeAutoSize() + Padding.Total; - - autoSizeResizeTo(new Vector2( - (AutoSizeAxes & Axes.X) > 0 ? b.X : base.Width, - (AutoSizeAxes & Axes.Y) > 0 ? b.Y : base.Height - ), AutoSizeDuration, AutoSizeEasing); - - //note that this is called before autoSize becomes valid. may be something to consider down the line. - //might work better to add an OnRefresh event in Cached<> and invoke there. - OnAutoSize?.Invoke(); - } - - private void updateChildrenSizeDependencies() - { - isComputingChildrenSizeDependencies = true; - - try - { - if (!childrenSizeDependencies.IsValid) - { - updateAutoSize(); - childrenSizeDependencies.Validate(); - } - } - finally - { - isComputingChildrenSizeDependencies = false; - } - } - - private void autoSizeResizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) => - this.TransformTo(this.PopulateTransform(new AutoSizeTransform { Rewindable = false }, newSize, duration, easing)); - - /// - /// A helper property for to change the size of s with . - /// - private Vector2 baseSize - { - get { return new Vector2(base.Width, base.Height); } - - set - { - base.Width = value.X; - base.Height = value.Y; - } - } - - private class AutoSizeTransform : TransformCustom - { - public AutoSizeTransform() - : base(nameof(baseSize)) - { - } - } - - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Lists; +using System.Collections.Generic; +using System; +using System.Diagnostics; +using OpenTK; +using osu.Framework.Graphics.OpenGL; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Colour; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Timing; +using osu.Framework.Caching; +using osu.Framework.Threading; +using osu.Framework.Statistics; +using System.Threading.Tasks; +using osu.Framework.Graphics.Primitives; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A drawable consisting of a composite of child drawables which are + /// manages by the composite object itself. Transformations applied to + /// a are also applied to its children. + /// Additionally, s support various effects, such as masking, edge effect, + /// padding, and automatic sizing depending on their children. + /// + public abstract class CompositeDrawable : Drawable + { + #region Contruction and disposal + + /// + /// Contructs a that stores children. + /// + protected CompositeDrawable() + { + internalChildren = new SortedList(new ChildComparer(this)); + aliveInternalChildren = new SortedList(new ChildComparer(this)); + } + + private Game game; + + /// + /// Loads a future child or grand-child of this asyncronously. + /// and are inherited from this . + /// + /// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level, + /// consider using + /// + /// The type of the future future child or grand-child to be loaded. + /// The type of the future future child or grand-child to be loaded. + /// Callback to be invoked on the update thread after loading is complete. + /// The task which is used for loading and callbacks. + protected Task LoadComponentAsync(TLoadable component, Action onLoaded = null) where TLoadable : Drawable + { + if (game == null) + throw new InvalidOperationException($"May not invoke {nameof(LoadComponentAsync)} prior to this {nameof(CompositeDrawable)} being loaded."); + + return component.LoadAsync(game, this, () => onLoaded?.Invoke(component)); + } + + [BackgroundDependencyLoader(true)] + private void load(Game game, ShaderManager shaders) + { + this.game = game; + + if (shader == null) + shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + + // We are in a potentially async context, so let's aggressively load all our children + // regardless of their alive state. this also gives children a clock so they can be checked + // for their correct alive state in the case LifetimeStart is set to a definite value. + internalChildren.ForEach(loadChild); + + // Let's also perform an update on our children's life to add any alive children. + UpdateChildrenLife(); + } + + private void loadChild(Drawable child) + { + child.Load(Clock, Dependencies); + child.Parent = this; + } + + protected override void LoadComplete() + { + schedulerAfterChildren?.SetCurrentThread(MainThread); + base.LoadComplete(); + } + + protected override void Dispose(bool isDisposing) + { + InternalChildren?.ForEach(c => c.Dispose()); + + OnAutoSize = null; + schedulerAfterChildren = null; + + base.Dispose(isDisposing); + } + + #endregion + + #region Children management + + /// + /// Invoked when a child has entered . + /// + internal event Action ChildBecameAlive; + + /// + /// Invoked when a child has left . + /// + internal event Action ChildDied; + + /// + /// Gets or sets the only child in . + /// + protected internal Drawable InternalChild + { + get + { + if (InternalChildren.Count != 1) + throw new InvalidOperationException($"{nameof(InternalChild)} is only available when there's only 1 in {nameof(InternalChildren)}!"); + + return InternalChildren[0]; + } + set + { + ClearInternal(); + AddInternal(value); + } + } + + protected class ChildComparer : IComparer + { + private readonly CompositeDrawable owner; + + public ChildComparer(CompositeDrawable owner) + { + this.owner = owner; + } + + public int Compare(Drawable x, Drawable y) => owner.Compare(x, y); + } + + /// + /// Compares two to determine their sorting. + /// + /// The first child to compare. + /// The second child to compare. + /// -1 if comes before , and 1 otherwise. + protected virtual int Compare(Drawable x, Drawable y) + { + if (x == null) throw new ArgumentNullException(nameof(x)); + if (y == null) throw new ArgumentNullException(nameof(y)); + + int i = y.Depth.CompareTo(x.Depth); + if (i != 0) return i; + return x.ChildID.CompareTo(y.ChildID); + } + + /// + /// Helper method comparing children by their depth first, and then by their reversed child ID. + /// + /// The first child to compare. + /// The second child to compare. + /// -1 if comes before , and 1 otherwise. + protected int CompareReverseChildID(Drawable x, Drawable y) + { + if (x == null) throw new ArgumentNullException(nameof(x)); + if (y == null) throw new ArgumentNullException(nameof(y)); + + int i = y.Depth.CompareTo(x.Depth); + if (i != 0) return i; + return y.ChildID.CompareTo(x.ChildID); + } + + private readonly SortedList internalChildren; + /// + /// This list of children. Assigning to this property will dispose all existing children of this . + /// + protected internal IReadOnlyList InternalChildren + { + get { return internalChildren; } + set { InternalChildrenEnumerable = value; } + } + + /// + /// Replaces all internal children of this with the elements contained in the enumerable. + /// + protected internal IEnumerable InternalChildrenEnumerable + { + set + { + ClearInternal(); + AddRangeInternal(value); + } + } + + private readonly SortedList aliveInternalChildren; + protected internal IReadOnlyList AliveInternalChildren => aliveInternalChildren; + + /// + /// The index of a given child within . + /// + /// + /// If the child is found, its index. Otherwise, the negated index it would obtain + /// if it were added to . + /// + protected internal int IndexOfInternal(Drawable drawable) => internalChildren.IndexOf(drawable); + + /// + /// Checks whether a given child is contained within . + /// + protected internal bool ContainsInternal(Drawable drawable) => IndexOfInternal(drawable) >= 0; + + /// + /// Removes a given child from this . + /// + /// The to be removed. + /// False if was not a child of this and true otherwise. + protected internal virtual bool RemoveInternal(Drawable drawable) + { + if (drawable == null) + throw new ArgumentNullException(nameof(drawable)); + + int index = IndexOfInternal(drawable); + if (index < 0) + return false; + + internalChildren.RemoveAt(index); + if (drawable.IsAlive) + { + aliveInternalChildren.Remove(drawable); + ChildDied?.Invoke(drawable); + } + + if (drawable.LoadState >= LoadState.Ready && drawable.Parent != this) + throw new InvalidOperationException($@"Removed a drawable ({drawable}) whose parent was not this ({this}), but {drawable.Parent}."); + + drawable.Parent = null; + drawable.IsAlive = false; + + if (AutoSizeAxes != Axes.None) + InvalidateFromChild(Invalidation.RequiredParentSizeToFit); + + return true; + } + + /// + /// Clear all of . + /// + /// + /// Whether removed children should also get disposed. + /// Disposal will be recursive. + /// + protected internal virtual void ClearInternal(bool disposeChildren = true) + { + if (internalChildren.Count == 0) return; + + foreach (Drawable t in internalChildren) + { + if (t.IsAlive) + ChildDied?.Invoke(t); + + t.IsAlive = false; + + if (disposeChildren) + { + //cascade disposal + (t as CompositeDrawable)?.ClearInternal(); + t.Dispose(); + } + else + t.Parent = null; + + Trace.Assert(t.Parent == null); + } + + internalChildren.Clear(); + aliveInternalChildren.Clear(); + + if (AutoSizeAxes != Axes.None) + InvalidateFromChild(Invalidation.RequiredParentSizeToFit); + } + + /// + /// Used to assign a monotonically increasing ID to children as they are added. This member is + /// incremented whenever a child is added. + /// + private ulong currentChildID; + + /// + /// Adds a child to . + /// + protected internal virtual void AddInternal(Drawable drawable) + { + if (drawable == null) + throw new ArgumentNullException(nameof(drawable), $"null {nameof(Drawable)}s may not be added to {nameof(CompositeDrawable)}."); + + if (drawable == this) + throw new InvalidOperationException($"{nameof(CompositeDrawable)} may not be added to itself."); + + // If the drawable's ChildId is not zero, then it was added to another parent even if it wasn't loaded + if (drawable.ChildID != 0) + throw new InvalidOperationException("May not add a drawable to multiple containers."); + + drawable.ChildID = ++currentChildID; + drawable.RemoveCompletedTransforms = RemoveCompletedTransforms; + + if (drawable.LoadState >= LoadState.Ready) + drawable.Parent = this; + // If we're already loaded, we can eagerly allow children to be loaded + else if (LoadState >= LoadState.Loading) + loadChild(drawable); + + internalChildren.Add(drawable); + checkChildLife(drawable); + + if (AutoSizeAxes != Axes.None) + InvalidateFromChild(Invalidation.RequiredParentSizeToFit); + } + + /// + /// Adds a range of children to . This is equivalent to calling + /// on each element of the range in order. + /// + protected internal void AddRangeInternal(IEnumerable range) + { + foreach (Drawable d in range) + AddInternal(d); + } + + /// + /// Changes the depth of an internal child. This affects ordering of . + /// + /// The child whose depth is to be changed. + /// The new depth value to be set. + protected internal void ChangeInternalChildDepth(Drawable child, float newDepth) + { + if (child.Depth == newDepth) return; + + var index = IndexOfInternal(child); + if (index < 0) + throw new InvalidOperationException($"Can not change depth of drawable which is not contained within this {nameof(CompositeDrawable)}."); + + internalChildren.RemoveAt(index); + var aliveIndex = aliveInternalChildren.IndexOf(child); + if (aliveIndex >= 0) // remove if found + aliveInternalChildren.RemoveAt(aliveIndex); + + var chId = child.ChildID; + child.ChildID = 0; // ensure Depth-change does not throw an exception + child.Depth = newDepth; + child.ChildID = chId; + + internalChildren.Add(child); + if (aliveIndex >= 0) // re-add if it used to be in aliveInternalChildren + aliveInternalChildren.Add(child); + } + + #endregion + + #region Updating (per-frame periodic) + + private Scheduler schedulerAfterChildren; + + protected Scheduler SchedulerAfterChildren => schedulerAfterChildren ?? (schedulerAfterChildren = new Scheduler(MainThread, Clock)); + + /// + /// Updates the life status of according to their + /// property. + /// + /// True iff the life status of at least one child changed. + protected virtual bool UpdateChildrenLife() + { + bool anyAliveChanged = false; + + // checkChildLife may remove a child from internalChildren. In order to not skip children, + // we keep track of the original amount children to apply an offset to the iterator + int originalCount = internalChildren.Count; + for (int i = 0; i < internalChildren.Count; i++) + anyAliveChanged |= checkChildLife(internalChildren[i + internalChildren.Count - originalCount]); + + if (anyAliveChanged) + childrenSizeDependencies.Invalidate(); + + return anyAliveChanged; + } + + /// + /// Checks whether the alive state of a child has changed processes it. This will add or remove + /// the child from depending on its alive state. + /// + /// The child to check. + /// Whether the child's alive state has changed. + private bool checkChildLife(Drawable child) + { + Debug.Assert(internalChildren.Contains(child), "Can only check and react to the life of our own children."); + + // Can not have alive children if we are not loaded. + if (LoadState < LoadState.Ready) + return false; + + bool changed = false; + + if (child.ShouldBeAlive) + { + if (!child.IsAlive) + { + loadChild(child); + + if (child.LoadState >= LoadState.Ready) + { + aliveInternalChildren.Add(child); + ChildBecameAlive?.Invoke(child); + child.IsAlive = true; + changed = true; + } + } + } + else + { + if (child.IsAlive) + { + aliveInternalChildren.Remove(child); + ChildDied?.Invoke(child); + child.IsAlive = false; + changed = true; + } + + if (child.RemoveWhenNotAlive) + { + RemoveInternal(child); + + if (child.DisposeOnDeathRemoval) + child.Dispose(); + } + } + + return changed; + } + + internal override void UpdateClock(IFrameBasedClock clock) + { + if (Clock == clock) + return; + + base.UpdateClock(clock); + foreach (Drawable child in internalChildren) + child.UpdateClock(Clock); + + schedulerAfterChildren?.UpdateClock(Clock); + } + + /// + /// Specifies whether this requires an update of its children. + /// If the return value is false, then children are not updated and + /// is not called. + /// + protected virtual bool RequiresChildrenUpdate => !IsMaskedAway || !childrenSizeDependencies.IsValid; + + public override bool UpdateSubTree() + { + if (!base.UpdateSubTree()) return false; + + // We update our children's life even if we are invisible. + // Note, that this does not propagate down and may need + // generalization in the future. + UpdateChildrenLife(); + + // If we are not present then there is never a reason to check + // for children, as they should never affect our present status. + if (!IsPresent || !RequiresChildrenUpdate) return false; + + UpdateAfterChildrenLife(); + + // We iterate by index to gain performance + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < aliveInternalChildren.Count; ++i) + { + Drawable c = aliveInternalChildren[i]; + Debug.Assert(c.LoadState >= LoadState.Ready); + c.UpdateSubTree(); + } + + if (schedulerAfterChildren != null) + { + int amountScheduledTasks = schedulerAfterChildren.Update(); + FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks); + } + + UpdateAfterChildren(); + + updateChildrenSizeDependencies(); + UpdateAfterAutoSize(); + return true; + } + + /// + /// Updates all masking calculations for this and its . + /// This occurs post- to ensure that all updates have taken place. + /// + /// The parent that triggered this update on this . + /// The that defines the masking bounds. + /// Whether masking calculations have taken place. + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + { + if (!base.UpdateSubTreeMasking(source, maskingBounds)) + return false; + + if (IsMaskedAway) + return true; + + if (aliveInternalChildren.Count == 0) + return true; + + if (RequiresChildrenUpdate) + { + var childMaskingBounds = ComputeChildMaskingBounds(maskingBounds); + + + // We iterate by index to gain performance + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < aliveInternalChildren.Count; i++) + aliveInternalChildren[i].UpdateSubTreeMasking(this, childMaskingBounds); + } + + return true; + } + + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) + { + if (!CanBeFlattened) + return base.ComputeIsMaskedAway(maskingBounds); + + // The masking check is overly expensive (requires creation of ScreenSpaceDrawQuad) + // when only few children exist. + return aliveInternalChildren.Count >= amount_children_required_for_masking_check && base.ComputeIsMaskedAway(maskingBounds); + } + + /// + /// Computes the to be used as the masking bounds for all . + /// + /// The that defines the masking bounds for this . + /// The to be used as the masking bounds for . + protected virtual RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => Masking ? RectangleF.Intersect(maskingBounds, ScreenSpaceDrawQuad.AABBFloat) : maskingBounds; + + /// + /// Invoked after and state checks have taken place, + /// but before is invoked for all . + /// This occurs after has been invoked on this + /// + protected virtual void UpdateAfterChildrenLife() + { + } + + /// + /// An opportunity to update state once-per-frame after has been called + /// for all . + /// This is invoked prior to any autosize calculations of this . + /// + protected virtual void UpdateAfterChildren() + { + } + + /// + /// Invoked after all autosize calculations have taken place. + /// + protected virtual void UpdateAfterAutoSize() + { + } + + #endregion + + #region Invalidation + + /// + /// Informs this that a child has been invalidated. + /// + /// The type of invalidation applied to the child. + public virtual void InvalidateFromChild(Invalidation invalidation) + { + //Colour captures potential changes in IsPresent. If this ever becomes a bottleneck, + //Invalidation could be further separated into presence changes. + if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) + childrenSizeDependencies.Invalidate(); + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if (!base.Invalidate(invalidation, source, shallPropagate)) + return false; + + if (!shallPropagate) return true; + + // This way of looping turns out to be slightly faster than a foreach + // or directly indexing a SortedList. This part of the code is often + // hot, so an optimization like this makes sense here. + SortedList current = internalChildren; + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < current.Count; ++i) + { + Drawable c = current[i]; + Debug.Assert(c != source); + + Invalidation childInvalidation = invalidation; + if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) + childInvalidation |= Invalidation.DrawInfo; + + // Other geometry things like rotation, shearing, etc don't affect child properties. + childInvalidation &= ~Invalidation.MiscGeometry; + + // Relative positioning can however affect child geometry + // ReSharper disable once PossibleNullReferenceException + if (c.RelativePositionAxes != Axes.None && (invalidation & Invalidation.DrawSize) > 0) + childInvalidation |= Invalidation.MiscGeometry; + + // No draw size changes if relative size axes does not propagate it downward. + if (c.RelativeSizeAxes == Axes.None) + childInvalidation &= ~Invalidation.DrawSize; + + c.Invalidate(childInvalidation, this); + } + + return true; + } + + #endregion + + #region DrawNode + + private readonly CompositeDrawNodeSharedData compositeDrawNodeSharedData = new CompositeDrawNodeSharedData(); + private Shader shader; + + protected override DrawNode CreateDrawNode() => new CompositeDrawNode(); + + protected override void ApplyDrawNode(DrawNode node) + { + CompositeDrawNode n = (CompositeDrawNode)node; + + if (!Masking && (BorderThickness != 0.0f || EdgeEffect.Type != EdgeEffectType.None)) + throw new InvalidOperationException("Can not have border effects/edge effects if masking is disabled."); + + Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); + + n.MaskingInfo = !Masking + ? (MaskingInfo?)null + : new MaskingInfo + { + ScreenSpaceAABB = ScreenSpaceDrawQuad.AABB, + MaskingRect = DrawRectangle, + ToMaskingSpace = DrawInfo.MatrixInverse, + CornerRadius = CornerRadius, + BorderThickness = BorderThickness, + BorderColour = BorderColour, + // We are setting the linear blend range to the approximate size of a _pixel_ here. + // This results in the optimal trade-off between crispness and smoothness of the + // edges of the masked region according to sampling theory. + BlendRange = MaskingSmoothness * (scale.X + scale.Y) / 2, + AlphaExponent = 1, + }; + + n.EdgeEffect = EdgeEffect; + + n.ScreenSpaceMaskingQuad = null; + n.Shared = compositeDrawNodeSharedData; + + n.Shader = shader; + + base.ApplyDrawNode(node); + } + + protected virtual bool CanBeFlattened => !Masking; + + private const int amount_children_required_for_masking_check = 2; + + /// + /// This function adds all children's s to a target List, flattening the children of certain types + /// of subtrees for optimization purposes. + /// + /// The index of the currently in-use DrawNode tree. + /// The running index into the target List. + /// The whose children's DrawNodes to add. + /// The target list to fill with DrawNodes. + private static void addFromComposite(int treeIndex, ref int j, CompositeDrawable parentComposite, List target) + { + SortedList current = parentComposite.aliveInternalChildren; + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < current.Count; ++i) + { + Drawable drawable = current[i]; + + // If we are proxied somewhere, then we want to be drawn at the proxy's location + // in the scene graph, rather than at our own location, thus no draw nodes for us. + if (drawable.HasProxy) + continue; + + // Take drawable.Original until drawable.Original == drawable + while (drawable != (drawable = drawable.Original)) + { + } + + if (!drawable.IsPresent) + continue; + + CompositeDrawable composite = drawable as CompositeDrawable; + if (composite?.CanBeFlattened == true) + { + if (!composite.IsMaskedAway) + addFromComposite(treeIndex, ref j, composite, target); + + continue; + } + + if (drawable.IsMaskedAway) + continue; + + DrawNode next = drawable.GenerateDrawNodeSubtree(treeIndex); + if (next == null) + continue; + + if (j < target.Count) + target[j] = next; + else + target.Add(next); + + j++; + } + } + + internal virtual bool AddChildDrawNodes => true; + + internal override DrawNode GenerateDrawNodeSubtree(int treeIndex) + { + // No need for a draw node at all if there are no children and we are not glowing. + if (aliveInternalChildren.Count == 0 && CanBeFlattened) + return null; + + CompositeDrawNode cNode = base.GenerateDrawNodeSubtree(treeIndex) as CompositeDrawNode; + if (cNode == null) + return null; + + if (cNode.Children == null) + cNode.Children = new List(aliveInternalChildren.Count); + + if (AddChildDrawNodes) + { + List target = cNode.Children; + + int j = 0; + addFromComposite(treeIndex, ref j, this, target); + + if (j < target.Count) + target.RemoveRange(j, target.Count - j); + } + + return cNode; + } + + #endregion + + #region Transforms + + /// + /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. + /// + /// This value is passed down to children. + /// + /// + public override bool RemoveCompletedTransforms + { + get { return base.RemoveCompletedTransforms; } + internal set + { + if (base.RemoveCompletedTransforms == value) + return; + base.RemoveCompletedTransforms = value; + + foreach (var c in internalChildren) + c.RemoveCompletedTransforms = RemoveCompletedTransforms; + } + } + + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + base.ApplyTransformsAt(time, propagateChildren); + + if (!propagateChildren) + return; + + foreach (var c in internalChildren) + c.ApplyTransformsAt(time, true); + } + + public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) + { + base.ClearTransformsAfter(time, propagateChildren, targetMember); + + if (!propagateChildren) + return; + + foreach (var c in internalChildren) + c.ClearTransformsAfter(time, true, targetMember); + } + + internal override void AddDelay(double duration, bool propagateChildren = false) + { + if (duration == 0) + return; + + base.AddDelay(duration, propagateChildren); + + if (propagateChildren) + foreach (var c in internalChildren) + c.AddDelay(duration, true); + } + + protected ScheduledDelegate ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay); + + public override InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false) + { + var baseDisposalAction = base.BeginAbsoluteSequence(newTransformStartTime, recursive); + if (!recursive) + return baseDisposalAction; + + List disposalActions = new List(internalChildren.Count + 1) { baseDisposalAction }; + foreach (var c in internalChildren) + disposalActions.Add(c.BeginAbsoluteSequence(newTransformStartTime, true)); + + return new InvokeOnDisposal(() => + { + foreach (var a in disposalActions) + a.Dispose(); + }); + } + + public override void FinishTransforms(bool propagateChildren = false, string targetMember = null) + { + base.FinishTransforms(propagateChildren, targetMember); + + if (propagateChildren) + foreach (var c in internalChildren) + c.FinishTransforms(true, targetMember); + } + + /// + /// Helper function for creating and adding a that fades the current . + /// + protected TransformSequence FadeEdgeEffectTo(float newAlpha, double duration = 0, Easing easing = Easing.None) + { + Color4 targetColour = EdgeEffect.Colour; + targetColour.A = newAlpha; + return FadeEdgeEffectTo(targetColour, duration, easing); + } + + /// + /// Helper function for creating and adding a that fades the current . + /// + protected TransformSequence FadeEdgeEffectTo(Color4 newColour, double duration = 0, Easing easing = Easing.None) + { + var effect = EdgeEffect; + effect.Colour = newColour; + return TweenEdgeEffectTo(effect, duration, easing); + } + + /// + /// Helper function for creating and adding a that tweens the current . + /// + protected TransformSequence TweenEdgeEffectTo(EdgeEffectParameters newParams, double duration = 0, Easing easing = Easing.None) => + this.TransformTo(nameof(EdgeEffect), newParams, duration, easing); + + #endregion + + #region Interaction / Input + + // Required to pass through input to children by default. + // TODO: Evaluate effects of this on performance and address. + public override bool HandleKeyboardInput => true; + public override bool HandleMouseInput => true; + + public override bool Contains(Vector2 screenSpacePos) + { + float cRadius = CornerRadius; + + // Select a cheaper contains method when we don't need rounded edges. + if (cRadius == 0.0f) + return base.Contains(screenSpacePos); + return DrawRectangle.Shrink(cRadius).DistanceSquared(ToLocalSpace(screenSpacePos)) <= cRadius * cRadius; + } + + internal override bool BuildKeyboardInputQueue(List queue) + { + if (!base.BuildKeyboardInputQueue(queue)) + return false; + + // We iterate by index to gain performance + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < aliveInternalChildren.Count; ++i) + aliveInternalChildren[i].BuildKeyboardInputQueue(queue); + + return true; + } + + internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) + { + if (!base.BuildMouseInputQueue(screenSpaceMousePos, queue) && (!CanReceiveMouseInput || Masking)) + return false; + + // We iterate by index to gain performance + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < aliveInternalChildren.Count; ++i) + aliveInternalChildren[i].BuildMouseInputQueue(screenSpaceMousePos, queue); + + return true; + } + + #endregion + + #region Masking and related effects (e.g. round corners) + + private bool masking; + + /// + /// If enabled, only the portion of children that falls within this 's + /// shape is drawn to the screen. + /// + public bool Masking + { + get { return masking; } + protected set + { + if (masking == value) + return; + + masking = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float maskingSmoothness = 1; + + /// + /// Determines over how many pixels the alpha component smoothly fades out. + /// Only has an effect when is true. + /// + public float MaskingSmoothness + { + get { return maskingSmoothness; } + protected set + { + //must be above zero to avoid a div-by-zero in the shader logic. + value = Math.Max(0.01f, value); + + if (maskingSmoothness == value) + return; + + maskingSmoothness = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float cornerRadius; + + /// + /// Determines how large of a radius is masked away around the corners. + /// Only has an effect when is true. + /// + public float CornerRadius + { + get { return cornerRadius; } + protected set + { + if (cornerRadius == value) + return; + + cornerRadius = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float borderThickness; + + /// + /// Determines how thick of a border to draw around the inside of the masked region. + /// Only has an effect when is true. + /// The border only is drawn on top of children using a sprite shader. + /// + /// + /// Drawing borders is optimized heavily into our sprite shaders. As a consequence + /// borders are only drawn correctly on top of quad-shaped children using our sprite + /// shaders. + /// + public float BorderThickness + { + get { return borderThickness; } + protected set + { + if (borderThickness == value) + return; + + borderThickness = value; + Invalidate(Invalidation.DrawNode); + } + } + + private SRGBColour borderColour = Color4.Black; + + /// + /// Determines the color of the border controlled by . + /// Only has an effect when is true. + /// + public SRGBColour BorderColour + { + get { return borderColour; } + protected set + { + if (borderColour.Equals(value)) + return; + + borderColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private EdgeEffectParameters edgeEffect; + + /// + /// Determines an edge effect of this . + /// Edge effects are e.g. glow or a shadow. + /// Only has an effect when is true. + /// + public EdgeEffectParameters EdgeEffect + { + get { return edgeEffect; } + protected set + { + if (edgeEffect.Equals(value)) + return; + + edgeEffect = value; + Invalidate(Invalidation.DrawNode); + } + } + + #endregion + + #region Sizing + + public override RectangleF BoundingBox + { + get + { + float cRadius = CornerRadius; + if (cRadius == 0.0f) + return base.BoundingBox; + + RectangleF drawRect = LayoutRectangle.Shrink(cRadius); + + // Inflate bounding box in parent space by the half-size of the bounding box of the + // ellipse obtained by transforming the unit circle into parent space. + Vector2 offset = ToParentSpace(Vector2.Zero); + Vector2 u = ToParentSpace(new Vector2(cRadius, 0)) - offset; + Vector2 v = ToParentSpace(new Vector2(0, cRadius)) - offset; + Vector2 inflation = new Vector2((float)Math.Sqrt(u.X * u.X + v.X * v.X), (float)Math.Sqrt(u.Y * u.Y + v.Y * v.Y)); + + RectangleF result = ToParentSpace(drawRect).AABBFloat.Inflate(inflation); + // The above algorithm will return incorrect results if the rounded corners are not fully visible. + // To limit bad behavior we at least enforce here, that the bounding box with rounded corners + // is never larger than the bounding box without. + if (DrawSize.X < CornerRadius * 2 || DrawSize.Y < CornerRadius * 2) + result.Intersect(base.BoundingBox); + + return result; + } + } + + private MarginPadding padding; + + /// + /// Shrinks the space children may occupy within this + /// by the specified amount on each side. + /// + public MarginPadding Padding + { + get { return padding; } + protected set + { + if (padding.Equals(value)) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}."); + + padding = value; + + foreach (Drawable c in internalChildren) + c.Invalidate(c.InvalidationFromParentSize); + } + } + + /// + /// The size of the coordinate space revealed to . + /// Captures the effect of e.g. . + /// + public Vector2 ChildSize => DrawSize - new Vector2(Padding.TotalHorizontal, Padding.TotalVertical); + + /// + /// Positional offset applied to . + /// Captures the effect of e.g. . + /// + public Vector2 ChildOffset => new Vector2(Padding.Left, Padding.Top); + + private Vector2 relativeChildSize = Vector2.One; + + /// + /// The size of the relative position/size coordinate space of children of this . + /// Children positioned at this size will appear as if they were positioned at = in this . + /// + public Vector2 RelativeChildSize + { + get { return relativeChildSize; } + protected set + { + if (relativeChildSize == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be finite, but is {value}."); + if (value.X == 0 || value.Y == 0) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be non-zero, but is {value}."); + + relativeChildSize = value; + + foreach (Drawable c in internalChildren) + c.Invalidate(c.InvalidationFromParentSize); + } + } + + private Vector2 relativeChildOffset = Vector2.Zero; + + /// + /// The offset of the relative position/size coordinate space of children of this . + /// Children positioned at this offset will appear as if they were positioned at = in this . + /// + public Vector2 RelativeChildOffset + { + get { return relativeChildOffset; } + protected set + { + if (relativeChildOffset == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildOffset)} must be finite, but is {value}."); + + relativeChildOffset = value; + + foreach (Drawable c in internalChildren) + c.Invalidate(c.InvalidationFromParentSize & ~Invalidation.DrawSize); + } + } + + /// + /// Conversion factor from relative to absolute coordinates in our space. + /// + public Vector2 RelativeToAbsoluteFactor => Vector2.Divide(ChildSize, RelativeChildSize); + + /// + /// Tweens the of this . + /// + /// The coordinate space to tween to. + /// The tween duration. + /// The tween easing. + protected TransformSequence TransformRelativeChildSizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) => + this.TransformTo(nameof(RelativeChildSize), newSize, duration, easing); + + /// + /// Tweens the of this . + /// + /// The coordinate space to tween to. + /// The tween duration. + /// The tween easing. + protected TransformSequence TransformRelativeChildOffsetTo(Vector2 newOffset, double duration = 0, Easing easing = Easing.None) => + this.TransformTo(nameof(RelativeChildOffset), newOffset, duration, easing); + + public override Axes RelativeSizeAxes + { + get { return base.RelativeSizeAxes; } + set + { + if ((AutoSizeAxes & value) != 0) + throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); + + base.RelativeSizeAxes = value; + } + } + + private Axes autoSizeAxes; + + /// + /// Controls which are automatically sized w.r.t. . + /// Children's are ignored for automatic sizing. + /// Most notably, and of children + /// do not affect automatic sizing to avoid circular size dependencies. + /// It is not allowed to manually set (or / ) + /// on any which are automatically sized. + /// + public virtual Axes AutoSizeAxes + { + get { return autoSizeAxes; } + protected set + { + if (value == autoSizeAxes) + return; + + if ((RelativeSizeAxes & value) != 0) + throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); + + autoSizeAxes = value; + childrenSizeDependencies.Invalidate(); + OnSizingChanged(); + } + } + + /// + /// The duration which automatic sizing should take. If zero, then it is instantaneous. + /// Otherwise, this is equivalent to applying an automatic size via a resize transform. + /// + public float AutoSizeDuration { get; protected set; } + + /// + /// The type of easing which should be used for smooth automatic sizing when + /// is non-zero. + /// + public Easing AutoSizeEasing { get; protected set; } + + /// + /// THIS EVENT PURELY EXISTS FOR THE SCENE GRAPH VISUALIZER. DO NOT USE. + /// This event will fire after our is updated from autosizing. + /// + internal event Action OnAutoSize; + + private Cached childrenSizeDependencies = new Cached(); + + public override float Width + { + get + { + if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && (AutoSizeAxes & Axes.X) > 0) + updateChildrenSizeDependencies(); + return base.Width; + } + + set + { + if ((AutoSizeAxes & Axes.X) != 0) + throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); + base.Width = value; + } + } + + public override float Height + { + get + { + if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && (AutoSizeAxes & Axes.Y) > 0) + updateChildrenSizeDependencies(); + return base.Height; + } + + set + { + if ((AutoSizeAxes & Axes.Y) != 0) + throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); + base.Height = value; + } + } + + private bool isComputingChildrenSizeDependencies; + + public override Vector2 Size + { + get + { + if (!StaticCached.BypassCache && !isComputingChildrenSizeDependencies && AutoSizeAxes != Axes.None) + updateChildrenSizeDependencies(); + return base.Size; + } + + set + { + if ((AutoSizeAxes & Axes.Both) != 0) + throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent."); + base.Size = value; + } + } + + private Vector2 computeAutoSize() + { + MarginPadding originalPadding = Padding; + MarginPadding originalMargin = Margin; + + try + { + Padding = new MarginPadding(); + Margin = new MarginPadding(); + + if (AutoSizeAxes == Axes.None) return DrawSize; + + Vector2 maxBoundSize = Vector2.Zero; + + // Find the maximum width/height of children + foreach (Drawable c in AliveInternalChildren) + { + if (!c.IsPresent) + continue; + + Vector2 cBound = c.RequiredParentSizeToFit; + + if ((c.BypassAutoSizeAxes & Axes.X) == 0) + maxBoundSize.X = Math.Max(maxBoundSize.X, cBound.X); + + if ((c.BypassAutoSizeAxes & Axes.Y) == 0) + maxBoundSize.Y = Math.Max(maxBoundSize.Y, cBound.Y); + } + + if ((AutoSizeAxes & Axes.X) == 0) + maxBoundSize.X = DrawSize.X; + if ((AutoSizeAxes & Axes.Y) == 0) + maxBoundSize.Y = DrawSize.Y; + + return new Vector2(maxBoundSize.X, maxBoundSize.Y); + } + finally + { + Padding = originalPadding; + Margin = originalMargin; + } + } + + private void updateAutoSize() + { + if (AutoSizeAxes == Axes.None) + return; + + Vector2 b = computeAutoSize() + Padding.Total; + + autoSizeResizeTo(new Vector2( + (AutoSizeAxes & Axes.X) > 0 ? b.X : base.Width, + (AutoSizeAxes & Axes.Y) > 0 ? b.Y : base.Height + ), AutoSizeDuration, AutoSizeEasing); + + //note that this is called before autoSize becomes valid. may be something to consider down the line. + //might work better to add an OnRefresh event in Cached<> and invoke there. + OnAutoSize?.Invoke(); + } + + private void updateChildrenSizeDependencies() + { + isComputingChildrenSizeDependencies = true; + + try + { + if (!childrenSizeDependencies.IsValid) + { + updateAutoSize(); + childrenSizeDependencies.Validate(); + } + } + finally + { + isComputingChildrenSizeDependencies = false; + } + } + + private void autoSizeResizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) => + this.TransformTo(this.PopulateTransform(new AutoSizeTransform { Rewindable = false }, newSize, duration, easing)); + + /// + /// A helper property for to change the size of s with . + /// + private Vector2 baseSize + { + get { return new Vector2(base.Width, base.Height); } + + set + { + base.Width = value.X; + base.Height = value.Y; + } + } + + private class AutoSizeTransform : TransformCustom + { + public AutoSizeTransform() + : base(nameof(baseSize)) + { + } + } + + #endregion + } +} diff --git a/osu.Framework/Graphics/Containers/Container.cs b/osu.Framework/Graphics/Containers/Container.cs index cf55dfd0e..9c3155ab7 100644 --- a/osu.Framework/Graphics/Containers/Container.cs +++ b/osu.Framework/Graphics/Containers/Container.cs @@ -1,412 +1,412 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Lists; -using System.Collections.Generic; -using System; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Graphics.Colour; -using OpenTK; -using System.Collections; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A drawable which can have children added to it. Transformations applied to - /// a container are also applied to its children. - /// Additionally, containers support various effects, such as masking, edge effect, - /// padding, and automatic sizing depending on their children. - /// If all children are of a specific non- type, use the - /// generic version . - /// - public class Container : Container - { - } - - /// - /// A drawable which can have children added to it. Transformations applied to - /// a container are also applied to its children. - /// Additionally, containers support various effects, such as masking, edge effect, - /// padding, and automatic sizing depending on their children. - /// - public class Container : CompositeDrawable, IContainerEnumerable, IContainerCollection, ICollection, IReadOnlyList - where T : Drawable - { - /// - /// Contructs a that stores children. - /// - public Container() - { - if (typeof(T) == typeof(Drawable)) - internalChildrenAsT = (IReadOnlyList)InternalChildren; - else - internalChildrenAsT = new LazyList(InternalChildren, c => (T)c); - } - - /// - /// The content of this container. and all methods that mutate - /// (e.g. and ) are - /// forwarded to the content. By default a container's content is itself, in which case - /// refers to . - /// This property is useful for containers that require internal children that should - /// not be exposed to the outside world, e.g. . - /// - protected virtual Container Content => this; - - /// - /// The publicly accessible list of children. Forwards to the children of . - /// If is this container, then returns . - /// Assigning to this property will dispose all existing children of this Container. - /// - public IReadOnlyList Children - { - get - { - if (Content != this) - return Content.Children; - - return internalChildrenAsT; - } - set - { - ChildrenEnumerable = value; - } - } - - /// - /// Accesses the -th child. - /// - /// The index of the child to access. - /// The -th child. - public T this[int index] => Children[index]; - - /// - /// The amount of elements in . - /// - public int Count => Children.Count; - - /// - /// Whether this can have elements added and removed. Always false. - /// - public bool IsReadOnly => false; - - /// - /// Copies the elements of the to an Array, starting at a particular Array index. - /// - /// The Array into which all children should be copied. - /// The starting index in the Array. - public void CopyTo(T[] array, int arrayIndex) - { - foreach (var c in Children) - array[arrayIndex++] = c; - } - - /// - /// Gets the enumerator over . - /// - /// The enumerator over . - public IEnumerator GetEnumerator() => Children.GetEnumerator(); - - /// - /// Gets the enumerator over . - /// - /// The enumerator over . - IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator(); - - /// - /// Sets all children of this container to the elements contained in the enumerable. - /// - public IEnumerable ChildrenEnumerable - { - set - { - Clear(); - AddRange(value); - } - } - - /// - /// Gets or sets the only child of this container. - /// - public T Child - { - get - { - if (Children.Count != 1) - throw new InvalidOperationException($"{nameof(Child)} is only available when there's only 1 in {nameof(Children)}!"); - - return Children[0]; - } - set - { - Clear(); - Add(value); - } - } - - private readonly IReadOnlyList internalChildrenAsT; - - /// - /// The index of a given child within . - /// - /// - /// If the child is found, its index. Otherwise, the negated index it would obtain - /// if it were added to . - /// - public int IndexOf(T drawable) - { - if (Content != this) - return Content.IndexOf(drawable); - - return IndexOfInternal(drawable); - } - - /// - /// Checks whether a given child is contained within . - /// - public bool Contains(T drawable) => IndexOf(drawable) >= 0; - - /// - /// Adds a child to this container. This amount to adding a child to 's - /// , recursing until == this. - /// - public virtual void Add(T drawable) - { - if (drawable == Content) - throw new InvalidOperationException("Content may not be added to itself."); - - if (Content == this) - AddInternal(drawable); - else - Content.Add(drawable); - } - - /// - /// Adds a range of children. This is equivalent to calling on - /// each element of the range in order. - /// - public void AddRange(IEnumerable range) - { - foreach (T d in range) - Add(d); - } - - protected internal override void AddInternal(Drawable drawable) - { - if (Content == this && !(drawable is T)) - throw new InvalidOperationException($"Only {typeof(T).ReadableName()} type drawables may be added to a container of type {GetType().ReadableName()} which does not redirect {nameof(Content)}."); - - base.AddInternal(drawable); - } - - /// - /// Removes a given child from this container. - /// - public virtual bool Remove(T drawable) => Content != this ? Content.Remove(drawable) : RemoveInternal(drawable); - - /// - /// Removes all children which match the given predicate. - /// This is equivalent to calling for each child that - /// matches the given predicate. - /// - /// The amount of removed children. - public int RemoveAll(Predicate pred) - { - if (Content != this) - return Content.RemoveAll(pred); - - int removedCount = 0; - - for (int i = 0; i < InternalChildren.Count; i++) - { - var tChild = (T)InternalChildren[i]; - - if (pred.Invoke(tChild)) - { - RemoveInternal(tChild); - removedCount++; - i--; - } - } - - return removedCount; - } - - /// - /// Removes a range of children. This is equivalent to calling on - /// each element of the range in order. - /// - public void RemoveRange(IEnumerable range) - { - if (range == null) - return; - - foreach (T p in range) - Remove(p); - } - - /// - /// Removes all children. - /// - public void Clear() => Clear(true); - - /// - /// Removes all children. - /// - /// - /// Whether removed children should also get disposed. - /// Disposal will be recursive. - /// - public virtual void Clear(bool disposeChildren) - { - if (Content != null && Content != this) - Content.Clear(disposeChildren); - else - ClearInternal(disposeChildren); - } - - /// - /// Changes the depth of a child. This affects ordering of children within this container. - /// - /// The child whose depth is to be changed. - /// The new depth value to be set. - public void ChangeChildDepth(T child, float newDepth) - { - if (Content != this) - Content.ChangeChildDepth(child, newDepth); - else - ChangeInternalChildDepth(child, newDepth); - } - - /// - /// If enabled, only the portion of children that falls within this 's - /// shape is drawn to the screen. - /// - public new bool Masking - { - get { return base.Masking; } - set { base.Masking = value; } - } - - /// - /// Determines over how many pixels the alpha component smoothly fades out. - /// Only has an effect when is true. - /// - public new float MaskingSmoothness - { - get { return base.MaskingSmoothness; } - set { base.MaskingSmoothness = value; } - } - - /// - /// Determines how large of a radius is masked away around the corners. - /// Only has an effect when is true. - /// - public new float CornerRadius - { - get { return base.CornerRadius; } - set { base.CornerRadius = value; } - } - - /// - /// Determines how thick of a border to draw around the inside of the masked region. - /// Only has an effect when is true. - /// The border only is drawn on top of children using a sprite shader. - /// - /// - /// Drawing borders is optimized heavily into our sprite shaders. As a consequence - /// borders are only drawn correctly on top of quad-shaped children using our sprite - /// shaders. - /// - public new float BorderThickness - { - get { return base.BorderThickness; } - set { base.BorderThickness = value; } - } - - /// - /// Determines the color of the border controlled by . - /// Only has an effect when is true. - /// - public new SRGBColour BorderColour - { - get { return base.BorderColour; } - set { base.BorderColour = value; } - } - - /// - /// Determines an edge effect of this . - /// Edge effects are e.g. glow or a shadow. - /// Only has an effect when is true. - /// - public new EdgeEffectParameters EdgeEffect - { - get { return base.EdgeEffect; } - set { base.EdgeEffect = value; } - } - - /// - /// Shrinks the space children may occupy within this - /// by the specified amount on each side. - /// - public new MarginPadding Padding - { - get { return base.Padding; } - set { base.Padding = value; } - } - - /// - /// The size of the relative position/size coordinate space of children of this . - /// Children positioned at this size will appear as if they were positioned at = in this . - /// - public new Vector2 RelativeChildSize - { - get { return base.RelativeChildSize; } - set { base.RelativeChildSize = value; } - } - - /// - /// The offset of the relative position/size coordinate space of children of this . - /// Children positioned at this offset will appear as if they were positioned at = in this . - /// - public new Vector2 RelativeChildOffset - { - get { return base.RelativeChildOffset; } - set { base.RelativeChildOffset = value; } - } - - /// - /// Controls which are automatically sized w.r.t. . - /// Children's are ignored for automatic sizing. - /// Most notably, and of children - /// do not affect automatic sizing to avoid circular size dependencies. - /// It is not allowed to manually set (or / ) - /// on any which are automatically sized. - /// - public new Axes AutoSizeAxes - { - get { return base.AutoSizeAxes; } - set { base.AutoSizeAxes = value; } - } - - /// - /// The duration which automatic sizing should take. If zero, then it is instantaneous. - /// Otherwise, this is equivalent to applying an automatic size via a resize transform. - /// - public new float AutoSizeDuration - { - get { return base.AutoSizeDuration; } - set { base.AutoSizeDuration = value; } - } - - /// - /// The type of easing which should be used for smooth automatic sizing when - /// is non-zero. - /// - public new Easing AutoSizeEasing - { - get { return base.AutoSizeEasing; } - set { base.AutoSizeEasing = value; } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Lists; +using System.Collections.Generic; +using System; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics.Colour; +using OpenTK; +using System.Collections; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A drawable which can have children added to it. Transformations applied to + /// a container are also applied to its children. + /// Additionally, containers support various effects, such as masking, edge effect, + /// padding, and automatic sizing depending on their children. + /// If all children are of a specific non- type, use the + /// generic version . + /// + public class Container : Container + { + } + + /// + /// A drawable which can have children added to it. Transformations applied to + /// a container are also applied to its children. + /// Additionally, containers support various effects, such as masking, edge effect, + /// padding, and automatic sizing depending on their children. + /// + public class Container : CompositeDrawable, IContainerEnumerable, IContainerCollection, ICollection, IReadOnlyList + where T : Drawable + { + /// + /// Contructs a that stores children. + /// + public Container() + { + if (typeof(T) == typeof(Drawable)) + internalChildrenAsT = (IReadOnlyList)InternalChildren; + else + internalChildrenAsT = new LazyList(InternalChildren, c => (T)c); + } + + /// + /// The content of this container. and all methods that mutate + /// (e.g. and ) are + /// forwarded to the content. By default a container's content is itself, in which case + /// refers to . + /// This property is useful for containers that require internal children that should + /// not be exposed to the outside world, e.g. . + /// + protected virtual Container Content => this; + + /// + /// The publicly accessible list of children. Forwards to the children of . + /// If is this container, then returns . + /// Assigning to this property will dispose all existing children of this Container. + /// + public IReadOnlyList Children + { + get + { + if (Content != this) + return Content.Children; + + return internalChildrenAsT; + } + set + { + ChildrenEnumerable = value; + } + } + + /// + /// Accesses the -th child. + /// + /// The index of the child to access. + /// The -th child. + public T this[int index] => Children[index]; + + /// + /// The amount of elements in . + /// + public int Count => Children.Count; + + /// + /// Whether this can have elements added and removed. Always false. + /// + public bool IsReadOnly => false; + + /// + /// Copies the elements of the to an Array, starting at a particular Array index. + /// + /// The Array into which all children should be copied. + /// The starting index in the Array. + public void CopyTo(T[] array, int arrayIndex) + { + foreach (var c in Children) + array[arrayIndex++] = c; + } + + /// + /// Gets the enumerator over . + /// + /// The enumerator over . + public IEnumerator GetEnumerator() => Children.GetEnumerator(); + + /// + /// Gets the enumerator over . + /// + /// The enumerator over . + IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator(); + + /// + /// Sets all children of this container to the elements contained in the enumerable. + /// + public IEnumerable ChildrenEnumerable + { + set + { + Clear(); + AddRange(value); + } + } + + /// + /// Gets or sets the only child of this container. + /// + public T Child + { + get + { + if (Children.Count != 1) + throw new InvalidOperationException($"{nameof(Child)} is only available when there's only 1 in {nameof(Children)}!"); + + return Children[0]; + } + set + { + Clear(); + Add(value); + } + } + + private readonly IReadOnlyList internalChildrenAsT; + + /// + /// The index of a given child within . + /// + /// + /// If the child is found, its index. Otherwise, the negated index it would obtain + /// if it were added to . + /// + public int IndexOf(T drawable) + { + if (Content != this) + return Content.IndexOf(drawable); + + return IndexOfInternal(drawable); + } + + /// + /// Checks whether a given child is contained within . + /// + public bool Contains(T drawable) => IndexOf(drawable) >= 0; + + /// + /// Adds a child to this container. This amount to adding a child to 's + /// , recursing until == this. + /// + public virtual void Add(T drawable) + { + if (drawable == Content) + throw new InvalidOperationException("Content may not be added to itself."); + + if (Content == this) + AddInternal(drawable); + else + Content.Add(drawable); + } + + /// + /// Adds a range of children. This is equivalent to calling on + /// each element of the range in order. + /// + public void AddRange(IEnumerable range) + { + foreach (T d in range) + Add(d); + } + + protected internal override void AddInternal(Drawable drawable) + { + if (Content == this && !(drawable is T)) + throw new InvalidOperationException($"Only {typeof(T).ReadableName()} type drawables may be added to a container of type {GetType().ReadableName()} which does not redirect {nameof(Content)}."); + + base.AddInternal(drawable); + } + + /// + /// Removes a given child from this container. + /// + public virtual bool Remove(T drawable) => Content != this ? Content.Remove(drawable) : RemoveInternal(drawable); + + /// + /// Removes all children which match the given predicate. + /// This is equivalent to calling for each child that + /// matches the given predicate. + /// + /// The amount of removed children. + public int RemoveAll(Predicate pred) + { + if (Content != this) + return Content.RemoveAll(pred); + + int removedCount = 0; + + for (int i = 0; i < InternalChildren.Count; i++) + { + var tChild = (T)InternalChildren[i]; + + if (pred.Invoke(tChild)) + { + RemoveInternal(tChild); + removedCount++; + i--; + } + } + + return removedCount; + } + + /// + /// Removes a range of children. This is equivalent to calling on + /// each element of the range in order. + /// + public void RemoveRange(IEnumerable range) + { + if (range == null) + return; + + foreach (T p in range) + Remove(p); + } + + /// + /// Removes all children. + /// + public void Clear() => Clear(true); + + /// + /// Removes all children. + /// + /// + /// Whether removed children should also get disposed. + /// Disposal will be recursive. + /// + public virtual void Clear(bool disposeChildren) + { + if (Content != null && Content != this) + Content.Clear(disposeChildren); + else + ClearInternal(disposeChildren); + } + + /// + /// Changes the depth of a child. This affects ordering of children within this container. + /// + /// The child whose depth is to be changed. + /// The new depth value to be set. + public void ChangeChildDepth(T child, float newDepth) + { + if (Content != this) + Content.ChangeChildDepth(child, newDepth); + else + ChangeInternalChildDepth(child, newDepth); + } + + /// + /// If enabled, only the portion of children that falls within this 's + /// shape is drawn to the screen. + /// + public new bool Masking + { + get { return base.Masking; } + set { base.Masking = value; } + } + + /// + /// Determines over how many pixels the alpha component smoothly fades out. + /// Only has an effect when is true. + /// + public new float MaskingSmoothness + { + get { return base.MaskingSmoothness; } + set { base.MaskingSmoothness = value; } + } + + /// + /// Determines how large of a radius is masked away around the corners. + /// Only has an effect when is true. + /// + public new float CornerRadius + { + get { return base.CornerRadius; } + set { base.CornerRadius = value; } + } + + /// + /// Determines how thick of a border to draw around the inside of the masked region. + /// Only has an effect when is true. + /// The border only is drawn on top of children using a sprite shader. + /// + /// + /// Drawing borders is optimized heavily into our sprite shaders. As a consequence + /// borders are only drawn correctly on top of quad-shaped children using our sprite + /// shaders. + /// + public new float BorderThickness + { + get { return base.BorderThickness; } + set { base.BorderThickness = value; } + } + + /// + /// Determines the color of the border controlled by . + /// Only has an effect when is true. + /// + public new SRGBColour BorderColour + { + get { return base.BorderColour; } + set { base.BorderColour = value; } + } + + /// + /// Determines an edge effect of this . + /// Edge effects are e.g. glow or a shadow. + /// Only has an effect when is true. + /// + public new EdgeEffectParameters EdgeEffect + { + get { return base.EdgeEffect; } + set { base.EdgeEffect = value; } + } + + /// + /// Shrinks the space children may occupy within this + /// by the specified amount on each side. + /// + public new MarginPadding Padding + { + get { return base.Padding; } + set { base.Padding = value; } + } + + /// + /// The size of the relative position/size coordinate space of children of this . + /// Children positioned at this size will appear as if they were positioned at = in this . + /// + public new Vector2 RelativeChildSize + { + get { return base.RelativeChildSize; } + set { base.RelativeChildSize = value; } + } + + /// + /// The offset of the relative position/size coordinate space of children of this . + /// Children positioned at this offset will appear as if they were positioned at = in this . + /// + public new Vector2 RelativeChildOffset + { + get { return base.RelativeChildOffset; } + set { base.RelativeChildOffset = value; } + } + + /// + /// Controls which are automatically sized w.r.t. . + /// Children's are ignored for automatic sizing. + /// Most notably, and of children + /// do not affect automatic sizing to avoid circular size dependencies. + /// It is not allowed to manually set (or / ) + /// on any which are automatically sized. + /// + public new Axes AutoSizeAxes + { + get { return base.AutoSizeAxes; } + set { base.AutoSizeAxes = value; } + } + + /// + /// The duration which automatic sizing should take. If zero, then it is instantaneous. + /// Otherwise, this is equivalent to applying an automatic size via a resize transform. + /// + public new float AutoSizeDuration + { + get { return base.AutoSizeDuration; } + set { base.AutoSizeDuration = value; } + } + + /// + /// The type of easing which should be used for smooth automatic sizing when + /// is non-zero. + /// + public new Easing AutoSizeEasing + { + get { return base.AutoSizeEasing; } + set { base.AutoSizeEasing = value; } + } + } +} diff --git a/osu.Framework/Graphics/Containers/ContainerExtensions.cs b/osu.Framework/Graphics/Containers/ContainerExtensions.cs index 9a18fe8b6..f469496c6 100644 --- a/osu.Framework/Graphics/Containers/ContainerExtensions.cs +++ b/osu.Framework/Graphics/Containers/ContainerExtensions.cs @@ -1,62 +1,62 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// Holds extension methods for . - /// - public static class ContainerExtensions - { - /// - /// Wraps the given with the given - /// such that the can be used instead of the - /// without affecting the layout. The must not contain any children before wrapping. - /// - /// The type of the . - /// The type of the children of . - /// The that should wrap the given . - /// The that should be wrapped by the given . - /// The given . - public static T Wrap(this T container, U drawable) - where T : Container - where U : Drawable - { - if (container.Children.Count != 0) - throw new InvalidOperationException($"You may not wrap a {nameof(Container)} that has children."); - - container.RelativeSizeAxes = drawable.RelativeSizeAxes; - container.AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes; - container.Anchor = drawable.Anchor; - container.Origin = drawable.Origin; - container.Position = drawable.Position; - container.Rotation = drawable.Rotation; - - drawable.Position = Vector2.Zero; - drawable.Rotation = 0; - drawable.Anchor = Anchor.TopLeft; - drawable.Origin = Anchor.TopLeft; - - // For anchor/origin positioning to be preserved correctly, - // relatively sized axes must be lifted to the wrapping container. - if ((container.RelativeSizeAxes & Axes.X) > 0) - { - container.Width = drawable.Width; - drawable.Width = 1; - } - - if ((container.RelativeSizeAxes & Axes.Y) > 0) - { - container.Height = drawable.Height; - drawable.Height = 1; - } - - container.Add(drawable); - - return container; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// Holds extension methods for . + /// + public static class ContainerExtensions + { + /// + /// Wraps the given with the given + /// such that the can be used instead of the + /// without affecting the layout. The must not contain any children before wrapping. + /// + /// The type of the . + /// The type of the children of . + /// The that should wrap the given . + /// The that should be wrapped by the given . + /// The given . + public static T Wrap(this T container, U drawable) + where T : Container + where U : Drawable + { + if (container.Children.Count != 0) + throw new InvalidOperationException($"You may not wrap a {nameof(Container)} that has children."); + + container.RelativeSizeAxes = drawable.RelativeSizeAxes; + container.AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes; + container.Anchor = drawable.Anchor; + container.Origin = drawable.Origin; + container.Position = drawable.Position; + container.Rotation = drawable.Rotation; + + drawable.Position = Vector2.Zero; + drawable.Rotation = 0; + drawable.Anchor = Anchor.TopLeft; + drawable.Origin = Anchor.TopLeft; + + // For anchor/origin positioning to be preserved correctly, + // relatively sized axes must be lifted to the wrapping container. + if ((container.RelativeSizeAxes & Axes.X) > 0) + { + container.Width = drawable.Width; + drawable.Width = 1; + } + + if ((container.RelativeSizeAxes & Axes.Y) > 0) + { + container.Height = drawable.Height; + drawable.Height = 1; + } + + container.Add(drawable); + + return container; + } + } +} diff --git a/osu.Framework/Graphics/Containers/CustomizableTextContainer.cs b/osu.Framework/Graphics/Containers/CustomizableTextContainer.cs index 037508974..60e5e0a42 100644 --- a/osu.Framework/Graphics/Containers/CustomizableTextContainer.cs +++ b/osu.Framework/Graphics/Containers/CustomizableTextContainer.cs @@ -1,172 +1,172 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Sprites; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A that supports adding icons into its text. Inherit from this class to define reusable custom placeholders for icons. - /// - public class CustomizableTextContainer : TextFlowContainer - { - /// - /// Sets the placeholders that should be used to replace the numeric placeholders, in the order given. - /// - public IEnumerable Placeholders - { - set - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - placeholders.Clear(); - placeholders.AddRange(value); - } - } - - private readonly List placeholders = new List(); - private readonly Dictionary iconFactories = new Dictionary(); - - /// - /// Adds the given drawable as a placeholder that can be used when adding text. The drawable must not have a parent. Returns the index that can be used to reference the added placeholder. - /// - /// The drawable to use as a placeholder. This drawable must not have a parent. - /// The index that can be used to reference the added placeholder. - public int AddPlaceholder(Drawable drawable) - { - placeholders.Add(drawable); - return placeholders.Count - 1; - } - - /// - /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. - /// - /// The name of the placeholder that the factory should create drawables for. - /// The factory method creating drawables. - protected void AddIconFactory(string name, Delegate factory) => iconFactories.Add(name, factory); - - // I dislike the following overloads as much as you, but if we only had the general overload taking a Delegate, AddIconFactory("test", someInstanceMethod) would not compile (because we would need to cast someInstanceMethod to a delegate type first). - /// - /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. - /// - /// The name of the placeholder that the factory should create drawables for. - /// The factory method creating drawables. - protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); - - /// - /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. - /// - /// The name of the placeholder that the factory should create drawables for. - /// The factory method creating drawables. - protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); - - /// - /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. - /// - /// The name of the placeholder that the factory should create drawables for. - /// The factory method creating drawables. - protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); - - internal override IEnumerable AddLine(TextLine line, bool newLineIsParagraph) - { - if (!newLineIsParagraph) - AddInternal(new NewLineContainer(true)); - - var sprites = new List(); - int index = 0; - string str = line.Text; - while (index < str.Length) - { - Drawable placeholderDrawable = null; - int nextPlaceholderIndex = str.IndexOf('[', index); - // make sure we skip ahead to the next [ as long as the current [ is escaped - while (nextPlaceholderIndex != -1 && str.IndexOf("[[", nextPlaceholderIndex, StringComparison.InvariantCulture) == nextPlaceholderIndex) - nextPlaceholderIndex = str.IndexOf('[', nextPlaceholderIndex + 2); - - string strPiece = null; - if (nextPlaceholderIndex != -1) - { - int placeholderEnd = str.IndexOf(']', nextPlaceholderIndex); - // make sure we skip ahead to the next ] as long as the current ] is escaped - while (placeholderEnd != -1 && str.IndexOf("]]", placeholderEnd, StringComparison.InvariantCulture) == placeholderEnd) - placeholderEnd = str.IndexOf(']', placeholderEnd + 2); - - if (placeholderEnd != -1) - { - strPiece = str.Substring(index, nextPlaceholderIndex - index); - string placeholderStr = str.Substring(nextPlaceholderIndex + 1, placeholderEnd - nextPlaceholderIndex - 1).Trim(); - string placeholderName = placeholderStr; - string paramStr = ""; - int parensOpen = placeholderStr.IndexOf('('); - if (parensOpen != -1) - { - placeholderName = placeholderStr.Substring(0, parensOpen).Trim(); - int parensClose = placeholderStr.IndexOf(')', parensOpen); - if (parensClose != -1) - paramStr = placeholderStr.Substring(parensOpen + 1, parensClose - parensOpen - 1).Trim(); - else - throw new ArgumentException($"Missing ) in placeholder {placeholderStr}."); - } - - if (int.TryParse(placeholderStr, out int placeholderIndex)) - { - if (placeholderIndex >= placeholders.Count) - throw new ArgumentException($"This text has {placeholders.Count} placeholders. But placeholder with index {placeholderIndex} was used."); - if (placeholderIndex < 0) - throw new ArgumentException($"Negative placeholder indices are invalid. Index {placeholderIndex} was used."); - - placeholderDrawable = placeholders[placeholderIndex]; - } - else - { - object[] args; - if (string.IsNullOrWhiteSpace(paramStr)) - { - args = Array.Empty(); - } - else - { - string[] argStrs = paramStr.Split(','); - args = new object[argStrs.Length]; - for (int i = 0; i < argStrs.Length; ++i) - { - if (!int.TryParse(argStrs[i], out int argVal)) - throw new ArgumentException($"The argument \"{argStrs[i]}\" in placeholder {placeholderStr} is not an integer."); - - args[i] = argVal; - } - } - - if (!iconFactories.TryGetValue(placeholderName, out Delegate cb)) - throw new ArgumentException($"There is no placeholder named {placeholderName}."); - - placeholderDrawable = (Drawable)cb.DynamicInvoke(args); - } - index = placeholderEnd + 1; - } - } - - if (strPiece == null) - { - strPiece = str.Substring(index); - index = str.Length; - } - // unescape stuff - strPiece = strPiece.Replace("[[", "[").Replace("]]", "]"); - sprites.AddRange(AddString(new TextLine(strPiece, line.CreationParameters), newLineIsParagraph)); - - if (placeholderDrawable != null) - { - if (placeholderDrawable.Parent != null) - throw new ArgumentException("All icons used by a customizable text container must not have a parent. If you get this error message it means one of your icon factories created a drawable that was already added to another parent, or you used a drawable as a placeholder that already has another parent or you used an index-based placeholder (like [2]) more than once."); - AddInternal(placeholderDrawable); - } - } - - return sprites; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A that supports adding icons into its text. Inherit from this class to define reusable custom placeholders for icons. + /// + public class CustomizableTextContainer : TextFlowContainer + { + /// + /// Sets the placeholders that should be used to replace the numeric placeholders, in the order given. + /// + public IEnumerable Placeholders + { + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + placeholders.Clear(); + placeholders.AddRange(value); + } + } + + private readonly List placeholders = new List(); + private readonly Dictionary iconFactories = new Dictionary(); + + /// + /// Adds the given drawable as a placeholder that can be used when adding text. The drawable must not have a parent. Returns the index that can be used to reference the added placeholder. + /// + /// The drawable to use as a placeholder. This drawable must not have a parent. + /// The index that can be used to reference the added placeholder. + public int AddPlaceholder(Drawable drawable) + { + placeholders.Add(drawable); + return placeholders.Count - 1; + } + + /// + /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. + /// + /// The name of the placeholder that the factory should create drawables for. + /// The factory method creating drawables. + protected void AddIconFactory(string name, Delegate factory) => iconFactories.Add(name, factory); + + // I dislike the following overloads as much as you, but if we only had the general overload taking a Delegate, AddIconFactory("test", someInstanceMethod) would not compile (because we would need to cast someInstanceMethod to a delegate type first). + /// + /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. + /// + /// The name of the placeholder that the factory should create drawables for. + /// The factory method creating drawables. + protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); + + /// + /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. + /// + /// The name of the placeholder that the factory should create drawables for. + /// The factory method creating drawables. + protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); + + /// + /// Adds the given factory method as a placeholder. It will be used to create a drawable each time [] is encountered in the text. The method must return a and may contain an arbitrary number of integer parameters. If there are, fe, 2 integer parameters on the factory method, the placeholder in the text would need to look like [(42, 1337)] supplying the values 42 and 1337 to the method as arguments. + /// + /// The name of the placeholder that the factory should create drawables for. + /// The factory method creating drawables. + protected void AddIconFactory(string name, Func factory) => iconFactories.Add(name, factory); + + internal override IEnumerable AddLine(TextLine line, bool newLineIsParagraph) + { + if (!newLineIsParagraph) + AddInternal(new NewLineContainer(true)); + + var sprites = new List(); + int index = 0; + string str = line.Text; + while (index < str.Length) + { + Drawable placeholderDrawable = null; + int nextPlaceholderIndex = str.IndexOf('[', index); + // make sure we skip ahead to the next [ as long as the current [ is escaped + while (nextPlaceholderIndex != -1 && str.IndexOf("[[", nextPlaceholderIndex, StringComparison.InvariantCulture) == nextPlaceholderIndex) + nextPlaceholderIndex = str.IndexOf('[', nextPlaceholderIndex + 2); + + string strPiece = null; + if (nextPlaceholderIndex != -1) + { + int placeholderEnd = str.IndexOf(']', nextPlaceholderIndex); + // make sure we skip ahead to the next ] as long as the current ] is escaped + while (placeholderEnd != -1 && str.IndexOf("]]", placeholderEnd, StringComparison.InvariantCulture) == placeholderEnd) + placeholderEnd = str.IndexOf(']', placeholderEnd + 2); + + if (placeholderEnd != -1) + { + strPiece = str.Substring(index, nextPlaceholderIndex - index); + string placeholderStr = str.Substring(nextPlaceholderIndex + 1, placeholderEnd - nextPlaceholderIndex - 1).Trim(); + string placeholderName = placeholderStr; + string paramStr = ""; + int parensOpen = placeholderStr.IndexOf('('); + if (parensOpen != -1) + { + placeholderName = placeholderStr.Substring(0, parensOpen).Trim(); + int parensClose = placeholderStr.IndexOf(')', parensOpen); + if (parensClose != -1) + paramStr = placeholderStr.Substring(parensOpen + 1, parensClose - parensOpen - 1).Trim(); + else + throw new ArgumentException($"Missing ) in placeholder {placeholderStr}."); + } + + if (int.TryParse(placeholderStr, out int placeholderIndex)) + { + if (placeholderIndex >= placeholders.Count) + throw new ArgumentException($"This text has {placeholders.Count} placeholders. But placeholder with index {placeholderIndex} was used."); + if (placeholderIndex < 0) + throw new ArgumentException($"Negative placeholder indices are invalid. Index {placeholderIndex} was used."); + + placeholderDrawable = placeholders[placeholderIndex]; + } + else + { + object[] args; + if (string.IsNullOrWhiteSpace(paramStr)) + { + args = Array.Empty(); + } + else + { + string[] argStrs = paramStr.Split(','); + args = new object[argStrs.Length]; + for (int i = 0; i < argStrs.Length; ++i) + { + if (!int.TryParse(argStrs[i], out int argVal)) + throw new ArgumentException($"The argument \"{argStrs[i]}\" in placeholder {placeholderStr} is not an integer."); + + args[i] = argVal; + } + } + + if (!iconFactories.TryGetValue(placeholderName, out Delegate cb)) + throw new ArgumentException($"There is no placeholder named {placeholderName}."); + + placeholderDrawable = (Drawable)cb.DynamicInvoke(args); + } + index = placeholderEnd + 1; + } + } + + if (strPiece == null) + { + strPiece = str.Substring(index); + index = str.Length; + } + // unescape stuff + strPiece = strPiece.Replace("[[", "[").Replace("]]", "]"); + sprites.AddRange(AddString(new TextLine(strPiece, line.CreationParameters), newLineIsParagraph)); + + if (placeholderDrawable != null) + { + if (placeholderDrawable.Parent != null) + throw new ArgumentException("All icons used by a customizable text container must not have a parent. If you get this error message it means one of your icon factories created a drawable that was already added to another parent, or you used a drawable as a placeholder that already has another parent or you used an index-based placeholder (like [2]) more than once."); + AddInternal(placeholderDrawable); + } + } + + return sprites; + } + } +} diff --git a/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs b/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs index f0e794fd6..f5e7540a9 100644 --- a/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs +++ b/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs @@ -1,101 +1,101 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading.Tasks; -using osu.Framework.Caching; -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container which asynchronously loads specified content. - /// Has the ability to delay the loading until it has been visible on-screen for a specified duration. - /// In order to benefit from delayed load, we must be inside a . - /// - public class DelayedLoadWrapper : CompositeDrawable - { - /// - /// Creates a that will asynchronously load the given with a delay. - /// - /// If is set to 0, the loading process will begin on the next Update call. - /// The to be loaded. - /// The delay in milliseconds before loading can begin. - public DelayedLoadWrapper(Drawable content, double timeBeforeLoad = 500) - { - Content = content ?? throw new ArgumentNullException(nameof(content), $@"{nameof(DelayedLoadWrapper)} required non-null {nameof(content)}."); - this.timeBeforeLoad = timeBeforeLoad; - - RelativeSizeAxes = content.RelativeSizeAxes; - AutoSizeAxes = (content as CompositeDrawable)?.AutoSizeAxes ?? AutoSizeAxes; - } - - public override double LifetimeStart => Content.LifetimeStart; - - public override double LifetimeEnd => Content.LifetimeEnd; - - internal readonly Drawable Content; - - /// - /// The amount of time on-screen in milliseconds before we begin a load of children. - /// - private readonly double timeBeforeLoad; - - private double timeVisible; - - protected bool ShouldLoadContent => timeBeforeLoad == 0 || timeVisible > timeBeforeLoad; - - private Task loadTask; - - protected override void Update() - { - // This code can be expensive, so only run if we haven't yet loaded. - if (!LoadTriggered) - { - if (!isIntersecting) - timeVisible = 0; - else - timeVisible += Time.Elapsed; - } - - base.Update(); - - if (!LoadTriggered && ShouldLoadContent) - loadTask = LoadComponentAsync(Content, AddInternal); - } - - /// - /// True if the load task for our content has been started. - /// Will remain true even after load is completed. - /// - protected bool LoadTriggered => loadTask != null; - - private Cached isIntersectingBacking; - - private bool isIntersecting => isIntersectingBacking.IsValid ? isIntersectingBacking : (isIntersectingBacking.Value = checkScrollIntersection()); - - private bool checkScrollIntersection() - { - IOnScreenOptimisingContainer scroll = null; - CompositeDrawable cursor = this; - while (scroll == null && (cursor = cursor.Parent) != null) - scroll = cursor as IOnScreenOptimisingContainer; - - return scroll?.ScreenSpaceDrawQuad.Intersects(ScreenSpaceDrawQuad) ?? true; - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - isIntersectingBacking.Invalidate(); - return base.Invalidate(invalidation, source, shallPropagate); - } - - /// - /// A container which acts as a masking parent for on-screen delayed load optimisations. - /// - public interface IOnScreenOptimisingContainer - { - Quad ScreenSpaceDrawQuad { get; } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading.Tasks; +using osu.Framework.Caching; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which asynchronously loads specified content. + /// Has the ability to delay the loading until it has been visible on-screen for a specified duration. + /// In order to benefit from delayed load, we must be inside a . + /// + public class DelayedLoadWrapper : CompositeDrawable + { + /// + /// Creates a that will asynchronously load the given with a delay. + /// + /// If is set to 0, the loading process will begin on the next Update call. + /// The to be loaded. + /// The delay in milliseconds before loading can begin. + public DelayedLoadWrapper(Drawable content, double timeBeforeLoad = 500) + { + Content = content ?? throw new ArgumentNullException(nameof(content), $@"{nameof(DelayedLoadWrapper)} required non-null {nameof(content)}."); + this.timeBeforeLoad = timeBeforeLoad; + + RelativeSizeAxes = content.RelativeSizeAxes; + AutoSizeAxes = (content as CompositeDrawable)?.AutoSizeAxes ?? AutoSizeAxes; + } + + public override double LifetimeStart => Content.LifetimeStart; + + public override double LifetimeEnd => Content.LifetimeEnd; + + internal readonly Drawable Content; + + /// + /// The amount of time on-screen in milliseconds before we begin a load of children. + /// + private readonly double timeBeforeLoad; + + private double timeVisible; + + protected bool ShouldLoadContent => timeBeforeLoad == 0 || timeVisible > timeBeforeLoad; + + private Task loadTask; + + protected override void Update() + { + // This code can be expensive, so only run if we haven't yet loaded. + if (!LoadTriggered) + { + if (!isIntersecting) + timeVisible = 0; + else + timeVisible += Time.Elapsed; + } + + base.Update(); + + if (!LoadTriggered && ShouldLoadContent) + loadTask = LoadComponentAsync(Content, AddInternal); + } + + /// + /// True if the load task for our content has been started. + /// Will remain true even after load is completed. + /// + protected bool LoadTriggered => loadTask != null; + + private Cached isIntersectingBacking; + + private bool isIntersecting => isIntersectingBacking.IsValid ? isIntersectingBacking : (isIntersectingBacking.Value = checkScrollIntersection()); + + private bool checkScrollIntersection() + { + IOnScreenOptimisingContainer scroll = null; + CompositeDrawable cursor = this; + while (scroll == null && (cursor = cursor.Parent) != null) + scroll = cursor as IOnScreenOptimisingContainer; + + return scroll?.ScreenSpaceDrawQuad.Intersects(ScreenSpaceDrawQuad) ?? true; + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + isIntersectingBacking.Invalidate(); + return base.Invalidate(invalidation, source, shallPropagate); + } + + /// + /// A container which acts as a masking parent for on-screen delayed load optimisations. + /// + public interface IOnScreenOptimisingContainer + { + Quad ScreenSpaceDrawQuad { get; } + } + } +} diff --git a/osu.Framework/Graphics/Containers/DrawSizePreservingFillContainer.cs b/osu.Framework/Graphics/Containers/DrawSizePreservingFillContainer.cs index 445075ba9..0ee075628 100644 --- a/osu.Framework/Graphics/Containers/DrawSizePreservingFillContainer.cs +++ b/osu.Framework/Graphics/Containers/DrawSizePreservingFillContainer.cs @@ -1,102 +1,102 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A filling its parent while preserving a given target - /// according to a . - /// This is useful, for example, to automatically scale the user interface according to - /// the window resolution, or to provide automatic HiDPI display support. - /// - public class DrawSizePreservingFillContainer : Container - { - private readonly Container content; - - protected override Container Content => content; - - /// - /// The target to be enforced according to . - /// - public Vector2 TargetDrawSize = new Vector2(1024, 768); - - /// - /// The strategy to be used for enforcing . The default strategy - /// is Minimum, which preserves the aspect ratio of all children while ensuring one of the - /// two axes matches while the other is always larger. - /// - public DrawSizePreservationStrategy Strategy; - - public DrawSizePreservingFillContainer() - { - AddInternal(content = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - RelativeSizeAxes = Axes.Both; - } - - protected override void Update() - { - base.Update(); - - Vector2 drawSizeRatio = Vector2.Divide(Parent.DrawSize, TargetDrawSize); - - switch (Strategy) - { - case DrawSizePreservationStrategy.Minimum: - content.Scale = new Vector2(Math.Min(drawSizeRatio.X, drawSizeRatio.Y)); - break; - - case DrawSizePreservationStrategy.Maximum: - content.Scale = new Vector2(Math.Max(drawSizeRatio.X, drawSizeRatio.Y)); - break; - - case DrawSizePreservationStrategy.Average: - content.Scale = new Vector2(0.5f * (drawSizeRatio.X + drawSizeRatio.Y)); - break; - - case DrawSizePreservationStrategy.Separate: - content.Scale = drawSizeRatio; - break; - } - - content.Size = Vector2.Divide(Vector2.One, content.Scale); - } - } - - /// - /// Strategies used by to enforce its - /// . - /// - public enum DrawSizePreservationStrategy - { - /// - /// Preserves the aspect ratio of all children while ensuring one of the - /// two axes matches - /// while the other is always larger. - /// - Minimum, - /// - /// Preserves the aspect ratio of all children while ensuring one of the - /// two axes matches - /// while the other is always smaller. - /// - Maximum, - /// - /// Preserves the aspect ratio of all children while one axis is always larger and - /// the other always smaller than , - /// achieving a good compromise. - /// - Average, - /// - /// Ensures is perfectly - /// matched while aspect ratio of children is disregarded. - /// - Separate, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A filling its parent while preserving a given target + /// according to a . + /// This is useful, for example, to automatically scale the user interface according to + /// the window resolution, or to provide automatic HiDPI display support. + /// + public class DrawSizePreservingFillContainer : Container + { + private readonly Container content; + + protected override Container Content => content; + + /// + /// The target to be enforced according to . + /// + public Vector2 TargetDrawSize = new Vector2(1024, 768); + + /// + /// The strategy to be used for enforcing . The default strategy + /// is Minimum, which preserves the aspect ratio of all children while ensuring one of the + /// two axes matches while the other is always larger. + /// + public DrawSizePreservationStrategy Strategy; + + public DrawSizePreservingFillContainer() + { + AddInternal(content = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + RelativeSizeAxes = Axes.Both; + } + + protected override void Update() + { + base.Update(); + + Vector2 drawSizeRatio = Vector2.Divide(Parent.DrawSize, TargetDrawSize); + + switch (Strategy) + { + case DrawSizePreservationStrategy.Minimum: + content.Scale = new Vector2(Math.Min(drawSizeRatio.X, drawSizeRatio.Y)); + break; + + case DrawSizePreservationStrategy.Maximum: + content.Scale = new Vector2(Math.Max(drawSizeRatio.X, drawSizeRatio.Y)); + break; + + case DrawSizePreservationStrategy.Average: + content.Scale = new Vector2(0.5f * (drawSizeRatio.X + drawSizeRatio.Y)); + break; + + case DrawSizePreservationStrategy.Separate: + content.Scale = drawSizeRatio; + break; + } + + content.Size = Vector2.Divide(Vector2.One, content.Scale); + } + } + + /// + /// Strategies used by to enforce its + /// . + /// + public enum DrawSizePreservationStrategy + { + /// + /// Preserves the aspect ratio of all children while ensuring one of the + /// two axes matches + /// while the other is always larger. + /// + Minimum, + /// + /// Preserves the aspect ratio of all children while ensuring one of the + /// two axes matches + /// while the other is always smaller. + /// + Maximum, + /// + /// Preserves the aspect ratio of all children while one axis is always larger and + /// the other always smaller than , + /// achieving a good compromise. + /// + Average, + /// + /// Ensures is perfectly + /// matched while aspect ratio of children is disregarded. + /// + Separate, + } +} diff --git a/osu.Framework/Graphics/Containers/FillFlowContainer.cs b/osu.Framework/Graphics/Containers/FillFlowContainer.cs index 8e58db392..bd928a108 100644 --- a/osu.Framework/Graphics/Containers/FillFlowContainer.cs +++ b/osu.Framework/Graphics/Containers/FillFlowContainer.cs @@ -1,266 +1,266 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; -using System.Linq; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A that fills space by arranging its children - /// next to each other. - /// can be arranged horizontally, vertically, and in a - /// combined fashion, which is controlled by . - /// are arranged from left-to-right if their - /// is to the left or centered horizontally. - /// They are arranged from right-to-left otherwise. - /// are arranged from top-to-bottom if their - /// is to the top or centered vertically. - /// They are arranged from bottom-to-top otherwise. - /// If non- are desired, use - /// . - /// - public class FillFlowContainer : FillFlowContainer - { - } - - /// - /// A that fills space by arranging its children - /// next to each other. - /// can be arranged horizontally, vertically, and in a - /// combined fashion, which is controlled by . - /// are arranged from left-to-right if their - /// is to the left or centered horizontally. - /// They are arranged from right-to-left otherwise. - /// are arranged from top-to-bottom if their - /// is to the top or centered vertically. - /// They are arranged from bottom-to-top otherwise. - /// - public class FillFlowContainer : FlowContainer, IFillFlowContainer where T : Drawable - { - private FillDirection direction = FillDirection.Full; - - /// - /// If or , - /// are arranged from left-to-right if their - /// is to the left or centered horizontally. - /// They are arranged from right-to-left otherwise. - /// If or , - /// are arranged from top-to-bottom if their - /// is to the top or centered vertically. - /// They are arranged from bottom-to-top otherwise. - /// - public FillDirection Direction - { - get { return direction; } - set - { - if (direction == value) - return; - - direction = value; - InvalidateLayout(); - } - } - - private Vector2 spacing; - - /// - /// The spacing between individual elements. Default is . - /// - public Vector2 Spacing - { - get { return spacing; } - set - { - if (spacing == value) - return; - - spacing = value; - InvalidateLayout(); - } - } - - private Vector2 spacingFactor(Drawable c) - { - Vector2 result = c.RelativeOriginPosition; - if ((c.Anchor & Anchor.x2) > 0) - result.X = 1 - result.X; - if ((c.Anchor & Anchor.y2) > 0) - result.Y = 1 - result.Y; - return result; - } - - protected override IEnumerable ComputeLayoutPositions() - { - var max = MaximumSize; - if (max == Vector2.Zero) - { - var s = ChildSize; - - // If we are autosize and haven't specified a maximum size, we should allow infinite expansion. - // If we are inheriting then we need to use the parent size (our ActualSize). - max.X = (AutoSizeAxes & Axes.X) > 0 ? float.MaxValue : s.X; - max.Y = (AutoSizeAxes & Axes.Y) > 0 ? float.MaxValue : s.Y; - } - - var children = FlowingChildren.ToArray(); - if (children.Length == 0) - return new List(); - - // The positions for each child we will return later on. - Vector2[] result = new Vector2[children.Length]; - - // We need to keep track of row widths such that we can compute correct - // positions for horizontal centre anchor children. - // We also store for each child to which row it belongs. - int[] rowIndices = new int[children.Length]; - List rowOffsetsToMiddle = new List { 0 }; - - // Variables keeping track of the current state while iterating over children - // and computing initial flow positions. - float rowHeight = 0; - float rowBeginOffset = 0; - var current = Vector2.Zero; - - // First pass, computing initial flow positions - Vector2 size = Vector2.Zero; - for (int i = 0; i < children.Length; ++i) - { - Drawable c = children[i]; - - // Populate running variables with sane initial values. - if (i == 0) - { - size = c.BoundingBox.Size; - rowBeginOffset = spacingFactor(c).X * size.X; - } - - float rowWidth = rowBeginOffset + current.X + (1 - spacingFactor(c).X) * size.X; - - //We've exceeded our allowed width, move to a new row - if (direction != FillDirection.Horizontal && (Precision.DefinitelyBigger(rowWidth, max.X) || direction == FillDirection.Vertical || ForceNewRow(c))) - { - current.X = 0; - current.Y += rowHeight; - - result[i] = current; - - rowOffsetsToMiddle.Add(0); - rowBeginOffset = spacingFactor(c).X * size.X; - - rowHeight = 0; - } - else - { - result[i] = current; - - // Compute offset to the middle of the row, to be applied in case of centre anchor - // in a second pass. - rowOffsetsToMiddle[rowOffsetsToMiddle.Count - 1] = rowBeginOffset - rowWidth / 2; - } - - rowIndices[i] = rowOffsetsToMiddle.Count - 1; - - Vector2 stride = Vector2.Zero; - if (i < children.Length - 1) - { - // Compute stride. Note, that the stride depends on the origins of the drawables - // on both sides of the step to be taken. - stride = (Vector2.One - spacingFactor(c)) * size; - - c = children[i + 1]; - size = c.BoundingBox.Size; - - stride += spacingFactor(c) * size; - } - - stride += Spacing; - - if (stride.Y > rowHeight) - rowHeight = stride.Y; - current.X += stride.X; - } - - float height = result.Last().Y; - - Vector2 ourRelativeAnchor = children[0].RelativeAnchorPosition; - - // Second pass, adjusting the positions for anchors of children. - // Uses rowWidths and height for centre-anchors. - for (int i = 0; i < children.Length; ++i) - { - var c = children[i]; - - switch (Direction) - { - case FillDirection.Vertical: - if (c.RelativeAnchorPosition.Y != ourRelativeAnchor.Y) - throw new InvalidOperationException( - $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.Y} != {c.RelativeAnchorPosition.Y}). " - + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); - break; - case FillDirection.Horizontal: - if (c.RelativeAnchorPosition.X != ourRelativeAnchor.X) - throw new InvalidOperationException( - $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.X} != {c.RelativeAnchorPosition.X}). " - + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); - break; - default: - if (c.RelativeAnchorPosition != ourRelativeAnchor) - throw new InvalidOperationException( - $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor} != {c.RelativeAnchorPosition}). " - + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); - break; - } - - if ((c.Anchor & Anchor.x1) > 0) - // Begin flow at centre of row - result[i].X += rowOffsetsToMiddle[rowIndices[i]]; - else if ((c.Anchor & Anchor.x2) > 0) - // Flow right-to-left - result[i].X = -result[i].X; - - if ((c.Anchor & Anchor.y1) > 0) - // Begin flow at centre of total height - result[i].Y -= height / 2; - else if ((c.Anchor & Anchor.y2) > 0) - // Flow bottom-to-top - result[i].Y = -result[i].Y; - } - - return result; - } - - /// - /// Returns true if the given child should be placed on a new row, false otherwise. This will be called automatically for each child in this FillFlowContainers FlowingChildren-List. - /// - /// The child to check. - /// True if the given child should be placed on a new row, false otherwise. - protected virtual bool ForceNewRow(Drawable child) => false; - } - - /// - /// Represents the horizontal direction of a fill flow. - /// - public enum FillDirection - { - /// - /// Fill horizontally first, then fill vertically via multiple rows. - /// - Full, - - /// - /// Fill only horizontally. - /// - Horizontal, - - /// - /// Fill only vertically. - /// - Vertical, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using OpenTK; +using System.Linq; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A that fills space by arranging its children + /// next to each other. + /// can be arranged horizontally, vertically, and in a + /// combined fashion, which is controlled by . + /// are arranged from left-to-right if their + /// is to the left or centered horizontally. + /// They are arranged from right-to-left otherwise. + /// are arranged from top-to-bottom if their + /// is to the top or centered vertically. + /// They are arranged from bottom-to-top otherwise. + /// If non- are desired, use + /// . + /// + public class FillFlowContainer : FillFlowContainer + { + } + + /// + /// A that fills space by arranging its children + /// next to each other. + /// can be arranged horizontally, vertically, and in a + /// combined fashion, which is controlled by . + /// are arranged from left-to-right if their + /// is to the left or centered horizontally. + /// They are arranged from right-to-left otherwise. + /// are arranged from top-to-bottom if their + /// is to the top or centered vertically. + /// They are arranged from bottom-to-top otherwise. + /// + public class FillFlowContainer : FlowContainer, IFillFlowContainer where T : Drawable + { + private FillDirection direction = FillDirection.Full; + + /// + /// If or , + /// are arranged from left-to-right if their + /// is to the left or centered horizontally. + /// They are arranged from right-to-left otherwise. + /// If or , + /// are arranged from top-to-bottom if their + /// is to the top or centered vertically. + /// They are arranged from bottom-to-top otherwise. + /// + public FillDirection Direction + { + get { return direction; } + set + { + if (direction == value) + return; + + direction = value; + InvalidateLayout(); + } + } + + private Vector2 spacing; + + /// + /// The spacing between individual elements. Default is . + /// + public Vector2 Spacing + { + get { return spacing; } + set + { + if (spacing == value) + return; + + spacing = value; + InvalidateLayout(); + } + } + + private Vector2 spacingFactor(Drawable c) + { + Vector2 result = c.RelativeOriginPosition; + if ((c.Anchor & Anchor.x2) > 0) + result.X = 1 - result.X; + if ((c.Anchor & Anchor.y2) > 0) + result.Y = 1 - result.Y; + return result; + } + + protected override IEnumerable ComputeLayoutPositions() + { + var max = MaximumSize; + if (max == Vector2.Zero) + { + var s = ChildSize; + + // If we are autosize and haven't specified a maximum size, we should allow infinite expansion. + // If we are inheriting then we need to use the parent size (our ActualSize). + max.X = (AutoSizeAxes & Axes.X) > 0 ? float.MaxValue : s.X; + max.Y = (AutoSizeAxes & Axes.Y) > 0 ? float.MaxValue : s.Y; + } + + var children = FlowingChildren.ToArray(); + if (children.Length == 0) + return new List(); + + // The positions for each child we will return later on. + Vector2[] result = new Vector2[children.Length]; + + // We need to keep track of row widths such that we can compute correct + // positions for horizontal centre anchor children. + // We also store for each child to which row it belongs. + int[] rowIndices = new int[children.Length]; + List rowOffsetsToMiddle = new List { 0 }; + + // Variables keeping track of the current state while iterating over children + // and computing initial flow positions. + float rowHeight = 0; + float rowBeginOffset = 0; + var current = Vector2.Zero; + + // First pass, computing initial flow positions + Vector2 size = Vector2.Zero; + for (int i = 0; i < children.Length; ++i) + { + Drawable c = children[i]; + + // Populate running variables with sane initial values. + if (i == 0) + { + size = c.BoundingBox.Size; + rowBeginOffset = spacingFactor(c).X * size.X; + } + + float rowWidth = rowBeginOffset + current.X + (1 - spacingFactor(c).X) * size.X; + + //We've exceeded our allowed width, move to a new row + if (direction != FillDirection.Horizontal && (Precision.DefinitelyBigger(rowWidth, max.X) || direction == FillDirection.Vertical || ForceNewRow(c))) + { + current.X = 0; + current.Y += rowHeight; + + result[i] = current; + + rowOffsetsToMiddle.Add(0); + rowBeginOffset = spacingFactor(c).X * size.X; + + rowHeight = 0; + } + else + { + result[i] = current; + + // Compute offset to the middle of the row, to be applied in case of centre anchor + // in a second pass. + rowOffsetsToMiddle[rowOffsetsToMiddle.Count - 1] = rowBeginOffset - rowWidth / 2; + } + + rowIndices[i] = rowOffsetsToMiddle.Count - 1; + + Vector2 stride = Vector2.Zero; + if (i < children.Length - 1) + { + // Compute stride. Note, that the stride depends on the origins of the drawables + // on both sides of the step to be taken. + stride = (Vector2.One - spacingFactor(c)) * size; + + c = children[i + 1]; + size = c.BoundingBox.Size; + + stride += spacingFactor(c) * size; + } + + stride += Spacing; + + if (stride.Y > rowHeight) + rowHeight = stride.Y; + current.X += stride.X; + } + + float height = result.Last().Y; + + Vector2 ourRelativeAnchor = children[0].RelativeAnchorPosition; + + // Second pass, adjusting the positions for anchors of children. + // Uses rowWidths and height for centre-anchors. + for (int i = 0; i < children.Length; ++i) + { + var c = children[i]; + + switch (Direction) + { + case FillDirection.Vertical: + if (c.RelativeAnchorPosition.Y != ourRelativeAnchor.Y) + throw new InvalidOperationException( + $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.Y} != {c.RelativeAnchorPosition.Y}). " + + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); + break; + case FillDirection.Horizontal: + if (c.RelativeAnchorPosition.X != ourRelativeAnchor.X) + throw new InvalidOperationException( + $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.X} != {c.RelativeAnchorPosition.X}). " + + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); + break; + default: + if (c.RelativeAnchorPosition != ourRelativeAnchor) + throw new InvalidOperationException( + $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor} != {c.RelativeAnchorPosition}). " + + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional."); + break; + } + + if ((c.Anchor & Anchor.x1) > 0) + // Begin flow at centre of row + result[i].X += rowOffsetsToMiddle[rowIndices[i]]; + else if ((c.Anchor & Anchor.x2) > 0) + // Flow right-to-left + result[i].X = -result[i].X; + + if ((c.Anchor & Anchor.y1) > 0) + // Begin flow at centre of total height + result[i].Y -= height / 2; + else if ((c.Anchor & Anchor.y2) > 0) + // Flow bottom-to-top + result[i].Y = -result[i].Y; + } + + return result; + } + + /// + /// Returns true if the given child should be placed on a new row, false otherwise. This will be called automatically for each child in this FillFlowContainers FlowingChildren-List. + /// + /// The child to check. + /// True if the given child should be placed on a new row, false otherwise. + protected virtual bool ForceNewRow(Drawable child) => false; + } + + /// + /// Represents the horizontal direction of a fill flow. + /// + public enum FillDirection + { + /// + /// Fill horizontally first, then fill vertically via multiple rows. + /// + Full, + + /// + /// Fill only horizontally. + /// + Horizontal, + + /// + /// Fill only vertically. + /// + Vertical, + } +} diff --git a/osu.Framework/Graphics/Containers/FlowContainer.cs b/osu.Framework/Graphics/Containers/FlowContainer.cs index a94ebdeef..de053183d 100644 --- a/osu.Framework/Graphics/Containers/FlowContainer.cs +++ b/osu.Framework/Graphics/Containers/FlowContainer.cs @@ -1,221 +1,221 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using osu.Framework.Caching; -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics.Transforms; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container that can be used to fluently arrange its children. - /// - public abstract class FlowContainer : Container - where T : Drawable - { - internal event Action OnLayout; - - /// - /// The easing that should be used when children are moved to their position in the layout. - /// - public Easing LayoutEasing - { - get { return AutoSizeEasing; } - set { AutoSizeEasing = value; } - } - - /// - /// The time it should take to move a child from its current position to its new layout position. - /// - public float LayoutDuration - { - get { return AutoSizeDuration * 2; } - set - { - //coupling with autosizeduration allows us to smoothly transition our size - //when no children are left to dictate autosize. - AutoSizeDuration = value / 2; - } - } - - private Cached layout = new Cached(); - - protected void InvalidateLayout() => layout.Invalidate(); - - private Vector2 maximumSize; - - /// - /// Optional maximum dimensions for this container. Note that the meaning of this value can change - /// depending on the implementation. - /// - public Vector2 MaximumSize - { - get { return maximumSize; } - set - { - if (maximumSize == value) return; - - maximumSize = value; - Invalidate(Invalidation.DrawSize); - } - } - - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || !layout.IsValid; - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - InvalidateLayout(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - - private readonly Dictionary layoutChildren = new Dictionary(); - - protected internal override void AddInternal(Drawable drawable) - { - layoutChildren.Add(drawable, 0f); - // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate - // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. - InvalidateLayout(); - base.AddInternal(drawable); - } - - protected internal override bool RemoveInternal(Drawable drawable) - { - layoutChildren.Remove(drawable); - // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate - // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. - InvalidateLayout(); - return base.RemoveInternal(drawable); - } - - protected internal override void ClearInternal(bool disposeChildren = true) - { - layoutChildren.Clear(); - // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate - // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. - InvalidateLayout(); - base.ClearInternal(disposeChildren); - } - - /// - /// Changes the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). - /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . - /// - /// The drawable whose position should be changed, must be a child of this container. - /// The new position in the layout the drawable should have. - public void SetLayoutPosition(Drawable drawable, float newPosition) - { - if (!layoutChildren.ContainsKey(drawable)) - throw new InvalidOperationException($"Cannot change layout position of drawable which is not contained within this {nameof(FlowContainer)}."); - layoutChildren[drawable] = newPosition; - InvalidateLayout(); - } - - /// - /// Gets the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). - /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . - /// - /// The drawable whose position should be retrieved, must be a child of this container. - /// The position of the drawable in the layout. - public float GetLayoutPosition(Drawable drawable) - { - if (!layoutChildren.ContainsKey(drawable)) - throw new InvalidOperationException($"Cannot get layout position of drawable which is not contained within this {nameof(FlowContainer)}."); - - return layoutChildren[drawable]; - } - - protected override bool UpdateChildrenLife() - { - bool changed = base.UpdateChildrenLife(); - - if (changed) - InvalidateLayout(); - - return changed; - } - - public override void InvalidateFromChild(Invalidation invalidation) - { - //Colour captures potential changes in IsPresent. If this ever becomes a bottleneck, - //Invalidation could be further separated into presence changes. - if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) - InvalidateLayout(); - - base.InvalidateFromChild(invalidation); - } - - /// - /// Gets the children that appear in the flow of this in the order in which they are processed within the flowing layout. - /// - public virtual IEnumerable FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => layoutChildren[d]).ThenBy(d => d.ChildID); - - protected abstract IEnumerable ComputeLayoutPositions(); - - private void performLayout() - { - OnLayout?.Invoke(); - - if (!Children.Any()) - return; - - var positions = ComputeLayoutPositions().ToArray(); - - int i = 0; - foreach (var d in FlowingChildren) - { - if (i > positions.Length) - throw new InvalidOperationException( - $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); - - // In some cases (see the right hand side of the conditional) we want to permit relatively sized children - // in our flow direction; specifically, when children use FillMode.Fit to preserve the aspect ratio. - // Consider the following use case: A flow container has a fixed width but an automatic height, and flows - // in the vertical direction. Now, we can add relatively sized children with FillMode.Fit to make sure their - // aspect ratio is preserved while still allowing them to flow vertically. This special case can not result - // in an autosize-related feedback loop, and we can thus simply allow it. - if ((d.RelativeSizeAxes & AutoSizeAxes) != 0 && (d.FillMode != FillMode.Fit || d.RelativeSizeAxes != Axes.Both || d.Size.X > RelativeChildSize.X || d.Size.Y > RelativeChildSize.Y || AutoSizeAxes == Axes.Both)) - throw new InvalidOperationException( - "Drawables inside a flow container may not have a relative size axis that the flow container is auto sizing for." + - $"The flow container is set to autosize in {AutoSizeAxes} axes and the child is set to relative size in {d.RelativeSizeAxes} axes."); - - if (d.RelativePositionAxes != Axes.None) - throw new InvalidOperationException($"A flow container cannot contain a child with relative positioning (it is {d.RelativePositionAxes})."); - - var finalPos = positions[i]; - if (d.Position != finalPos) - d.TransformTo(d.PopulateTransform(new FlowTransform { Rewindable = false }, finalPos, LayoutDuration, LayoutEasing)); - - ++i; - } - - if (i != positions.Length) - throw new InvalidOperationException( - $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!layout.IsValid) - { - performLayout(); - layout.Validate(); - } - } - - private class FlowTransform : TransformCustom - { - public FlowTransform() - : base(nameof(Position)) - { - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using osu.Framework.Caching; +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics.Transforms; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container that can be used to fluently arrange its children. + /// + public abstract class FlowContainer : Container + where T : Drawable + { + internal event Action OnLayout; + + /// + /// The easing that should be used when children are moved to their position in the layout. + /// + public Easing LayoutEasing + { + get { return AutoSizeEasing; } + set { AutoSizeEasing = value; } + } + + /// + /// The time it should take to move a child from its current position to its new layout position. + /// + public float LayoutDuration + { + get { return AutoSizeDuration * 2; } + set + { + //coupling with autosizeduration allows us to smoothly transition our size + //when no children are left to dictate autosize. + AutoSizeDuration = value / 2; + } + } + + private Cached layout = new Cached(); + + protected void InvalidateLayout() => layout.Invalidate(); + + private Vector2 maximumSize; + + /// + /// Optional maximum dimensions for this container. Note that the meaning of this value can change + /// depending on the implementation. + /// + public Vector2 MaximumSize + { + get { return maximumSize; } + set + { + if (maximumSize == value) return; + + maximumSize = value; + Invalidate(Invalidation.DrawSize); + } + } + + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || !layout.IsValid; + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawSize) > 0) + InvalidateLayout(); + + return base.Invalidate(invalidation, source, shallPropagate); + } + + private readonly Dictionary layoutChildren = new Dictionary(); + + protected internal override void AddInternal(Drawable drawable) + { + layoutChildren.Add(drawable, 0f); + // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate + // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. + InvalidateLayout(); + base.AddInternal(drawable); + } + + protected internal override bool RemoveInternal(Drawable drawable) + { + layoutChildren.Remove(drawable); + // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate + // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. + InvalidateLayout(); + return base.RemoveInternal(drawable); + } + + protected internal override void ClearInternal(bool disposeChildren = true) + { + layoutChildren.Clear(); + // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate + // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. + InvalidateLayout(); + base.ClearInternal(disposeChildren); + } + + /// + /// Changes the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). + /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . + /// + /// The drawable whose position should be changed, must be a child of this container. + /// The new position in the layout the drawable should have. + public void SetLayoutPosition(Drawable drawable, float newPosition) + { + if (!layoutChildren.ContainsKey(drawable)) + throw new InvalidOperationException($"Cannot change layout position of drawable which is not contained within this {nameof(FlowContainer)}."); + layoutChildren[drawable] = newPosition; + InvalidateLayout(); + } + + /// + /// Gets the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). + /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . + /// + /// The drawable whose position should be retrieved, must be a child of this container. + /// The position of the drawable in the layout. + public float GetLayoutPosition(Drawable drawable) + { + if (!layoutChildren.ContainsKey(drawable)) + throw new InvalidOperationException($"Cannot get layout position of drawable which is not contained within this {nameof(FlowContainer)}."); + + return layoutChildren[drawable]; + } + + protected override bool UpdateChildrenLife() + { + bool changed = base.UpdateChildrenLife(); + + if (changed) + InvalidateLayout(); + + return changed; + } + + public override void InvalidateFromChild(Invalidation invalidation) + { + //Colour captures potential changes in IsPresent. If this ever becomes a bottleneck, + //Invalidation could be further separated into presence changes. + if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) + InvalidateLayout(); + + base.InvalidateFromChild(invalidation); + } + + /// + /// Gets the children that appear in the flow of this in the order in which they are processed within the flowing layout. + /// + public virtual IEnumerable FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => layoutChildren[d]).ThenBy(d => d.ChildID); + + protected abstract IEnumerable ComputeLayoutPositions(); + + private void performLayout() + { + OnLayout?.Invoke(); + + if (!Children.Any()) + return; + + var positions = ComputeLayoutPositions().ToArray(); + + int i = 0; + foreach (var d in FlowingChildren) + { + if (i > positions.Length) + throw new InvalidOperationException( + $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); + + // In some cases (see the right hand side of the conditional) we want to permit relatively sized children + // in our flow direction; specifically, when children use FillMode.Fit to preserve the aspect ratio. + // Consider the following use case: A flow container has a fixed width but an automatic height, and flows + // in the vertical direction. Now, we can add relatively sized children with FillMode.Fit to make sure their + // aspect ratio is preserved while still allowing them to flow vertically. This special case can not result + // in an autosize-related feedback loop, and we can thus simply allow it. + if ((d.RelativeSizeAxes & AutoSizeAxes) != 0 && (d.FillMode != FillMode.Fit || d.RelativeSizeAxes != Axes.Both || d.Size.X > RelativeChildSize.X || d.Size.Y > RelativeChildSize.Y || AutoSizeAxes == Axes.Both)) + throw new InvalidOperationException( + "Drawables inside a flow container may not have a relative size axis that the flow container is auto sizing for." + + $"The flow container is set to autosize in {AutoSizeAxes} axes and the child is set to relative size in {d.RelativeSizeAxes} axes."); + + if (d.RelativePositionAxes != Axes.None) + throw new InvalidOperationException($"A flow container cannot contain a child with relative positioning (it is {d.RelativePositionAxes})."); + + var finalPos = positions[i]; + if (d.Position != finalPos) + d.TransformTo(d.PopulateTransform(new FlowTransform { Rewindable = false }, finalPos, LayoutDuration, LayoutEasing)); + + ++i; + } + + if (i != positions.Length) + throw new InvalidOperationException( + $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!layout.IsValid) + { + performLayout(); + layout.Validate(); + } + } + + private class FlowTransform : TransformCustom + { + public FlowTransform() + : base(nameof(Position)) + { + } + } + } +} diff --git a/osu.Framework/Graphics/Containers/FocusedOverlayContainer.cs b/osu.Framework/Graphics/Containers/FocusedOverlayContainer.cs index 89a07da01..65237a8c3 100644 --- a/osu.Framework/Graphics/Containers/FocusedOverlayContainer.cs +++ b/osu.Framework/Graphics/Containers/FocusedOverlayContainer.cs @@ -1,44 +1,44 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Input; -using OpenTK.Input; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// An overlay container that eagerly holds keyboard focus. - /// - public abstract class FocusedOverlayContainer : OverlayContainer - { - public override bool RequestsFocus => State == Visibility.Visible; - - public override bool AcceptsFocus => true; - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (HasFocus && State == Visibility.Visible && !args.Repeat) - { - switch (args.Key) - { - case Key.Escape: - Hide(); - return true; - } - } - - return base.OnKeyDown(state, args); - } - - protected override void PopIn() - { - Schedule(() => GetContainingInputManager().TriggerFocusContention(this)); - } - - protected override void PopOut() - { - if (HasFocus) - GetContainingInputManager().ChangeFocus(null); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Input; +using OpenTK.Input; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// An overlay container that eagerly holds keyboard focus. + /// + public abstract class FocusedOverlayContainer : OverlayContainer + { + public override bool RequestsFocus => State == Visibility.Visible; + + public override bool AcceptsFocus => true; + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (HasFocus && State == Visibility.Visible && !args.Repeat) + { + switch (args.Key) + { + case Key.Escape: + Hide(); + return true; + } + } + + return base.OnKeyDown(state, args); + } + + protected override void PopIn() + { + Schedule(() => GetContainingInputManager().TriggerFocusContention(this)); + } + + protected override void PopOut() + { + if (HasFocus) + GetContainingInputManager().ChangeFocus(null); + } + } +} diff --git a/osu.Framework/Graphics/Containers/GridContainer.cs b/osu.Framework/Graphics/Containers/GridContainer.cs index fcbc4f0e4..f9c2e1442 100644 --- a/osu.Framework/Graphics/Containers/GridContainer.cs +++ b/osu.Framework/Graphics/Containers/GridContainer.cs @@ -1,342 +1,342 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Allocation; -using OpenTK; -using osu.Framework.Caching; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container which allows laying out s in a grid. - /// - public class GridContainer : CompositeDrawable - { - private Drawable[][] content; - /// - /// The content of this , arranged in a 2D grid array, where each array - /// of s represents a row and each element of that array represents a column. - /// - /// Null elements are allowed to represent blank rows/cells. - /// - /// - public Drawable[][] Content - { - get { return content; } - set - { - if (content == value) - return; - content = value; - - cellContent.Invalidate(); - } - } - - private Dimension[] rowDimensions; - /// - /// Explicit dimensions for rows. Each index of this array applies to the respective row index inside . - /// - public Dimension[] RowDimensions - { - set - { - if (rowDimensions == value) - return; - rowDimensions = value; - - cellLayout.Invalidate(); - } - } - - private Dimension[] columnDimensions; - /// - /// Explicit dimensions for columns. Each index of this array applies to the respective column index inside . - /// - public Dimension[] ColumnDimensions - { - set - { - if (columnDimensions == value) - return; - columnDimensions = value; - - cellLayout.Invalidate(); - } - } - - [BackgroundDependencyLoader] - private void load() - { - layoutContent(); - } - - protected override void Update() - { - base.Update(); - - layoutContent(); - layoutCells(); - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit)) > 0) - cellLayout.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - - public override void InvalidateFromChild(Invalidation invalidation) - { - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - cellLayout.Invalidate(); - - base.InvalidateFromChild(invalidation); - } - - private Cached cellContent = new Cached(); - private Cached cellLayout = new Cached(); - - private CellContainer[,] cells = new CellContainer[0, 0]; - private int cellRows => cells.GetLength(0); - private int cellColumns => cells.GetLength(1); - - /// - /// Moves content from into cells. - /// - private void layoutContent() - { - if (cellContent.IsValid) - return; - - int requiredRows = Content?.Length ?? 0; - int requiredColumns = requiredRows == 0 ? 0 : Content.Max(c => c?.Length ?? 0); - - // Clear cell containers without disposing, as the content might be reused - foreach (var cell in cells) - cell.Clear(false); - - // It's easier to just re-construct the cell containers instead of resizing - // If this becomes a bottleneck we can transition to using lists, but this keeps the structure clean... - ClearInternal(); - cellLayout.Invalidate(); - - // Create the new cell containers and add content - cells = new CellContainer[requiredRows, requiredColumns]; - for (int r = 0; r < cellRows; r++) - for (int c = 0; c < cellColumns; c++) - { - // Add cell - cells[r, c] = new CellContainer(); - - // Allow empty rows - if (Content[r] == null) - continue; - - // Allow non-square grids - if (c >= Content[r].Length) - continue; - - // Allow empty cells - if (Content[r][c] == null) - continue; - - // Add content - cells[r, c].Add(Content[r][c]); - cells[r, c].Depth = Content[r][c].Depth; - - AddInternal(cells[r, c]); - } - - cellContent.Validate(); - } - - /// - /// Repositions/resizes cells. - /// - private void layoutCells() - { - if (cellLayout.IsValid) - return; - - foreach (var cell in cells) - { - cell.IsWidthDefined = false; - cell.IsHeightDefined = false; - } - - int autoSizedRows = cellRows; - int autoSizedColumns = cellColumns; - - float definedWidth = 0; - float definedHeight = 0; - - // Compute the width of explicitly-defined columns - if (columnDimensions?.Length > 0) - { - for (int i = 0; i < columnDimensions.Length; i++) - { - if (i >= cellColumns) - continue; - - var d = columnDimensions[i]; - - float cellWidth = 0; - switch (d.Mode) - { - case GridSizeMode.Distributed: - continue; - case GridSizeMode.Relative: - cellWidth = d.Size * DrawWidth; - break; - case GridSizeMode.Absolute: - cellWidth = d.Size; - break; - case GridSizeMode.AutoSize: - for (int r = 0; r < cellRows; r++) - cellWidth = Math.Max(cellWidth, Content[r]?[i]?.DrawWidth ?? 0); - break; - } - - for (int r = 0; r < cellRows; r++) - { - cells[r, i].Width = cellWidth; - cells[r, i].IsWidthDefined = true; - } - - definedWidth += cellWidth; - autoSizedColumns--; - } - } - - // Compute the height of explicitly-defined rows - if (rowDimensions?.Length > 0) - { - for (int i = 0; i < rowDimensions.Length; i++) - { - if (i >= cellRows) - continue; - - var d = rowDimensions[i]; - - float cellHeight = 0; - switch (d.Mode) - { - case GridSizeMode.Distributed: - continue; - case GridSizeMode.Relative: - cellHeight = d.Size * DrawHeight; - break; - case GridSizeMode.Absolute: - cellHeight = d.Size; - break; - case GridSizeMode.AutoSize: - for (int c = 0; c < cellColumns; c++) - cellHeight = Math.Max(cellHeight, Content[i]?[c]?.DrawHeight ?? 0); - break; - } - - for (int c = 0; c < cellColumns; c++) - { - cells[i, c].IsHeightDefined = true; - cells[i, c].Height = cellHeight; - } - - definedHeight += cellHeight; - autoSizedRows--; - } - } - - // Compute the size of non-explicitly defined rows/columns that should fill the remaining area - var autoSize = new Vector2 - ( - Math.Max(0, DrawWidth - definedWidth) / autoSizedColumns, - Math.Max(0, DrawHeight - definedHeight) / autoSizedRows - ); - - // Add sizing to non-explicitly-defined columns and add positional offsets - for (int r = 0; r < cellRows; r++) - for (int c = 0; c < cellColumns; c++) - { - if (!cells[r, c].IsWidthDefined) - cells[r, c].Width = autoSize.X; - if (!cells[r, c].IsHeightDefined) - cells[r, c].Height = autoSize.Y; - - if (c > 0) - cells[r, c].X = cells[r, c - 1].X + cells[r, c - 1].Width; - if (r > 0) - cells[r, c].Y = cells[r - 1, c].Y + cells[r - 1, c].Height; - } - - cellLayout.Validate(); - } - - /// - /// Represents one cell of the . - /// - private class CellContainer : Container - { - /// - /// Whether this has an explicitly-defined width. - /// - public bool IsWidthDefined; - - /// - /// Whether this has an explicitly-defined height. - /// - public bool IsHeightDefined; - } - } - - /// - /// Defines the size of a row or column in a . - /// - public struct Dimension - { - /// - /// The mode in which this row or column is sized. - /// - public GridSizeMode Mode { get; private set; } - - /// - /// The size of the row or column which this applies to. - /// - public float Size { get; private set; } - - /// - /// Constructs a new . - /// - /// The sizing mode to use. - /// The size of this row or column. This only has an effect if is not . - public Dimension(GridSizeMode mode = GridSizeMode.Distributed, float size = 0) - { - Mode = mode; - Size = size; - } - } - - public enum GridSizeMode - { - /// - /// Any remaining area of the will be divided amongst this and all - /// other elements which use . - /// - Distributed, - /// - /// This element should be sized relative to the dimensions of the . - /// - Relative, - /// - /// This element has a size independent of the . - /// - Absolute, - /// - /// This element will be sized to the maximum size along its span. - /// - AutoSize - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using OpenTK; +using osu.Framework.Caching; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which allows laying out s in a grid. + /// + public class GridContainer : CompositeDrawable + { + private Drawable[][] content; + /// + /// The content of this , arranged in a 2D grid array, where each array + /// of s represents a row and each element of that array represents a column. + /// + /// Null elements are allowed to represent blank rows/cells. + /// + /// + public Drawable[][] Content + { + get { return content; } + set + { + if (content == value) + return; + content = value; + + cellContent.Invalidate(); + } + } + + private Dimension[] rowDimensions; + /// + /// Explicit dimensions for rows. Each index of this array applies to the respective row index inside . + /// + public Dimension[] RowDimensions + { + set + { + if (rowDimensions == value) + return; + rowDimensions = value; + + cellLayout.Invalidate(); + } + } + + private Dimension[] columnDimensions; + /// + /// Explicit dimensions for columns. Each index of this array applies to the respective column index inside . + /// + public Dimension[] ColumnDimensions + { + set + { + if (columnDimensions == value) + return; + columnDimensions = value; + + cellLayout.Invalidate(); + } + } + + [BackgroundDependencyLoader] + private void load() + { + layoutContent(); + } + + protected override void Update() + { + base.Update(); + + layoutContent(); + layoutCells(); + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit)) > 0) + cellLayout.Invalidate(); + + return base.Invalidate(invalidation, source, shallPropagate); + } + + public override void InvalidateFromChild(Invalidation invalidation) + { + if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) + cellLayout.Invalidate(); + + base.InvalidateFromChild(invalidation); + } + + private Cached cellContent = new Cached(); + private Cached cellLayout = new Cached(); + + private CellContainer[,] cells = new CellContainer[0, 0]; + private int cellRows => cells.GetLength(0); + private int cellColumns => cells.GetLength(1); + + /// + /// Moves content from into cells. + /// + private void layoutContent() + { + if (cellContent.IsValid) + return; + + int requiredRows = Content?.Length ?? 0; + int requiredColumns = requiredRows == 0 ? 0 : Content.Max(c => c?.Length ?? 0); + + // Clear cell containers without disposing, as the content might be reused + foreach (var cell in cells) + cell.Clear(false); + + // It's easier to just re-construct the cell containers instead of resizing + // If this becomes a bottleneck we can transition to using lists, but this keeps the structure clean... + ClearInternal(); + cellLayout.Invalidate(); + + // Create the new cell containers and add content + cells = new CellContainer[requiredRows, requiredColumns]; + for (int r = 0; r < cellRows; r++) + for (int c = 0; c < cellColumns; c++) + { + // Add cell + cells[r, c] = new CellContainer(); + + // Allow empty rows + if (Content[r] == null) + continue; + + // Allow non-square grids + if (c >= Content[r].Length) + continue; + + // Allow empty cells + if (Content[r][c] == null) + continue; + + // Add content + cells[r, c].Add(Content[r][c]); + cells[r, c].Depth = Content[r][c].Depth; + + AddInternal(cells[r, c]); + } + + cellContent.Validate(); + } + + /// + /// Repositions/resizes cells. + /// + private void layoutCells() + { + if (cellLayout.IsValid) + return; + + foreach (var cell in cells) + { + cell.IsWidthDefined = false; + cell.IsHeightDefined = false; + } + + int autoSizedRows = cellRows; + int autoSizedColumns = cellColumns; + + float definedWidth = 0; + float definedHeight = 0; + + // Compute the width of explicitly-defined columns + if (columnDimensions?.Length > 0) + { + for (int i = 0; i < columnDimensions.Length; i++) + { + if (i >= cellColumns) + continue; + + var d = columnDimensions[i]; + + float cellWidth = 0; + switch (d.Mode) + { + case GridSizeMode.Distributed: + continue; + case GridSizeMode.Relative: + cellWidth = d.Size * DrawWidth; + break; + case GridSizeMode.Absolute: + cellWidth = d.Size; + break; + case GridSizeMode.AutoSize: + for (int r = 0; r < cellRows; r++) + cellWidth = Math.Max(cellWidth, Content[r]?[i]?.DrawWidth ?? 0); + break; + } + + for (int r = 0; r < cellRows; r++) + { + cells[r, i].Width = cellWidth; + cells[r, i].IsWidthDefined = true; + } + + definedWidth += cellWidth; + autoSizedColumns--; + } + } + + // Compute the height of explicitly-defined rows + if (rowDimensions?.Length > 0) + { + for (int i = 0; i < rowDimensions.Length; i++) + { + if (i >= cellRows) + continue; + + var d = rowDimensions[i]; + + float cellHeight = 0; + switch (d.Mode) + { + case GridSizeMode.Distributed: + continue; + case GridSizeMode.Relative: + cellHeight = d.Size * DrawHeight; + break; + case GridSizeMode.Absolute: + cellHeight = d.Size; + break; + case GridSizeMode.AutoSize: + for (int c = 0; c < cellColumns; c++) + cellHeight = Math.Max(cellHeight, Content[i]?[c]?.DrawHeight ?? 0); + break; + } + + for (int c = 0; c < cellColumns; c++) + { + cells[i, c].IsHeightDefined = true; + cells[i, c].Height = cellHeight; + } + + definedHeight += cellHeight; + autoSizedRows--; + } + } + + // Compute the size of non-explicitly defined rows/columns that should fill the remaining area + var autoSize = new Vector2 + ( + Math.Max(0, DrawWidth - definedWidth) / autoSizedColumns, + Math.Max(0, DrawHeight - definedHeight) / autoSizedRows + ); + + // Add sizing to non-explicitly-defined columns and add positional offsets + for (int r = 0; r < cellRows; r++) + for (int c = 0; c < cellColumns; c++) + { + if (!cells[r, c].IsWidthDefined) + cells[r, c].Width = autoSize.X; + if (!cells[r, c].IsHeightDefined) + cells[r, c].Height = autoSize.Y; + + if (c > 0) + cells[r, c].X = cells[r, c - 1].X + cells[r, c - 1].Width; + if (r > 0) + cells[r, c].Y = cells[r - 1, c].Y + cells[r - 1, c].Height; + } + + cellLayout.Validate(); + } + + /// + /// Represents one cell of the . + /// + private class CellContainer : Container + { + /// + /// Whether this has an explicitly-defined width. + /// + public bool IsWidthDefined; + + /// + /// Whether this has an explicitly-defined height. + /// + public bool IsHeightDefined; + } + } + + /// + /// Defines the size of a row or column in a . + /// + public struct Dimension + { + /// + /// The mode in which this row or column is sized. + /// + public GridSizeMode Mode { get; private set; } + + /// + /// The size of the row or column which this applies to. + /// + public float Size { get; private set; } + + /// + /// Constructs a new . + /// + /// The sizing mode to use. + /// The size of this row or column. This only has an effect if is not . + public Dimension(GridSizeMode mode = GridSizeMode.Distributed, float size = 0) + { + Mode = mode; + Size = size; + } + } + + public enum GridSizeMode + { + /// + /// Any remaining area of the will be divided amongst this and all + /// other elements which use . + /// + Distributed, + /// + /// This element should be sized relative to the dimensions of the . + /// + Relative, + /// + /// This element has a size independent of the . + /// + Absolute, + /// + /// This element will be sized to the maximum size along its span. + /// + AutoSize + } +} diff --git a/osu.Framework/Graphics/Containers/IBufferedContainer.cs b/osu.Framework/Graphics/Containers/IBufferedContainer.cs index d65e5203f..4e5276a24 100644 --- a/osu.Framework/Graphics/Containers/IBufferedContainer.cs +++ b/osu.Framework/Graphics/Containers/IBufferedContainer.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Graphics.Containers -{ - public interface IBufferedContainer : IContainer - { - Vector2 BlurSigma { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Graphics.Containers +{ + public interface IBufferedContainer : IContainer + { + Vector2 BlurSigma { get; set; } + } +} diff --git a/osu.Framework/Graphics/Containers/IContainer.cs b/osu.Framework/Graphics/Containers/IContainer.cs index 048d9e06d..01efde89d 100644 --- a/osu.Framework/Graphics/Containers/IContainer.cs +++ b/osu.Framework/Graphics/Containers/IContainer.cs @@ -1,38 +1,38 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Containers -{ - public interface IContainer : IDrawable - { - EdgeEffectParameters EdgeEffect { get; set; } - - Vector2 RelativeChildSize { get; set; } - - Vector2 RelativeChildOffset { get; set; } - } - - public interface IContainerEnumerable : IContainer - where T : IDrawable - { - IReadOnlyList Children { get; } - - int RemoveAll(Predicate match); - } - - public interface IContainerCollection : IContainer - where T : IDrawable - { - IReadOnlyList Children { set; } - - void Add(T drawable); - void AddRange(IEnumerable collection); - - bool Remove(T drawable); - void RemoveRange(IEnumerable range); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Containers +{ + public interface IContainer : IDrawable + { + EdgeEffectParameters EdgeEffect { get; set; } + + Vector2 RelativeChildSize { get; set; } + + Vector2 RelativeChildOffset { get; set; } + } + + public interface IContainerEnumerable : IContainer + where T : IDrawable + { + IReadOnlyList Children { get; } + + int RemoveAll(Predicate match); + } + + public interface IContainerCollection : IContainer + where T : IDrawable + { + IReadOnlyList Children { set; } + + void Add(T drawable); + void AddRange(IEnumerable collection); + + bool Remove(T drawable); + void RemoveRange(IEnumerable range); + } +} diff --git a/osu.Framework/Graphics/Containers/IFillFlowContainer.cs b/osu.Framework/Graphics/Containers/IFillFlowContainer.cs index 72a3a9286..98906e1c3 100644 --- a/osu.Framework/Graphics/Containers/IFillFlowContainer.cs +++ b/osu.Framework/Graphics/Containers/IFillFlowContainer.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Graphics.Containers -{ - public interface IFillFlowContainer : IContainer - { - Vector2 Spacing { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Graphics.Containers +{ + public interface IFillFlowContainer : IContainer + { + Vector2 Spacing { get; set; } + } +} diff --git a/osu.Framework/Graphics/Containers/IFilterable.cs b/osu.Framework/Graphics/Containers/IFilterable.cs index 578cddbd6..d95ee71c8 100644 --- a/osu.Framework/Graphics/Containers/IFilterable.cs +++ b/osu.Framework/Graphics/Containers/IFilterable.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Containers -{ - public interface IFilterable : IHasFilterTerms - { - /// - /// Whether the current object is matching (ie. visible) given the current filter criteria of a parent. - /// - bool MatchingFilter { set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Containers +{ + public interface IFilterable : IHasFilterTerms + { + /// + /// Whether the current object is matching (ie. visible) given the current filter criteria of a parent. + /// + bool MatchingFilter { set; } + } +} diff --git a/osu.Framework/Graphics/Containers/IHasFilterTerms.cs b/osu.Framework/Graphics/Containers/IHasFilterTerms.cs index adcf3e5fe..73229013a 100644 --- a/osu.Framework/Graphics/Containers/IHasFilterTerms.cs +++ b/osu.Framework/Graphics/Containers/IHasFilterTerms.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// An interface to expose a number of keywords with the intent of helping a parent filter results. - /// See for an interface which adds a callback on matching keywords. - /// - public interface IHasFilterTerms - { - /// - /// An enumerator of relevant terms which match the current object in a filtered scenario. - /// - IEnumerable FilterTerms { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// An interface to expose a number of keywords with the intent of helping a parent filter results. + /// See for an interface which adds a callback on matching keywords. + /// + public interface IHasFilterTerms + { + /// + /// An enumerator of relevant terms which match the current object in a filtered scenario. + /// + IEnumerable FilterTerms { get; } + } +} diff --git a/osu.Framework/Graphics/Containers/IHasFilterableChildren.cs b/osu.Framework/Graphics/Containers/IHasFilterableChildren.cs index e9da9ca8c..e9ef8def0 100644 --- a/osu.Framework/Graphics/Containers/IHasFilterableChildren.cs +++ b/osu.Framework/Graphics/Containers/IHasFilterableChildren.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Containers -{ - public interface IHasFilterableChildren : IFilterable - { - /// - /// List of children that can be filtered - /// - IEnumerable FilterableChildren { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Containers +{ + public interface IHasFilterableChildren : IFilterable + { + /// + /// List of children that can be filtered + /// + IEnumerable FilterableChildren { get; } + } +} diff --git a/osu.Framework/Graphics/Containers/OverlayContainer.cs b/osu.Framework/Graphics/Containers/OverlayContainer.cs index 13c97cdb8..24725e85a 100644 --- a/osu.Framework/Graphics/Containers/OverlayContainer.cs +++ b/osu.Framework/Graphics/Containers/OverlayContainer.cs @@ -1,49 +1,49 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Input; -using OpenTK; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// An element which starts hidden and can be toggled to visible. - /// - public abstract class OverlayContainer : VisibilityContainer - { - /// - /// Whether we should block any mouse input from interacting with things behind us. - /// - protected virtual bool BlockPassThroughMouse => true; - - /// - /// Whether we should block any keyboard input from interacting with things behind us. - /// - protected virtual bool BlockPassThroughKeyboard => false; - - internal override bool BuildKeyboardInputQueue(List queue) - { - if (CanReceiveKeyboardInput && BlockPassThroughKeyboard) - { - // when blocking keyboard input behind us, we still want to make sure the global handlers receive events - // but we don't want other drawables behind us handling them. - queue.RemoveAll(d => !(d is IHandleGlobalInput)); - } - - return base.BuildKeyboardInputQueue(queue); - } - - internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) - { - if (CanReceiveMouseInput && BlockPassThroughMouse && ReceiveMouseInputAt(screenSpaceMousePos)) - { - // when blocking mouse input behind us, we still want to make sure the global handlers receive events - // but we don't want other drawables behind us handling them. - queue.RemoveAll(d => !(d is IHandleGlobalInput)); - } - - return base.BuildMouseInputQueue(screenSpaceMousePos, queue); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input; +using OpenTK; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// An element which starts hidden and can be toggled to visible. + /// + public abstract class OverlayContainer : VisibilityContainer + { + /// + /// Whether we should block any mouse input from interacting with things behind us. + /// + protected virtual bool BlockPassThroughMouse => true; + + /// + /// Whether we should block any keyboard input from interacting with things behind us. + /// + protected virtual bool BlockPassThroughKeyboard => false; + + internal override bool BuildKeyboardInputQueue(List queue) + { + if (CanReceiveKeyboardInput && BlockPassThroughKeyboard) + { + // when blocking keyboard input behind us, we still want to make sure the global handlers receive events + // but we don't want other drawables behind us handling them. + queue.RemoveAll(d => !(d is IHandleGlobalInput)); + } + + return base.BuildKeyboardInputQueue(queue); + } + + internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) + { + if (CanReceiveMouseInput && BlockPassThroughMouse && ReceiveMouseInputAt(screenSpaceMousePos)) + { + // when blocking mouse input behind us, we still want to make sure the global handlers receive events + // but we don't want other drawables behind us handling them. + queue.RemoveAll(d => !(d is IHandleGlobalInput)); + } + + return base.BuildMouseInputQueue(screenSpaceMousePos, queue); + } + } +} diff --git a/osu.Framework/Graphics/Containers/ScrollContainer.cs b/osu.Framework/Graphics/Containers/ScrollContainer.cs index d8d1938a9..7ca3db85d 100644 --- a/osu.Framework/Graphics/Containers/ScrollContainer.cs +++ b/osu.Framework/Graphics/Containers/ScrollContainer.cs @@ -1,574 +1,574 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using osu.Framework.Input; -using osu.Framework.MathUtils; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; -using OpenTK.Input; - -namespace osu.Framework.Graphics.Containers -{ - public class ScrollContainer : ScrollContainer - { - /// - /// Creates a scroll container. - /// - /// The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical. - public ScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) - { - } - } - - public class ScrollContainer : Container, DelayedLoadWrapper.IOnScreenOptimisingContainer - where T : Drawable - { - /// - /// Determines whether the scroll dragger appears on the left side. If not, then it always appears on the right side. - /// - public Anchor ScrollbarAnchor - { - get { return Scrollbar.Anchor; } - - set - { - Scrollbar.Anchor = value; - Scrollbar.Origin = value; - updatePadding(); - } - } - - private bool scrollbarVisible = true; - - /// - /// Whether the scrollbar is visible. - /// - public bool ScrollbarVisible - { - get { return scrollbarVisible; } - set - { - scrollbarVisible = value; - updateScrollbar(); - } - } - - private readonly Container content; - - protected readonly ScrollbarContainer Scrollbar; - - private bool scrollbarOverlapsContent = true; - - /// - /// Whether the scrollbar overlaps the content or resides in its own padded space. - /// - public bool ScrollbarOverlapsContent - { - get { return scrollbarOverlapsContent; } - set - { - scrollbarOverlapsContent = value; - updatePadding(); - } - } - - - /// - /// Size of available content (i.e. everything that can be scrolled to) in the scroll direction. - /// - private float availableContent => content.DrawSize[ScrollDim]; - - /// - /// Size of the viewport in the scroll direction. - /// - private float displayableContent => ChildSize[ScrollDim]; - - /// - /// Controls the distance scrolled when turning the mouse wheel a single notch. - /// - public float MouseWheelScrollDistance = 80; - - /// - /// This limits how far out of clamping bounds we allow the target position to be at most. - /// Effectively, larger values result in bouncier behavior as the scroll boundaries are approached - /// with high velocity. - /// - public float ClampExtension = 500; - - /// - /// This corresponds to the clamping force. A larger value means more aggressive clamping. Default is 0.012. - /// - private const double distance_decay_clamping = 0.012; - - /// - /// Controls the rate with which the target position is approached after ending a drag. Default is 0.0035. - /// - public double DistanceDecayDrag = 0.0035; - - /// - /// Controls the rate with which the target position is approached after using the mouse wheel. Default is 0.01 - /// - public double DistanceDecayWheel = 0.01; - - /// - /// Controls the rate with which the target position is approached after jumping to a specific location. Default is 0.01. - /// - public double DistanceDecayJump = 0.01; - - /// - /// Controls the rate with which the target position is approached. It is automatically set after - /// dragging or using the mouse wheel. - /// - private double distanceDecay; - - /// - /// The current scroll position. - /// - public float Current { get; private set; } - - /// - /// The target scroll position which is exponentially approached by current via a rate of distanceDecay. - /// - private float target; - - private float scrollableExtent => Math.Max(availableContent - displayableContent, 0); - - /// - /// Clamp a value to the available scroll range. - /// - /// The value to clamp. - /// An extension value beyond the normal extent. - /// - protected float Clamp(float position, float extension = 0) => MathHelper.Clamp(position, -extension, scrollableExtent + extension); - - protected override Container Content => content; - - /// - /// Whether we are currently scrolled as far as possible into the scroll direction. - /// - /// How close to the extent we need to be. - public bool IsScrolledToEnd(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(target, scrollableExtent, lenience); - - /// - /// The container holding all children which are getting scrolled around. - /// - public Container ScrollContent => content; - - protected virtual bool IsDragging { get; private set; } - - /// - /// The direction in which scrolling is supported. - /// - protected readonly Direction ScrollDirection; - - /// - /// The direction in which scrolling is supported, converted to an int for array index lookups. - /// - protected int ScrollDim => ScrollDirection == Direction.Horizontal ? 0 : 1; - - /// - /// Creates a scroll container. - /// - /// The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical. - public ScrollContainer(Direction scrollDirection = Direction.Vertical) - { - ScrollDirection = scrollDirection; - - Masking = true; - - Axes scrollAxis = scrollDirection == Direction.Horizontal ? Axes.X : Axes.Y; - AddRangeInternal(new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both & ~scrollAxis, - AutoSizeAxes = scrollAxis, - }, - Scrollbar = new ScrollbarContainer(scrollDirection) { Dragged = onScrollbarMovement } - }); - - ScrollbarAnchor = scrollDirection == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft; - } - - private float lastUpdateDisplayableContent = -1; - private float lastAvailableContent = -1; - - private void updateSize() - { - // ensure we only update scrollbar when something has changed, to avoid transform helpers resetting their transform every frame. - // also avoids creating many needless Transforms every update frame. - if (lastAvailableContent != availableContent || lastUpdateDisplayableContent != displayableContent) - { - lastAvailableContent = availableContent; - lastUpdateDisplayableContent = displayableContent; - updateScrollbar(); - } - } - - private void updateScrollbar() - { - Scrollbar.ResizeTo(Math.Min(1, availableContent > 0 ? displayableContent / availableContent : 0), 200, Easing.OutQuint); - Scrollbar.FadeTo(ScrollbarVisible && availableContent - 1 > displayableContent ? 1 : 0, 200); - updatePadding(); - } - - private void updatePadding() - { - if (scrollbarOverlapsContent || availableContent <= displayableContent) - content.Padding = new MarginPadding(); - else - { - if (ScrollDirection == Direction.Vertical) - { - content.Padding = ScrollbarAnchor == Anchor.TopLeft - ? new MarginPadding { Left = Scrollbar.Width + Scrollbar.Margin.Left } - : new MarginPadding { Right = Scrollbar.Width + Scrollbar.Margin.Right }; - } - else - { - content.Padding = ScrollbarAnchor == Anchor.TopLeft - ? new MarginPadding { Top = Scrollbar.Height + Scrollbar.Margin.Top } - : new MarginPadding { Bottom = Scrollbar.Height + Scrollbar.Margin.Bottom }; - } - } - } - - protected override bool OnDragStart(InputState state) - { - if (IsDragging || !state.Mouse.IsPressed(MouseButton.Left)) return false; - - lastDragTime = Time.Current; - averageDragDelta = averageDragTime = 0; - - IsDragging = true; - return true; - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - if (IsDragging || args.Button != MouseButton.Left) return false; - - // Continue from where we currently are scrolled to. - target = Current; - - return true; - } - - // We keep track of this because input events may happen at different intervals than update frames - // and we are interested in the time difference between drag _input_ events. - private double lastDragTime; - - // These keep track of a sliding average (w.r.t. time) of the time between drag events - // and the delta of drag events. Both of these moving averages are decayed at the same - // rate and thus the velocity remains constant across time. The overall magnitude - // of averageDragTime and averageDragDelta simple decreases such that more recent movements - // have a larger weight. - private double averageDragTime; - private double averageDragDelta; - - protected override bool OnDrag(InputState state) - { - Trace.Assert(IsDragging, "We should never receive OnDrag if we are not dragging."); - - double currentTime = Time.Current; - double timeDelta = currentTime - lastDragTime; - double decay = Math.Pow(0.95, timeDelta); - - averageDragTime = averageDragTime * decay + timeDelta; - averageDragDelta = averageDragDelta * decay - state.Mouse.Delta[ScrollDim]; - - lastDragTime = currentTime; - - Vector2 childDelta = ToLocalSpace(state.Mouse.NativeState.Position) - ToLocalSpace(state.Mouse.NativeState.LastPosition); - - float scrollOffset = -childDelta[ScrollDim]; - float clampedScrollOffset = Clamp(target + scrollOffset) - Clamp(target); - - Debug.Assert(Precision.AlmostBigger(Math.Abs(scrollOffset), clampedScrollOffset * Math.Sign(scrollOffset))); - - // If we are dragging past the extent of the scrollable area, half the offset - // such that the user can feel it. - scrollOffset = clampedScrollOffset + (scrollOffset - clampedScrollOffset) / 2; - - offset(scrollOffset, false); - return true; - } - - protected override bool OnDragEnd(InputState state) - { - Trace.Assert(IsDragging, "We should never receive OnDragEnd if we are not dragging."); - - IsDragging = false; - - if (averageDragTime <= 0.0) - return true; - - double velocity = averageDragDelta / averageDragTime; - - // Detect whether we halted at the end of the drag and in fact should _not_ - // perform a flick event. - const double velocity_cutoff = 0.1; - if (Math.Abs(Math.Pow(0.95, Time.Current - lastDragTime) * velocity) < velocity_cutoff) - velocity = 0; - - // Differentiate f(t) = distance * (1 - exp(-t)) w.r.t. "t" to obtain - // velocity w.r.t. time. Then rearrange to solve for distance given velocity. - double distance = velocity / (1 - Math.Exp(-DistanceDecayDrag)); - - offset((float)distance, true, DistanceDecayDrag); - - return true; - } - - protected override bool OnWheel(InputState state) - { - offset(-MouseWheelScrollDistance * state.Mouse.WheelDelta, true, DistanceDecayWheel); - return true; - } - - private void onScrollbarMovement(float value) => scrollTo(Clamp(value / Scrollbar.Size[ScrollDim]), false); - - /// - /// Immediately offsets the current and target scroll position. - /// - /// The scroll offset. - public void OffsetScrollPosition(float offset) - { - target += offset; - Current += offset; - } - - private void offset(float value, bool animated, double distanceDecay = float.PositiveInfinity) => scrollTo(target + value, animated, distanceDecay); - - /// - /// Scroll to the end of available content. - /// - /// Whether to animate the movement. - /// Whether we should interrupt a user's active drag. - public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) - { - if (!IsDragging || allowDuringDrag) - scrollTo(scrollableExtent, animated, DistanceDecayJump); - } - - /// - /// Scrolls to a new position relative to the current scroll offset. - /// - /// The amount by which we should scroll. - /// Whether to animate the movement. - public void ScrollBy(float offset, bool animated = true) => scrollTo(target + offset, animated); - - /// - /// Scrolls to an absolute position. - /// - /// The position to scroll to. - /// Whether to animate the movement. - /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . - public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); - - private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity) - { - target = value; - - if (animated) - this.distanceDecay = distanceDecay; - else - Current = target; - } - - /// - /// Scrolls a to the top. - /// - /// The to scroll to. - /// Whether to animate the movement. - public void ScrollTo(Drawable d, bool animated = true) => ScrollTo(GetChildPosInContent(d), animated); - - /// - /// Scrolls a into view. - /// - /// The to scroll into view. - /// Whether to animate the movement. - public void ScrollIntoView(Drawable d, bool animated = true) - { - float childPos0 = GetChildPosInContent(d); - float childPos1 = GetChildPosInContent(d, d.DrawSize); - - float minPos = Math.Min(childPos0, childPos1); - float maxPos = Math.Max(childPos0, childPos1); - - if (minPos < Current) - ScrollTo(minPos, animated); - else if (maxPos > Current + displayableContent) - ScrollTo(maxPos - displayableContent, animated); - } - - /// - /// Determines the position of a child in the content. - /// - /// The child to get the position from. - /// Positional offset in the child's space. - /// The position of the child. - public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, content)[ScrollDim]; - - /// - /// Determines the position of a child in the content. - /// - /// The child to get the position from. - /// The position of the child. - public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); - - private void updatePosition() - { - double localDistanceDecay = distanceDecay; - - // If we are not currently dragging the content, and we have scrolled out of bounds, - // then we should handle the clamping force. Note, that if the target is _within_ - // acceptable bounds, then we do not need special handling of the clamping force, as - // we will naturally scroll back into acceptable bounds. - if (!IsDragging && Current != Clamp(Current) && target != Clamp(target, -0.01f)) - { - // Firstly, we want to limit how far out the target may go to limit overly bouncy - // behaviour with extreme scroll velocities. - target = Clamp(target, ClampExtension); - - // Secondly, we would like to quickly approach the target while we are out of bounds. - // This is simulating a "strong" clamping force towards the target. - if (Current < target && target < 0 || Current > target && target > scrollableExtent) - localDistanceDecay = distance_decay_clamping * 2; - - // Lastly, we gradually nudge the target towards valid bounds. - target = (float)Interpolation.Lerp(Clamp(target), target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); - - float clampedTarget = Clamp(target); - if (Precision.AlmostEquals(clampedTarget, target)) - target = clampedTarget; - } - - // Exponential interpolation between the target and our current scroll position. - Current = (float)Interpolation.Lerp(target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); - - // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely. - if (Precision.AlmostEquals(Current, target)) - Current = target; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - updateSize(); - updatePosition(); - - if (ScrollDirection == Direction.Horizontal) - { - Scrollbar.X = Current * Scrollbar.Size.X; - content.X = -Current; - } - else - { - Scrollbar.Y = Current * Scrollbar.Size.Y; - content.Y = -Current; - } - } - - protected internal class ScrollbarContainer : Container - { - public Action Dragged; - - private readonly Color4 hoverColour = Color4.White; - private readonly Color4 defaultColour = Color4.Gray; - private readonly Color4 highlightColour = Color4.GreenYellow; - - private readonly Box box; - - private float dragOffset; - - private readonly int scrollDim; - - public ScrollbarContainer(Direction scrollDir) - { - scrollDim = (int)scrollDir; - RelativeSizeAxes = scrollDir == Direction.Horizontal ? Axes.X : Axes.Y; - Colour = defaultColour; - - Blending = BlendingMode.Additive; - - CornerRadius = 5; - - const float margin = 3; - - Margin = new MarginPadding - { - Left = scrollDir == Direction.Vertical ? margin : 0, - Right = scrollDir == Direction.Vertical ? margin : 0, - Top = scrollDir == Direction.Horizontal ? margin : 0, - Bottom = scrollDir == Direction.Horizontal ? margin : 0, - }; - - Masking = true; - - Child = box = new Box { RelativeSizeAxes = Axes.Both }; - - ResizeTo(1); - } - - public void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) - { - Vector2 size = new Vector2(10) - { - [scrollDim] = val - }; - this.ResizeTo(size, duration, easing); - } - - protected override bool OnClick(InputState state) => true; - - protected override bool OnHover(InputState state) - { - this.FadeColour(hoverColour, 100); - return true; - } - - protected override void OnHoverLost(InputState state) - { - this.FadeColour(defaultColour, 100); - } - - protected override bool OnDragStart(InputState state) - { - dragOffset = state.Mouse.Position[scrollDim] - Position[scrollDim]; - return true; - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - if (args.Button != MouseButton.Left) return false; - - //note that we are changing the colour of the box here as to not interfere with the hover effect. - box.FadeColour(highlightColour, 100); - - dragOffset = Position[scrollDim]; - Dragged?.Invoke(dragOffset); - return true; - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - if (args.Button != MouseButton.Left) return false; - - box.FadeColour(Color4.White, 100); - - return base.OnMouseUp(state, args); - } - - protected override bool OnDrag(InputState state) - { - Dragged?.Invoke(state.Mouse.Position[scrollDim] - dragOffset); - return true; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using OpenTK.Input; + +namespace osu.Framework.Graphics.Containers +{ + public class ScrollContainer : ScrollContainer + { + /// + /// Creates a scroll container. + /// + /// The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical. + public ScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) + { + } + } + + public class ScrollContainer : Container, DelayedLoadWrapper.IOnScreenOptimisingContainer + where T : Drawable + { + /// + /// Determines whether the scroll dragger appears on the left side. If not, then it always appears on the right side. + /// + public Anchor ScrollbarAnchor + { + get { return Scrollbar.Anchor; } + + set + { + Scrollbar.Anchor = value; + Scrollbar.Origin = value; + updatePadding(); + } + } + + private bool scrollbarVisible = true; + + /// + /// Whether the scrollbar is visible. + /// + public bool ScrollbarVisible + { + get { return scrollbarVisible; } + set + { + scrollbarVisible = value; + updateScrollbar(); + } + } + + private readonly Container content; + + protected readonly ScrollbarContainer Scrollbar; + + private bool scrollbarOverlapsContent = true; + + /// + /// Whether the scrollbar overlaps the content or resides in its own padded space. + /// + public bool ScrollbarOverlapsContent + { + get { return scrollbarOverlapsContent; } + set + { + scrollbarOverlapsContent = value; + updatePadding(); + } + } + + + /// + /// Size of available content (i.e. everything that can be scrolled to) in the scroll direction. + /// + private float availableContent => content.DrawSize[ScrollDim]; + + /// + /// Size of the viewport in the scroll direction. + /// + private float displayableContent => ChildSize[ScrollDim]; + + /// + /// Controls the distance scrolled when turning the mouse wheel a single notch. + /// + public float MouseWheelScrollDistance = 80; + + /// + /// This limits how far out of clamping bounds we allow the target position to be at most. + /// Effectively, larger values result in bouncier behavior as the scroll boundaries are approached + /// with high velocity. + /// + public float ClampExtension = 500; + + /// + /// This corresponds to the clamping force. A larger value means more aggressive clamping. Default is 0.012. + /// + private const double distance_decay_clamping = 0.012; + + /// + /// Controls the rate with which the target position is approached after ending a drag. Default is 0.0035. + /// + public double DistanceDecayDrag = 0.0035; + + /// + /// Controls the rate with which the target position is approached after using the mouse wheel. Default is 0.01 + /// + public double DistanceDecayWheel = 0.01; + + /// + /// Controls the rate with which the target position is approached after jumping to a specific location. Default is 0.01. + /// + public double DistanceDecayJump = 0.01; + + /// + /// Controls the rate with which the target position is approached. It is automatically set after + /// dragging or using the mouse wheel. + /// + private double distanceDecay; + + /// + /// The current scroll position. + /// + public float Current { get; private set; } + + /// + /// The target scroll position which is exponentially approached by current via a rate of distanceDecay. + /// + private float target; + + private float scrollableExtent => Math.Max(availableContent - displayableContent, 0); + + /// + /// Clamp a value to the available scroll range. + /// + /// The value to clamp. + /// An extension value beyond the normal extent. + /// + protected float Clamp(float position, float extension = 0) => MathHelper.Clamp(position, -extension, scrollableExtent + extension); + + protected override Container Content => content; + + /// + /// Whether we are currently scrolled as far as possible into the scroll direction. + /// + /// How close to the extent we need to be. + public bool IsScrolledToEnd(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(target, scrollableExtent, lenience); + + /// + /// The container holding all children which are getting scrolled around. + /// + public Container ScrollContent => content; + + protected virtual bool IsDragging { get; private set; } + + /// + /// The direction in which scrolling is supported. + /// + protected readonly Direction ScrollDirection; + + /// + /// The direction in which scrolling is supported, converted to an int for array index lookups. + /// + protected int ScrollDim => ScrollDirection == Direction.Horizontal ? 0 : 1; + + /// + /// Creates a scroll container. + /// + /// The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical. + public ScrollContainer(Direction scrollDirection = Direction.Vertical) + { + ScrollDirection = scrollDirection; + + Masking = true; + + Axes scrollAxis = scrollDirection == Direction.Horizontal ? Axes.X : Axes.Y; + AddRangeInternal(new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both & ~scrollAxis, + AutoSizeAxes = scrollAxis, + }, + Scrollbar = new ScrollbarContainer(scrollDirection) { Dragged = onScrollbarMovement } + }); + + ScrollbarAnchor = scrollDirection == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft; + } + + private float lastUpdateDisplayableContent = -1; + private float lastAvailableContent = -1; + + private void updateSize() + { + // ensure we only update scrollbar when something has changed, to avoid transform helpers resetting their transform every frame. + // also avoids creating many needless Transforms every update frame. + if (lastAvailableContent != availableContent || lastUpdateDisplayableContent != displayableContent) + { + lastAvailableContent = availableContent; + lastUpdateDisplayableContent = displayableContent; + updateScrollbar(); + } + } + + private void updateScrollbar() + { + Scrollbar.ResizeTo(Math.Min(1, availableContent > 0 ? displayableContent / availableContent : 0), 200, Easing.OutQuint); + Scrollbar.FadeTo(ScrollbarVisible && availableContent - 1 > displayableContent ? 1 : 0, 200); + updatePadding(); + } + + private void updatePadding() + { + if (scrollbarOverlapsContent || availableContent <= displayableContent) + content.Padding = new MarginPadding(); + else + { + if (ScrollDirection == Direction.Vertical) + { + content.Padding = ScrollbarAnchor == Anchor.TopLeft + ? new MarginPadding { Left = Scrollbar.Width + Scrollbar.Margin.Left } + : new MarginPadding { Right = Scrollbar.Width + Scrollbar.Margin.Right }; + } + else + { + content.Padding = ScrollbarAnchor == Anchor.TopLeft + ? new MarginPadding { Top = Scrollbar.Height + Scrollbar.Margin.Top } + : new MarginPadding { Bottom = Scrollbar.Height + Scrollbar.Margin.Bottom }; + } + } + } + + protected override bool OnDragStart(InputState state) + { + if (IsDragging || !state.Mouse.IsPressed(MouseButton.Left)) return false; + + lastDragTime = Time.Current; + averageDragDelta = averageDragTime = 0; + + IsDragging = true; + return true; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (IsDragging || args.Button != MouseButton.Left) return false; + + // Continue from where we currently are scrolled to. + target = Current; + + return true; + } + + // We keep track of this because input events may happen at different intervals than update frames + // and we are interested in the time difference between drag _input_ events. + private double lastDragTime; + + // These keep track of a sliding average (w.r.t. time) of the time between drag events + // and the delta of drag events. Both of these moving averages are decayed at the same + // rate and thus the velocity remains constant across time. The overall magnitude + // of averageDragTime and averageDragDelta simple decreases such that more recent movements + // have a larger weight. + private double averageDragTime; + private double averageDragDelta; + + protected override bool OnDrag(InputState state) + { + Trace.Assert(IsDragging, "We should never receive OnDrag if we are not dragging."); + + double currentTime = Time.Current; + double timeDelta = currentTime - lastDragTime; + double decay = Math.Pow(0.95, timeDelta); + + averageDragTime = averageDragTime * decay + timeDelta; + averageDragDelta = averageDragDelta * decay - state.Mouse.Delta[ScrollDim]; + + lastDragTime = currentTime; + + Vector2 childDelta = ToLocalSpace(state.Mouse.NativeState.Position) - ToLocalSpace(state.Mouse.NativeState.LastPosition); + + float scrollOffset = -childDelta[ScrollDim]; + float clampedScrollOffset = Clamp(target + scrollOffset) - Clamp(target); + + Debug.Assert(Precision.AlmostBigger(Math.Abs(scrollOffset), clampedScrollOffset * Math.Sign(scrollOffset))); + + // If we are dragging past the extent of the scrollable area, half the offset + // such that the user can feel it. + scrollOffset = clampedScrollOffset + (scrollOffset - clampedScrollOffset) / 2; + + offset(scrollOffset, false); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + Trace.Assert(IsDragging, "We should never receive OnDragEnd if we are not dragging."); + + IsDragging = false; + + if (averageDragTime <= 0.0) + return true; + + double velocity = averageDragDelta / averageDragTime; + + // Detect whether we halted at the end of the drag and in fact should _not_ + // perform a flick event. + const double velocity_cutoff = 0.1; + if (Math.Abs(Math.Pow(0.95, Time.Current - lastDragTime) * velocity) < velocity_cutoff) + velocity = 0; + + // Differentiate f(t) = distance * (1 - exp(-t)) w.r.t. "t" to obtain + // velocity w.r.t. time. Then rearrange to solve for distance given velocity. + double distance = velocity / (1 - Math.Exp(-DistanceDecayDrag)); + + offset((float)distance, true, DistanceDecayDrag); + + return true; + } + + protected override bool OnWheel(InputState state) + { + offset(-MouseWheelScrollDistance * state.Mouse.WheelDelta, true, DistanceDecayWheel); + return true; + } + + private void onScrollbarMovement(float value) => scrollTo(Clamp(value / Scrollbar.Size[ScrollDim]), false); + + /// + /// Immediately offsets the current and target scroll position. + /// + /// The scroll offset. + public void OffsetScrollPosition(float offset) + { + target += offset; + Current += offset; + } + + private void offset(float value, bool animated, double distanceDecay = float.PositiveInfinity) => scrollTo(target + value, animated, distanceDecay); + + /// + /// Scroll to the end of available content. + /// + /// Whether to animate the movement. + /// Whether we should interrupt a user's active drag. + public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) + { + if (!IsDragging || allowDuringDrag) + scrollTo(scrollableExtent, animated, DistanceDecayJump); + } + + /// + /// Scrolls to a new position relative to the current scroll offset. + /// + /// The amount by which we should scroll. + /// Whether to animate the movement. + public void ScrollBy(float offset, bool animated = true) => scrollTo(target + offset, animated); + + /// + /// Scrolls to an absolute position. + /// + /// The position to scroll to. + /// Whether to animate the movement. + /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . + public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); + + private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity) + { + target = value; + + if (animated) + this.distanceDecay = distanceDecay; + else + Current = target; + } + + /// + /// Scrolls a to the top. + /// + /// The to scroll to. + /// Whether to animate the movement. + public void ScrollTo(Drawable d, bool animated = true) => ScrollTo(GetChildPosInContent(d), animated); + + /// + /// Scrolls a into view. + /// + /// The to scroll into view. + /// Whether to animate the movement. + public void ScrollIntoView(Drawable d, bool animated = true) + { + float childPos0 = GetChildPosInContent(d); + float childPos1 = GetChildPosInContent(d, d.DrawSize); + + float minPos = Math.Min(childPos0, childPos1); + float maxPos = Math.Max(childPos0, childPos1); + + if (minPos < Current) + ScrollTo(minPos, animated); + else if (maxPos > Current + displayableContent) + ScrollTo(maxPos - displayableContent, animated); + } + + /// + /// Determines the position of a child in the content. + /// + /// The child to get the position from. + /// Positional offset in the child's space. + /// The position of the child. + public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, content)[ScrollDim]; + + /// + /// Determines the position of a child in the content. + /// + /// The child to get the position from. + /// The position of the child. + public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); + + private void updatePosition() + { + double localDistanceDecay = distanceDecay; + + // If we are not currently dragging the content, and we have scrolled out of bounds, + // then we should handle the clamping force. Note, that if the target is _within_ + // acceptable bounds, then we do not need special handling of the clamping force, as + // we will naturally scroll back into acceptable bounds. + if (!IsDragging && Current != Clamp(Current) && target != Clamp(target, -0.01f)) + { + // Firstly, we want to limit how far out the target may go to limit overly bouncy + // behaviour with extreme scroll velocities. + target = Clamp(target, ClampExtension); + + // Secondly, we would like to quickly approach the target while we are out of bounds. + // This is simulating a "strong" clamping force towards the target. + if (Current < target && target < 0 || Current > target && target > scrollableExtent) + localDistanceDecay = distance_decay_clamping * 2; + + // Lastly, we gradually nudge the target towards valid bounds. + target = (float)Interpolation.Lerp(Clamp(target), target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); + + float clampedTarget = Clamp(target); + if (Precision.AlmostEquals(clampedTarget, target)) + target = clampedTarget; + } + + // Exponential interpolation between the target and our current scroll position. + Current = (float)Interpolation.Lerp(target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); + + // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely. + if (Precision.AlmostEquals(Current, target)) + Current = target; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + updateSize(); + updatePosition(); + + if (ScrollDirection == Direction.Horizontal) + { + Scrollbar.X = Current * Scrollbar.Size.X; + content.X = -Current; + } + else + { + Scrollbar.Y = Current * Scrollbar.Size.Y; + content.Y = -Current; + } + } + + protected internal class ScrollbarContainer : Container + { + public Action Dragged; + + private readonly Color4 hoverColour = Color4.White; + private readonly Color4 defaultColour = Color4.Gray; + private readonly Color4 highlightColour = Color4.GreenYellow; + + private readonly Box box; + + private float dragOffset; + + private readonly int scrollDim; + + public ScrollbarContainer(Direction scrollDir) + { + scrollDim = (int)scrollDir; + RelativeSizeAxes = scrollDir == Direction.Horizontal ? Axes.X : Axes.Y; + Colour = defaultColour; + + Blending = BlendingMode.Additive; + + CornerRadius = 5; + + const float margin = 3; + + Margin = new MarginPadding + { + Left = scrollDir == Direction.Vertical ? margin : 0, + Right = scrollDir == Direction.Vertical ? margin : 0, + Top = scrollDir == Direction.Horizontal ? margin : 0, + Bottom = scrollDir == Direction.Horizontal ? margin : 0, + }; + + Masking = true; + + Child = box = new Box { RelativeSizeAxes = Axes.Both }; + + ResizeTo(1); + } + + public void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) + { + Vector2 size = new Vector2(10) + { + [scrollDim] = val + }; + this.ResizeTo(size, duration, easing); + } + + protected override bool OnClick(InputState state) => true; + + protected override bool OnHover(InputState state) + { + this.FadeColour(hoverColour, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.FadeColour(defaultColour, 100); + } + + protected override bool OnDragStart(InputState state) + { + dragOffset = state.Mouse.Position[scrollDim] - Position[scrollDim]; + return true; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (args.Button != MouseButton.Left) return false; + + //note that we are changing the colour of the box here as to not interfere with the hover effect. + box.FadeColour(highlightColour, 100); + + dragOffset = Position[scrollDim]; + Dragged?.Invoke(dragOffset); + return true; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + if (args.Button != MouseButton.Left) return false; + + box.FadeColour(Color4.White, 100); + + return base.OnMouseUp(state, args); + } + + protected override bool OnDrag(InputState state) + { + Dragged?.Invoke(state.Mouse.Position[scrollDim] - dragOffset); + return true; + } + } + } +} diff --git a/osu.Framework/Graphics/Containers/SearchContainer.cs b/osu.Framework/Graphics/Containers/SearchContainer.cs index eb5f45f9e..67d03faa6 100644 --- a/osu.Framework/Graphics/Containers/SearchContainer.cs +++ b/osu.Framework/Graphics/Containers/SearchContainer.cs @@ -1,55 +1,55 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; - -namespace osu.Framework.Graphics.Containers -{ - public class SearchContainer : SearchContainer - { - } - - public class SearchContainer : FillFlowContainer where T : Drawable - { - private string searchTerm; - - /// - /// A string that should match the children - /// - public string SearchTerm - { - get - { - return searchTerm; - } - set - { - searchTerm = value; - var terms = value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - Children.OfType().ForEach(child => match(child, terms)); - } - } - - private static bool match(IFilterable filterable, IEnumerable terms) - { - //Words matched by parent is not needed to match children - var childTerms = terms.Where(term => - !filterable.FilterTerms.Any(filterTerm => - filterTerm.IndexOf(term, StringComparison.InvariantCultureIgnoreCase) >= 0)).ToArray(); - - var hasFilterableChildren = filterable as IHasFilterableChildren; - - bool matching = childTerms.Length == 0; - - //We need to check the children and should any child match this matches aswell - if (hasFilterableChildren != null) - foreach (IFilterable searchableChildren in hasFilterableChildren.FilterableChildren) - matching |= match(searchableChildren, childTerms); - - return filterable.MatchingFilter = matching; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Framework.Graphics.Containers +{ + public class SearchContainer : SearchContainer + { + } + + public class SearchContainer : FillFlowContainer where T : Drawable + { + private string searchTerm; + + /// + /// A string that should match the children + /// + public string SearchTerm + { + get + { + return searchTerm; + } + set + { + searchTerm = value; + var terms = value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + Children.OfType().ForEach(child => match(child, terms)); + } + } + + private static bool match(IFilterable filterable, IEnumerable terms) + { + //Words matched by parent is not needed to match children + var childTerms = terms.Where(term => + !filterable.FilterTerms.Any(filterTerm => + filterTerm.IndexOf(term, StringComparison.InvariantCultureIgnoreCase) >= 0)).ToArray(); + + var hasFilterableChildren = filterable as IHasFilterableChildren; + + bool matching = childTerms.Length == 0; + + //We need to check the children and should any child match this matches aswell + if (hasFilterableChildren != null) + foreach (IFilterable searchableChildren in hasFilterableChildren.FilterableChildren) + matching |= match(searchableChildren, childTerms); + + return filterable.MatchingFilter = matching; + } + } +} diff --git a/osu.Framework/Graphics/Containers/TabbableContainer.cs b/osu.Framework/Graphics/Containers/TabbableContainer.cs index 625abf435..7b563978a 100644 --- a/osu.Framework/Graphics/Containers/TabbableContainer.cs +++ b/osu.Framework/Graphics/Containers/TabbableContainer.cs @@ -1,94 +1,94 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Input; -using OpenTK.Input; - -namespace osu.Framework.Graphics.Containers -{ - public class TabbableContainer : TabbableContainer - { - } - - /// - /// This interface is used for recogonizing of any type without reflection. - /// - internal interface ITabbableContainer - { - /// - /// Whether this can be tabbed to. - /// - bool CanBeTabbedTo { get; } - } - - public class TabbableContainer : Container, ITabbableContainer - where T : Drawable - { - /// - /// Whether this can be tabbed to. - /// - public virtual bool CanBeTabbedTo => true; - - /// - /// Allows for tabbing between multiple levels within the TabbableContentContainer. - /// - public Container TabbableContentContainer { private get; set; } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (TabbableContentContainer == null || args.Key != Key.Tab) - return false; - - var nextTab = nextTabStop(TabbableContentContainer, state.Keyboard.ShiftPressed); - if (nextTab != null) GetContainingInputManager().ChangeFocus(nextTab); - return true; - } - - private Drawable nextTabStop(Container target, bool reverse) - { - Stack stack = new Stack(); - stack.Push(target); // Extra push for circular tabbing - stack.Push(target); - - bool started = false; - while (stack.Count > 0) - { - var drawable = stack.Pop(); - - if (!started) - started = ReferenceEquals(drawable, this); - else if (drawable is ITabbableContainer tabbable && tabbable.CanBeTabbedTo) - return drawable; - - var composite = drawable as CompositeDrawable; - if (composite != null) - { - var newChildren = composite.InternalChildren.ToList(); - int bound = reverse ? newChildren.Count : 0; - if (!started) - { - // Find self, to know starting point - int index = newChildren.IndexOf(this); - if (index != -1) - bound = reverse ? index + 1 : index; - } - - if (reverse) - { - for (int i = 0; i < bound; i++) - stack.Push(newChildren[i]); - } - else - { - for (int i = newChildren.Count - 1; i >= bound; i--) - stack.Push(newChildren[i]); - } - } - } - - return null; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input; +using OpenTK.Input; + +namespace osu.Framework.Graphics.Containers +{ + public class TabbableContainer : TabbableContainer + { + } + + /// + /// This interface is used for recogonizing of any type without reflection. + /// + internal interface ITabbableContainer + { + /// + /// Whether this can be tabbed to. + /// + bool CanBeTabbedTo { get; } + } + + public class TabbableContainer : Container, ITabbableContainer + where T : Drawable + { + /// + /// Whether this can be tabbed to. + /// + public virtual bool CanBeTabbedTo => true; + + /// + /// Allows for tabbing between multiple levels within the TabbableContentContainer. + /// + public Container TabbableContentContainer { private get; set; } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (TabbableContentContainer == null || args.Key != Key.Tab) + return false; + + var nextTab = nextTabStop(TabbableContentContainer, state.Keyboard.ShiftPressed); + if (nextTab != null) GetContainingInputManager().ChangeFocus(nextTab); + return true; + } + + private Drawable nextTabStop(Container target, bool reverse) + { + Stack stack = new Stack(); + stack.Push(target); // Extra push for circular tabbing + stack.Push(target); + + bool started = false; + while (stack.Count > 0) + { + var drawable = stack.Pop(); + + if (!started) + started = ReferenceEquals(drawable, this); + else if (drawable is ITabbableContainer tabbable && tabbable.CanBeTabbedTo) + return drawable; + + var composite = drawable as CompositeDrawable; + if (composite != null) + { + var newChildren = composite.InternalChildren.ToList(); + int bound = reverse ? newChildren.Count : 0; + if (!started) + { + // Find self, to know starting point + int index = newChildren.IndexOf(this); + if (index != -1) + bound = reverse ? index + 1 : index; + } + + if (reverse) + { + for (int i = 0; i < bound; i++) + stack.Push(newChildren[i]); + } + else + { + for (int i = newChildren.Count - 1; i >= bound; i--) + stack.Push(newChildren[i]); + } + } + } + + return null; + } + } +} diff --git a/osu.Framework/Graphics/Containers/TextFlowContainer.cs b/osu.Framework/Graphics/Containers/TextFlowContainer.cs index 8722eb418..e10eb055d 100644 --- a/osu.Framework/Graphics/Containers/TextFlowContainer.cs +++ b/osu.Framework/Graphics/Containers/TextFlowContainer.cs @@ -1,375 +1,375 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Caching; -using osu.Framework.Graphics.Sprites; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A drawable text object that supports more advanced text formatting. - /// - public class TextFlowContainer : FillFlowContainer - { - private float firstLineIndent; - private readonly Action defaultCreationParameters; - - /// - /// An indent value for the first (header) line of a paragraph. - /// - public float FirstLineIndent - { - get { return firstLineIndent; } - set - { - if (value == firstLineIndent) return; - firstLineIndent = value; - - layout.Invalidate(); - } - } - - private float contentIndent; - - /// - /// An indent value for all lines proceeding the first line in a paragraph. - /// - public float ContentIndent - { - get { return contentIndent; } - set - { - if (value == contentIndent) return; - contentIndent = value; - - layout.Invalidate(); - } - } - - private float paragraphSpacing = 0.5f; - - /// - /// Vertical space between paragraphs (i.e. text separated by '\n') in multiples of the text size. - /// The default value is 0.5. - /// - public float ParagraphSpacing - { - get { return paragraphSpacing; } - set - { - if (value == paragraphSpacing) return; - paragraphSpacing = value; - - layout.Invalidate(); - } - } - - private float lineSpacing; - - /// - /// Vertical space between lines both when a new paragraph begins and when line wrapping occurs. - /// Additive with on new paragraph. Default value is 0. - /// - public float LineSpacing - { - get { return lineSpacing; } - set - { - if (value == lineSpacing) return; - lineSpacing = value; - - layout.Invalidate(); - } - } - - private Anchor textAnchor = Anchor.TopLeft; - /// - /// The which text should flow from. - /// - public Anchor TextAnchor - { - get { return textAnchor; } - set - { - if (textAnchor == value) - return; - textAnchor = value; - - // Todo: This is temporary for now because we don't have an easy way to re-flow the container... - if (IsLoaded) - throw new InvalidOperationException($"{nameof(TextAnchor)} may not change after the {nameof(TextFlowContainer)} is loaded."); - } - } - - /// - /// An easy way to set the full text of a text flow in one go. - /// This will overwrite any existing text added using this method of - /// - public string Text - { - set - { - Clear(); - AddText(value); - } - } - - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - layout.Invalidate(); - return base.Invalidate(invalidation, source, shallPropagate); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!layout.IsValid) - { - computeLayout(); - layout.Validate(); - } - } - - protected override int Compare(Drawable x, Drawable y) - { - // FillFlowContainer will reverse the ordering of right-anchored words such that the (previously) first word would be - // the right-most word, whereas it should still be flowed left-to-right. This is achieved by reversing the comparator. - if ((TextAnchor & Anchor.x2) > 0) - return base.Compare(y, x); - return base.Compare(x, y); - } - - /// - /// Add new text to this text flow. The \n character will create a new paragraph, not just a line break. If you need \n to be a line break, use instead. - /// - /// A collection of the objects for each word created from the given text. - /// The text to add. - /// A callback providing any instances created for this new text. - public IEnumerable AddText(string text, Action creationParameters = null) => AddLine(new TextLine(text, creationParameters), true); - - /// - /// Add an arbitrary to this . - /// While default creation parameters are applied automatically, word wrapping is unavailable for contained words. - /// This should only be used when a specialised type is requried. - /// - /// The text to add. - /// A callback providing any instances created for this new text. - public void AddText(SpriteText text, Action creationParameters = null) - { - base.Add(text); - defaultCreationParameters(text); - creationParameters?.Invoke(text); - } - - /// - /// Add a new paragraph to this text flow. The \n character will create a line break. If you need \n to be a new paragraph, not just a line break, use instead. - /// - /// A collection of the objects for each word created from the given text. - /// The paragraph to add. - /// A callback providing any instances created for this new paragraph. - public IEnumerable AddParagraph(string paragraph, Action creationParameters = null) => AddLine(new TextLine(paragraph, creationParameters), false); - - /// - /// End current line and start a new one. - /// - public void NewLine() => base.Add(new NewLineContainer(false)); - - /// - /// End current paragraph and start a new one. - /// - public void NewParagraph() => base.Add(new NewLineContainer(true)); - - public TextFlowContainer(Action defaultCreationParameters = null) - { - this.defaultCreationParameters = defaultCreationParameters; - } - - protected virtual SpriteText CreateSpriteText() => new SpriteText(); - - internal SpriteText CreateSpriteTextWithLine(TextLine line) - { - var spriteText = CreateSpriteText(); - defaultCreationParameters?.Invoke(spriteText); - line.ApplyParameters(spriteText); - return spriteText; - } - - public override void Add(Drawable drawable) - { - throw new InvalidOperationException($"Use {nameof(AddText)} to add text to a {nameof(TextFlowContainer)}."); - } - - internal virtual IEnumerable AddLine(TextLine line, bool newLineIsParagraph) - { - // !newLineIsParagraph effectively means that we want to add just *one* paragraph, which means we need to make sure that any previous paragraphs - // are terminated. Thus, we add a NewLineContainer that indicates the end of the paragraph before adding our current paragraph. - if (!newLineIsParagraph) - base.Add(new NewLineContainer(true)); - - return AddString(line, newLineIsParagraph); - } - - internal IEnumerable AddString(TextLine line, bool newLineIsParagraph) - { - bool first = true; - var sprites = new List(); - foreach (string l in line.Text.Split('\n')) - { - if (!first) - { - Drawable lastChild = Children.LastOrDefault(); - if (lastChild != null) - base.Add(new NewLineContainer(newLineIsParagraph)); - } - - foreach (string word in SplitWords(l)) - { - if (string.IsNullOrEmpty(word)) continue; - - var textSprite = CreateSpriteTextWithLine(line); - textSprite.Text = word; - sprites.Add(textSprite); - base.Add(textSprite); - } - - first = false; - } - - return sprites; - } - - protected string[] SplitWords(string text) - { - var words = new List(); - var builder = new StringBuilder(); - - for (var i = 0; i < text.Length; i++) - { - if (i == 0 || char.IsSeparator(text[i - 1]) || char.IsControl(text[i - 1])) - { - words.Add(builder.ToString()); - builder.Clear(); - } - - builder.Append(text[i]); - } - - if (builder.Length > 0) - words.Add(builder.ToString()); - - return words.ToArray(); - } - - private Cached layout = new Cached(); - - private void computeLayout() - { - var childrenByLine = new List>(); - var curLine = new List(); - foreach (var c in Children) - { - c.Anchor = TextAnchor; - c.Origin = TextAnchor; - - NewLineContainer nlc = c as NewLineContainer; - if (nlc != null) - { - curLine.Add(nlc); - childrenByLine.Add(curLine); - curLine = new List(); - } - else - { - if (c.X == 0) - { - if (curLine.Count > 0) - childrenByLine.Add(curLine); - curLine = new List(); - } - curLine.Add(c); - } - } - - if (curLine.Count > 0) - childrenByLine.Add(curLine); - - bool isFirstLine = true; - float lastLineHeight = 0f; - foreach (var line in childrenByLine) - { - bool isFirstChild = true; - IEnumerable lineBaseHeightValues = line.OfType().Select(l => l.LineBaseHeight); - float lineBaseHeight = lineBaseHeightValues.Any() ? lineBaseHeightValues.Max() : 0f; - float currentLineHeight = 0f; - float lineSpacingValue = lastLineHeight * LineSpacing; - - foreach (Drawable c in line) - { - NewLineContainer nlc = c as NewLineContainer; - if (nlc != null) - { - nlc.Height = nlc.IndicatesNewParagraph ? (currentLineHeight == 0 ? lastLineHeight : currentLineHeight) * ParagraphSpacing : 0; - continue; - } - - float childLineBaseHeight = (c as IHasLineBaseHeight)?.LineBaseHeight ?? 0f; - MarginPadding margin = new MarginPadding { Top = (childLineBaseHeight != 0f ? lineBaseHeight - childLineBaseHeight : 0f) + lineSpacingValue }; - if (isFirstLine) - margin.Left = FirstLineIndent; - else if (isFirstChild) - margin.Left = ContentIndent; - - c.Margin = margin; - - if (c.Height > currentLineHeight) - currentLineHeight = c.Height; - - isFirstChild = false; - } - - if (currentLineHeight != 0f) - lastLineHeight = currentLineHeight; - - isFirstLine = false; - } - } - - protected override bool ForceNewRow(Drawable child) => child is NewLineContainer; - - internal class NewLineContainer : Container - { - public readonly bool IndicatesNewParagraph; - - public NewLineContainer(bool newParagraph) - { - IndicatesNewParagraph = newParagraph; - } - } - - internal class TextLine - { - public readonly string Text; - internal readonly Action CreationParameters; - - public TextLine(string text, Action creationParameters = null) - { - Text = text; - CreationParameters = creationParameters; - } - - public void ApplyParameters(SpriteText spriteText) - { - CreationParameters?.Invoke(spriteText); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Caching; +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A drawable text object that supports more advanced text formatting. + /// + public class TextFlowContainer : FillFlowContainer + { + private float firstLineIndent; + private readonly Action defaultCreationParameters; + + /// + /// An indent value for the first (header) line of a paragraph. + /// + public float FirstLineIndent + { + get { return firstLineIndent; } + set + { + if (value == firstLineIndent) return; + firstLineIndent = value; + + layout.Invalidate(); + } + } + + private float contentIndent; + + /// + /// An indent value for all lines proceeding the first line in a paragraph. + /// + public float ContentIndent + { + get { return contentIndent; } + set + { + if (value == contentIndent) return; + contentIndent = value; + + layout.Invalidate(); + } + } + + private float paragraphSpacing = 0.5f; + + /// + /// Vertical space between paragraphs (i.e. text separated by '\n') in multiples of the text size. + /// The default value is 0.5. + /// + public float ParagraphSpacing + { + get { return paragraphSpacing; } + set + { + if (value == paragraphSpacing) return; + paragraphSpacing = value; + + layout.Invalidate(); + } + } + + private float lineSpacing; + + /// + /// Vertical space between lines both when a new paragraph begins and when line wrapping occurs. + /// Additive with on new paragraph. Default value is 0. + /// + public float LineSpacing + { + get { return lineSpacing; } + set + { + if (value == lineSpacing) return; + lineSpacing = value; + + layout.Invalidate(); + } + } + + private Anchor textAnchor = Anchor.TopLeft; + /// + /// The which text should flow from. + /// + public Anchor TextAnchor + { + get { return textAnchor; } + set + { + if (textAnchor == value) + return; + textAnchor = value; + + // Todo: This is temporary for now because we don't have an easy way to re-flow the container... + if (IsLoaded) + throw new InvalidOperationException($"{nameof(TextAnchor)} may not change after the {nameof(TextFlowContainer)} is loaded."); + } + } + + /// + /// An easy way to set the full text of a text flow in one go. + /// This will overwrite any existing text added using this method of + /// + public string Text + { + set + { + Clear(); + AddText(value); + } + } + + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawSize) > 0) + layout.Invalidate(); + return base.Invalidate(invalidation, source, shallPropagate); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!layout.IsValid) + { + computeLayout(); + layout.Validate(); + } + } + + protected override int Compare(Drawable x, Drawable y) + { + // FillFlowContainer will reverse the ordering of right-anchored words such that the (previously) first word would be + // the right-most word, whereas it should still be flowed left-to-right. This is achieved by reversing the comparator. + if ((TextAnchor & Anchor.x2) > 0) + return base.Compare(y, x); + return base.Compare(x, y); + } + + /// + /// Add new text to this text flow. The \n character will create a new paragraph, not just a line break. If you need \n to be a line break, use instead. + /// + /// A collection of the objects for each word created from the given text. + /// The text to add. + /// A callback providing any instances created for this new text. + public IEnumerable AddText(string text, Action creationParameters = null) => AddLine(new TextLine(text, creationParameters), true); + + /// + /// Add an arbitrary to this . + /// While default creation parameters are applied automatically, word wrapping is unavailable for contained words. + /// This should only be used when a specialised type is requried. + /// + /// The text to add. + /// A callback providing any instances created for this new text. + public void AddText(SpriteText text, Action creationParameters = null) + { + base.Add(text); + defaultCreationParameters(text); + creationParameters?.Invoke(text); + } + + /// + /// Add a new paragraph to this text flow. The \n character will create a line break. If you need \n to be a new paragraph, not just a line break, use instead. + /// + /// A collection of the objects for each word created from the given text. + /// The paragraph to add. + /// A callback providing any instances created for this new paragraph. + public IEnumerable AddParagraph(string paragraph, Action creationParameters = null) => AddLine(new TextLine(paragraph, creationParameters), false); + + /// + /// End current line and start a new one. + /// + public void NewLine() => base.Add(new NewLineContainer(false)); + + /// + /// End current paragraph and start a new one. + /// + public void NewParagraph() => base.Add(new NewLineContainer(true)); + + public TextFlowContainer(Action defaultCreationParameters = null) + { + this.defaultCreationParameters = defaultCreationParameters; + } + + protected virtual SpriteText CreateSpriteText() => new SpriteText(); + + internal SpriteText CreateSpriteTextWithLine(TextLine line) + { + var spriteText = CreateSpriteText(); + defaultCreationParameters?.Invoke(spriteText); + line.ApplyParameters(spriteText); + return spriteText; + } + + public override void Add(Drawable drawable) + { + throw new InvalidOperationException($"Use {nameof(AddText)} to add text to a {nameof(TextFlowContainer)}."); + } + + internal virtual IEnumerable AddLine(TextLine line, bool newLineIsParagraph) + { + // !newLineIsParagraph effectively means that we want to add just *one* paragraph, which means we need to make sure that any previous paragraphs + // are terminated. Thus, we add a NewLineContainer that indicates the end of the paragraph before adding our current paragraph. + if (!newLineIsParagraph) + base.Add(new NewLineContainer(true)); + + return AddString(line, newLineIsParagraph); + } + + internal IEnumerable AddString(TextLine line, bool newLineIsParagraph) + { + bool first = true; + var sprites = new List(); + foreach (string l in line.Text.Split('\n')) + { + if (!first) + { + Drawable lastChild = Children.LastOrDefault(); + if (lastChild != null) + base.Add(new NewLineContainer(newLineIsParagraph)); + } + + foreach (string word in SplitWords(l)) + { + if (string.IsNullOrEmpty(word)) continue; + + var textSprite = CreateSpriteTextWithLine(line); + textSprite.Text = word; + sprites.Add(textSprite); + base.Add(textSprite); + } + + first = false; + } + + return sprites; + } + + protected string[] SplitWords(string text) + { + var words = new List(); + var builder = new StringBuilder(); + + for (var i = 0; i < text.Length; i++) + { + if (i == 0 || char.IsSeparator(text[i - 1]) || char.IsControl(text[i - 1])) + { + words.Add(builder.ToString()); + builder.Clear(); + } + + builder.Append(text[i]); + } + + if (builder.Length > 0) + words.Add(builder.ToString()); + + return words.ToArray(); + } + + private Cached layout = new Cached(); + + private void computeLayout() + { + var childrenByLine = new List>(); + var curLine = new List(); + foreach (var c in Children) + { + c.Anchor = TextAnchor; + c.Origin = TextAnchor; + + NewLineContainer nlc = c as NewLineContainer; + if (nlc != null) + { + curLine.Add(nlc); + childrenByLine.Add(curLine); + curLine = new List(); + } + else + { + if (c.X == 0) + { + if (curLine.Count > 0) + childrenByLine.Add(curLine); + curLine = new List(); + } + curLine.Add(c); + } + } + + if (curLine.Count > 0) + childrenByLine.Add(curLine); + + bool isFirstLine = true; + float lastLineHeight = 0f; + foreach (var line in childrenByLine) + { + bool isFirstChild = true; + IEnumerable lineBaseHeightValues = line.OfType().Select(l => l.LineBaseHeight); + float lineBaseHeight = lineBaseHeightValues.Any() ? lineBaseHeightValues.Max() : 0f; + float currentLineHeight = 0f; + float lineSpacingValue = lastLineHeight * LineSpacing; + + foreach (Drawable c in line) + { + NewLineContainer nlc = c as NewLineContainer; + if (nlc != null) + { + nlc.Height = nlc.IndicatesNewParagraph ? (currentLineHeight == 0 ? lastLineHeight : currentLineHeight) * ParagraphSpacing : 0; + continue; + } + + float childLineBaseHeight = (c as IHasLineBaseHeight)?.LineBaseHeight ?? 0f; + MarginPadding margin = new MarginPadding { Top = (childLineBaseHeight != 0f ? lineBaseHeight - childLineBaseHeight : 0f) + lineSpacingValue }; + if (isFirstLine) + margin.Left = FirstLineIndent; + else if (isFirstChild) + margin.Left = ContentIndent; + + c.Margin = margin; + + if (c.Height > currentLineHeight) + currentLineHeight = c.Height; + + isFirstChild = false; + } + + if (currentLineHeight != 0f) + lastLineHeight = currentLineHeight; + + isFirstLine = false; + } + } + + protected override bool ForceNewRow(Drawable child) => child is NewLineContainer; + + internal class NewLineContainer : Container + { + public readonly bool IndicatesNewParagraph; + + public NewLineContainer(bool newParagraph) + { + IndicatesNewParagraph = newParagraph; + } + } + + internal class TextLine + { + public readonly string Text; + internal readonly Action CreationParameters; + + public TextLine(string text, Action creationParameters = null) + { + Text = text; + CreationParameters = creationParameters; + } + + public void ApplyParameters(SpriteText spriteText) + { + CreationParameters?.Invoke(spriteText); + } + } + } +} diff --git a/osu.Framework/Graphics/Containers/VisibilityContainer.cs b/osu.Framework/Graphics/Containers/VisibilityContainer.cs index a9291b3be..0647d954c 100644 --- a/osu.Framework/Graphics/Containers/VisibilityContainer.cs +++ b/osu.Framework/Graphics/Containers/VisibilityContainer.cs @@ -1,87 +1,87 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container which adds a basic visibility state. - /// - public abstract class VisibilityContainer : Container, IStateful - { - /// - /// Whether we should be in a hidden state when first displayed. - /// Override this and set to true to *always* perform a animation even when the state is non-hidden at - /// first display. - /// - protected virtual bool StartHidden => state == Visibility.Hidden; - - protected override void LoadComplete() - { - if (StartHidden) - { - // do this without triggering the StateChanged event, since hidden is a default. - PopOut(); - FinishTransforms(true); - } - - if (state != Visibility.Hidden) - updateState(); - - base.LoadComplete(); - } - - private Visibility state; - - public Visibility State - { - get { return state; } - set - { - if (value == state) return; - state = value; - - if (!IsLoaded) return; - - updateState(); - } - } - - private void updateState() - { - switch (state) - { - case Visibility.Hidden: - PopOut(); - break; - case Visibility.Visible: - PopIn(); - break; - } - - StateChanged?.Invoke(state); - } - - public override void Hide() => State = Visibility.Hidden; - - public override void Show() => State = Visibility.Visible; - - public override bool HandleKeyboardInput => State == Visibility.Visible; - public override bool HandleMouseInput => State == Visibility.Visible; - - public event Action StateChanged; - - protected abstract void PopIn(); - - protected abstract void PopOut(); - - public void ToggleVisibility() => State = State == Visibility.Visible ? Visibility.Hidden : Visibility.Visible; - } - - public enum Visibility - { - Hidden, - Visible - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which adds a basic visibility state. + /// + public abstract class VisibilityContainer : Container, IStateful + { + /// + /// Whether we should be in a hidden state when first displayed. + /// Override this and set to true to *always* perform a animation even when the state is non-hidden at + /// first display. + /// + protected virtual bool StartHidden => state == Visibility.Hidden; + + protected override void LoadComplete() + { + if (StartHidden) + { + // do this without triggering the StateChanged event, since hidden is a default. + PopOut(); + FinishTransforms(true); + } + + if (state != Visibility.Hidden) + updateState(); + + base.LoadComplete(); + } + + private Visibility state; + + public Visibility State + { + get { return state; } + set + { + if (value == state) return; + state = value; + + if (!IsLoaded) return; + + updateState(); + } + } + + private void updateState() + { + switch (state) + { + case Visibility.Hidden: + PopOut(); + break; + case Visibility.Visible: + PopIn(); + break; + } + + StateChanged?.Invoke(state); + } + + public override void Hide() => State = Visibility.Hidden; + + public override void Show() => State = Visibility.Visible; + + public override bool HandleKeyboardInput => State == Visibility.Visible; + public override bool HandleMouseInput => State == Visibility.Visible; + + public event Action StateChanged; + + protected abstract void PopIn(); + + protected abstract void PopOut(); + + public void ToggleVisibility() => State = State == Visibility.Visible ? Visibility.Hidden : Visibility.Visible; + } + + public enum Visibility + { + Hidden, + Visible + } +} diff --git a/osu.Framework/Graphics/Cursor/ContextMenuContainer.cs b/osu.Framework/Graphics/Cursor/ContextMenuContainer.cs index 39a8eae52..9e0b4168e 100644 --- a/osu.Framework/Graphics/Cursor/ContextMenuContainer.cs +++ b/osu.Framework/Graphics/Cursor/ContextMenuContainer.cs @@ -1,95 +1,95 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Linq; -using OpenTK; -using OpenTK.Input; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.Cursor -{ - /// - /// A container which manages a . - /// If a right-click happens on a that implements and exists as a child of the same as this container, - /// a will be displayed with bottom-right origin at the right-clicked position. - /// - public class ContextMenuContainer : CursorEffectContainer - { - private readonly Menu menu; - - private IHasContextMenu menuTarget; - private Vector2 relativeCursorPosition; - - /// - /// Creates a new context menu. Can be overridden to supply custom subclass of . - /// - protected virtual Menu CreateMenu() => new Menu(Direction.Vertical); - - private readonly Container content; - protected override Container Content => content; - - /// - /// Creates a new . - /// - public ContextMenuContainer() - { - AddInternal(content = new Container - { - RelativeSizeAxes = Axes.Both, - }); - - AddInternal(menu = CreateMenu()); - } - - protected override void OnSizingChanged() - { - base.OnSizingChanged(); - - if (content != null) - { - // reset to none to prevent exceptions - content.RelativeSizeAxes = Axes.None; - content.AutoSizeAxes = Axes.None; - - // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size - content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes; - content.AutoSizeAxes = AutoSizeAxes; - } - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - switch (args.Button) - { - case MouseButton.Right: - menuTarget = FindTargets().FirstOrDefault(); - - if (menuTarget == null) - { - if (menu.State == MenuState.Open) - menu.Close(); - return false; - } - - menu.Items = menuTarget.ContextMenuItems; - - menu.Position = ToLocalSpace(state.Mouse.NativeState.Position); - relativeCursorPosition = ToSpaceOfOtherDrawable(menu.Position, menuTarget); - menu.Open(); - return true; - default: - menu.Close(); - return false; - } - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - if (menu.State == MenuState.Open && menuTarget != null) - menu.Position = menuTarget.ToSpaceOfOtherDrawable(relativeCursorPosition, this); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Linq; +using OpenTK; +using OpenTK.Input; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.Cursor +{ + /// + /// A container which manages a . + /// If a right-click happens on a that implements and exists as a child of the same as this container, + /// a will be displayed with bottom-right origin at the right-clicked position. + /// + public class ContextMenuContainer : CursorEffectContainer + { + private readonly Menu menu; + + private IHasContextMenu menuTarget; + private Vector2 relativeCursorPosition; + + /// + /// Creates a new context menu. Can be overridden to supply custom subclass of . + /// + protected virtual Menu CreateMenu() => new Menu(Direction.Vertical); + + private readonly Container content; + protected override Container Content => content; + + /// + /// Creates a new . + /// + public ContextMenuContainer() + { + AddInternal(content = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + AddInternal(menu = CreateMenu()); + } + + protected override void OnSizingChanged() + { + base.OnSizingChanged(); + + if (content != null) + { + // reset to none to prevent exceptions + content.RelativeSizeAxes = Axes.None; + content.AutoSizeAxes = Axes.None; + + // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size + content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes; + content.AutoSizeAxes = AutoSizeAxes; + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + switch (args.Button) + { + case MouseButton.Right: + menuTarget = FindTargets().FirstOrDefault(); + + if (menuTarget == null) + { + if (menu.State == MenuState.Open) + menu.Close(); + return false; + } + + menu.Items = menuTarget.ContextMenuItems; + + menu.Position = ToLocalSpace(state.Mouse.NativeState.Position); + relativeCursorPosition = ToSpaceOfOtherDrawable(menu.Position, menuTarget); + menu.Open(); + return true; + default: + menu.Close(); + return false; + } + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + if (menu.State == MenuState.Open && menuTarget != null) + menu.Position = menuTarget.ToSpaceOfOtherDrawable(relativeCursorPosition, this); + } + } +} diff --git a/osu.Framework/Graphics/Cursor/CursorContainer.cs b/osu.Framework/Graphics/Cursor/CursorContainer.cs index bf64095c9..3908de206 100644 --- a/osu.Framework/Graphics/Cursor/CursorContainer.cs +++ b/osu.Framework/Graphics/Cursor/CursorContainer.cs @@ -1,78 +1,78 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Graphics.Cursor -{ - public class CursorContainer : VisibilityContainer, IRequireHighFrequencyMousePosition - { - public Drawable ActiveCursor { get; protected set; } - - public CursorContainer() - { - Depth = float.MinValue; - RelativeSizeAxes = Axes.Both; - - State = Visibility.Visible; - } - - [BackgroundDependencyLoader] - private void load() - { - Add(ActiveCursor = CreateCursor()); - } - - protected virtual Drawable CreateCursor() => new Cursor(); - - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; - - protected override bool OnMouseMove(InputState state) - { - ActiveCursor.Position = state.Mouse.Position; - return base.OnMouseMove(state); - } - - protected override void PopIn() - { - Alpha = 1; - } - - protected override void PopOut() - { - Alpha = 0; - } - - private class Cursor : CircularContainer - { - public Cursor() - { - AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; - - BorderThickness = 2; - BorderColour = new Color4(247, 99, 164, 255); - - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(247, 99, 164, 6), - Radius = 50 - }; - - Child = new Box - { - Size = new Vector2(8, 8), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - }; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Graphics.Cursor +{ + public class CursorContainer : VisibilityContainer, IRequireHighFrequencyMousePosition + { + public Drawable ActiveCursor { get; protected set; } + + public CursorContainer() + { + Depth = float.MinValue; + RelativeSizeAxes = Axes.Both; + + State = Visibility.Visible; + } + + [BackgroundDependencyLoader] + private void load() + { + Add(ActiveCursor = CreateCursor()); + } + + protected virtual Drawable CreateCursor() => new Cursor(); + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + + protected override bool OnMouseMove(InputState state) + { + ActiveCursor.Position = state.Mouse.Position; + return base.OnMouseMove(state); + } + + protected override void PopIn() + { + Alpha = 1; + } + + protected override void PopOut() + { + Alpha = 0; + } + + private class Cursor : CircularContainer + { + public Cursor() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + BorderThickness = 2; + BorderColour = new Color4(247, 99, 164, 255); + + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(247, 99, 164, 6), + Radius = 50 + }; + + Child = new Box + { + Size = new Vector2(8, 8), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }; + } + } + } +} diff --git a/osu.Framework/Graphics/Cursor/CursorEffectContainer.cs b/osu.Framework/Graphics/Cursor/CursorEffectContainer.cs index 8a0da7bbe..596dc338c 100644 --- a/osu.Framework/Graphics/Cursor/CursorEffectContainer.cs +++ b/osu.Framework/Graphics/Cursor/CursorEffectContainer.cs @@ -1,106 +1,106 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace osu.Framework.Graphics.Cursor -{ - public abstract class CursorEffectContainer : Container - where TSelf : CursorEffectContainer - where TTarget : class, IDrawable - { - private InputManager inputManager; - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager(); - } - - private readonly HashSet childDrawables = new HashSet(); - private readonly HashSet nestedTtcChildDrawables = new HashSet(); - private readonly List newChildDrawables = new List(); - private readonly List targetChildren = new List(); - - private void findTargetChildren() - { - Debug.Assert(childDrawables.Count == 0, $"{nameof(childDrawables)} should be empty but has {childDrawables.Count} elements."); - Debug.Assert(nestedTtcChildDrawables.Count == 0, $"{nameof(nestedTtcChildDrawables)} should be empty but has {nestedTtcChildDrawables.Count} elements."); - Debug.Assert(newChildDrawables.Count == 0, $"{nameof(newChildDrawables)} should be empty but has {newChildDrawables.Count} elements."); - Debug.Assert(targetChildren.Count == 0, $"{nameof(targetChildren)} should be empty but has {targetChildren.Count} elements."); - - // Skip all drawables in the hierarchy prior to (and including) ourself. - var targetCandidates = inputManager.PositionalInputQueue.Reverse().SkipWhile(d => d != this).Skip(1); - - childDrawables.Add(this); - - // keep track of all hovered drawables below this and nested effect containers - // so we can decide which ones are valid candidates for receiving our effect and so - // we know when we can abort our search. - foreach (var candidate in targetCandidates) - { - // Children of drawables we are responsible for transitively also fall into our subtree, - // and therefore we need to handle them. If they are not children of any drawables we handle, - // it means that we iterated beyond our subtree and may terminate. - IDrawable parent = candidate.Parent; - - // We keep track of all drawables we found while traversing the parent chain upwards. - newChildDrawables.Clear(); - newChildDrawables.Add(candidate); - // When we encounter a drawable we already encountered before, then there is no need - // to keep going upward, since we already recorded it previously. At that point we know - // the drawables we found are in fact children of ours. - while (!childDrawables.Contains(parent)) - { - // If we reach to the root node (i.e. parent == null), then we found a drawable - // which is no longer a child of ours and we may terminate. - if (parent == null) - return; - - newChildDrawables.Add(parent); - parent = parent.Parent; - } - - // Assuming we did _not_ end up terminating, then all found drawables are children of ours - // and need to be added. - childDrawables.UnionWith(newChildDrawables); - - // Keep track of child drawables whose effects are managed by a nested effect container. - // Note, that nested effect containers themselves could implement TTarget and - // are still our own responsibility to handle. - nestedTtcChildDrawables.UnionWith( - ((IEnumerable)newChildDrawables).Reverse() - .SkipWhile(d => d.Parent == this || !(d.Parent is TSelf) && !nestedTtcChildDrawables.Contains(d.Parent))); - - // Ignore drawables whose effects are managed by a nested effect container. - if (nestedTtcChildDrawables.Contains(candidate)) - continue; - - TTarget target = candidate as TTarget; - if (target != null && target.IsHovered) - // We found a valid candidate; keep track of it - targetChildren.Add(target); - } - } - - protected List FindTargets() - { - findTargetChildren(); - - List result = new List(targetChildren); - result.Reverse(); - - // Clean up - childDrawables.Clear(); - nestedTtcChildDrawables.Clear(); - newChildDrawables.Clear(); - targetChildren.Clear(); - - return result; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace osu.Framework.Graphics.Cursor +{ + public abstract class CursorEffectContainer : Container + where TSelf : CursorEffectContainer + where TTarget : class, IDrawable + { + private InputManager inputManager; + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + private readonly HashSet childDrawables = new HashSet(); + private readonly HashSet nestedTtcChildDrawables = new HashSet(); + private readonly List newChildDrawables = new List(); + private readonly List targetChildren = new List(); + + private void findTargetChildren() + { + Debug.Assert(childDrawables.Count == 0, $"{nameof(childDrawables)} should be empty but has {childDrawables.Count} elements."); + Debug.Assert(nestedTtcChildDrawables.Count == 0, $"{nameof(nestedTtcChildDrawables)} should be empty but has {nestedTtcChildDrawables.Count} elements."); + Debug.Assert(newChildDrawables.Count == 0, $"{nameof(newChildDrawables)} should be empty but has {newChildDrawables.Count} elements."); + Debug.Assert(targetChildren.Count == 0, $"{nameof(targetChildren)} should be empty but has {targetChildren.Count} elements."); + + // Skip all drawables in the hierarchy prior to (and including) ourself. + var targetCandidates = inputManager.PositionalInputQueue.Reverse().SkipWhile(d => d != this).Skip(1); + + childDrawables.Add(this); + + // keep track of all hovered drawables below this and nested effect containers + // so we can decide which ones are valid candidates for receiving our effect and so + // we know when we can abort our search. + foreach (var candidate in targetCandidates) + { + // Children of drawables we are responsible for transitively also fall into our subtree, + // and therefore we need to handle them. If they are not children of any drawables we handle, + // it means that we iterated beyond our subtree and may terminate. + IDrawable parent = candidate.Parent; + + // We keep track of all drawables we found while traversing the parent chain upwards. + newChildDrawables.Clear(); + newChildDrawables.Add(candidate); + // When we encounter a drawable we already encountered before, then there is no need + // to keep going upward, since we already recorded it previously. At that point we know + // the drawables we found are in fact children of ours. + while (!childDrawables.Contains(parent)) + { + // If we reach to the root node (i.e. parent == null), then we found a drawable + // which is no longer a child of ours and we may terminate. + if (parent == null) + return; + + newChildDrawables.Add(parent); + parent = parent.Parent; + } + + // Assuming we did _not_ end up terminating, then all found drawables are children of ours + // and need to be added. + childDrawables.UnionWith(newChildDrawables); + + // Keep track of child drawables whose effects are managed by a nested effect container. + // Note, that nested effect containers themselves could implement TTarget and + // are still our own responsibility to handle. + nestedTtcChildDrawables.UnionWith( + ((IEnumerable)newChildDrawables).Reverse() + .SkipWhile(d => d.Parent == this || !(d.Parent is TSelf) && !nestedTtcChildDrawables.Contains(d.Parent))); + + // Ignore drawables whose effects are managed by a nested effect container. + if (nestedTtcChildDrawables.Contains(candidate)) + continue; + + TTarget target = candidate as TTarget; + if (target != null && target.IsHovered) + // We found a valid candidate; keep track of it + targetChildren.Add(target); + } + } + + protected List FindTargets() + { + findTargetChildren(); + + List result = new List(targetChildren); + result.Reverse(); + + // Clean up + childDrawables.Clear(); + nestedTtcChildDrawables.Clear(); + newChildDrawables.Clear(); + targetChildren.Clear(); + + return result; + } + } +} diff --git a/osu.Framework/Graphics/Cursor/IHasContextMenu.cs b/osu.Framework/Graphics/Cursor/IHasContextMenu.cs index 50483aeff..d1446fbe6 100644 --- a/osu.Framework/Graphics/Cursor/IHasContextMenu.cs +++ b/osu.Framework/Graphics/Cursor/IHasContextMenu.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.UserInterface; - -namespace osu.Framework.Graphics.Cursor -{ - public interface IHasContextMenu : IDrawable - { - /// - /// Menu items that appear when the drawable is right-clicked. - /// - MenuItem[] ContextMenuItems { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.UserInterface; + +namespace osu.Framework.Graphics.Cursor +{ + public interface IHasContextMenu : IDrawable + { + /// + /// Menu items that appear when the drawable is right-clicked. + /// + MenuItem[] ContextMenuItems { get; } + } +} diff --git a/osu.Framework/Graphics/Cursor/IHasCustomTooltip.cs b/osu.Framework/Graphics/Cursor/IHasCustomTooltip.cs index 77a75bd67..00318b7e7 100644 --- a/osu.Framework/Graphics/Cursor/IHasCustomTooltip.cs +++ b/osu.Framework/Graphics/Cursor/IHasCustomTooltip.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Cursor -{ - /// - /// Implementing this interface allows the implementing to display a custom tooltip if it is the child of a . - /// Keep in mind that tooltips can only be displayed by a if the implementing has set to true. - /// - public interface IHasCustomTooltip : IHasTooltip - { - /// - /// The custom tooltip that should be displayed. - /// - /// The custom tooltip that should be displayed. - ITooltip GetCustomTooltip(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Cursor +{ + /// + /// Implementing this interface allows the implementing to display a custom tooltip if it is the child of a . + /// Keep in mind that tooltips can only be displayed by a if the implementing has set to true. + /// + public interface IHasCustomTooltip : IHasTooltip + { + /// + /// The custom tooltip that should be displayed. + /// + /// The custom tooltip that should be displayed. + ITooltip GetCustomTooltip(); + } +} diff --git a/osu.Framework/Graphics/Cursor/IHasTooltip.cs b/osu.Framework/Graphics/Cursor/IHasTooltip.cs index 5e9ab8a65..eb44d17ed 100644 --- a/osu.Framework/Graphics/Cursor/IHasTooltip.cs +++ b/osu.Framework/Graphics/Cursor/IHasTooltip.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Cursor -{ - /// - /// Implementing this interface allows the implementing to display a tooltip if it is the child of a . The tooltip used is - /// dependent on the implementation of the method of the containing this . - /// Keep in mind that tooltips can only be displayed by a if the implementing has set to true. - /// - public interface IHasTooltip : IDrawable - { - /// - /// Tooltip that shows when hovering the drawable. - /// - string TooltipText { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Cursor +{ + /// + /// Implementing this interface allows the implementing to display a tooltip if it is the child of a . The tooltip used is + /// dependent on the implementation of the method of the containing this . + /// Keep in mind that tooltips can only be displayed by a if the implementing has set to true. + /// + public interface IHasTooltip : IDrawable + { + /// + /// Tooltip that shows when hovering the drawable. + /// + string TooltipText { get; } + } +} diff --git a/osu.Framework/Graphics/Cursor/ITooltip.cs b/osu.Framework/Graphics/Cursor/ITooltip.cs index 62780e4af..c536bc3d3 100644 --- a/osu.Framework/Graphics/Cursor/ITooltip.cs +++ b/osu.Framework/Graphics/Cursor/ITooltip.cs @@ -1,30 +1,30 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - - -using OpenTK; - -namespace osu.Framework.Graphics.Cursor -{ - /// - /// A tooltip that can be used in conjunction with a and/or implementation. - /// - public interface ITooltip : IDrawable - { - /// - /// The text to display on the tooltip. - /// - string TooltipText { set; } - - /// - /// Refreshes the tooltip, updating potential non-text elements such as textures and colours. - /// - void Refresh(); - - /// - /// Moves the tooltip to the given position. May use easing. - /// - /// The position the tooltip should be moved to. - void Move(Vector2 pos); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + + +using OpenTK; + +namespace osu.Framework.Graphics.Cursor +{ + /// + /// A tooltip that can be used in conjunction with a and/or implementation. + /// + public interface ITooltip : IDrawable + { + /// + /// The text to display on the tooltip. + /// + string TooltipText { set; } + + /// + /// Refreshes the tooltip, updating potential non-text elements such as textures and colours. + /// + void Refresh(); + + /// + /// Moves the tooltip to the given position. May use easing. + /// + /// The position the tooltip should be moved to. + void Move(Vector2 pos); + } +} diff --git a/osu.Framework/Graphics/Cursor/TooltipContainer.cs b/osu.Framework/Graphics/Cursor/TooltipContainer.cs index 055ab40dd..78457bb37 100644 --- a/osu.Framework/Graphics/Cursor/TooltipContainer.cs +++ b/osu.Framework/Graphics/Cursor/TooltipContainer.cs @@ -1,310 +1,310 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Framework.Graphics.Cursor -{ - /// - /// Displays Tooltips for all its children that inherit from the or interfaces. Keep in mind that only children with set to true will be checked for their tooltips. - /// - public class TooltipContainer : CursorEffectContainer, IHandleGlobalInput - { - private readonly CursorContainer cursorContainer; - private readonly ITooltip defaultTooltip; - - private ITooltip currentTooltip; - - private InputManager inputManager; - - /// - /// Duration the cursor has to stay in a circular region of - /// for the tooltip to appear. - /// - protected virtual double AppearDelay => 220; - - /// - /// Radius of the circular region the cursor has to stay in for - /// milliseconds for the tooltip to appear. - /// - protected virtual float AppearRadius => 20; - - private IHasTooltip currentlyDisplayed; - - /// - /// Creates a new tooltip. Can be overridden to supply custom subclass of . - /// - protected virtual ITooltip CreateTooltip() => new Tooltip(); - - private bool hasValidTooltip(IHasTooltip target) => !string.IsNullOrEmpty(target?.TooltipText); - - private readonly Container content; - protected override Container Content => content; - - /// - /// Creates a tooltip container where the tooltip is positioned at the bottom-right of - /// the of the given . - /// - /// The of which the - /// shall be used for positioning. If null is provided, then a small offset from the current mouse position is used. - public TooltipContainer(CursorContainer cursorContainer = null) - { - this.cursorContainer = cursorContainer; - AddInternal(content = new Container - { - RelativeSizeAxes = Axes.Both, - }); - AddInternal((Drawable)(currentTooltip = CreateTooltip())); - defaultTooltip = currentTooltip; - } - - protected override void OnSizingChanged() - { - base.OnSizingChanged(); - - if (content != null) - { - // reset to none to prevent exceptions - content.RelativeSizeAxes = Axes.None; - content.AutoSizeAxes = Axes.None; - - // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size - content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes; - content.AutoSizeAxes = AutoSizeAxes; - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager(); - } - - private Vector2 computeTooltipPosition() - { - // Update the position of the displayed tooltip. - // Our goal is to find the bounding circle of the cursor in screen-space, and to - // position the top-left corner of the tooltip at the circle's southeast position. - float boundingRadius; - Vector2 cursorCentre; - - if (cursorContainer == null) - { - cursorCentre = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - boundingRadius = 14f; - } - else - { - Quad cursorQuad = cursorContainer.ActiveCursor.ToSpaceOfOtherDrawable(cursorContainer.ActiveCursor.DrawRectangle, this); - cursorCentre = cursorQuad.Centre; - // We only need to check 2 of the 4 vertices, because we only allow affine transformations - // and the quad is therefore symmetric around the centre. - boundingRadius = Math.Max( - (cursorQuad.TopLeft - cursorCentre).Length, - (cursorQuad.TopRight - cursorCentre).Length); - } - - Vector2 southEast = new Vector2(1).Normalized(); - Vector2 tooltipPos = cursorCentre + southEast * boundingRadius; - - // Clamp position to tooltip container - tooltipPos.X = Math.Min(tooltipPos.X, DrawWidth - currentTooltip.DrawSize.X - 5); - float dX = Math.Max(0, tooltipPos.X - cursorCentre.X); - float dY = (float)Math.Sqrt(boundingRadius * boundingRadius - dX * dX); - - if (tooltipPos.Y > DrawHeight - currentTooltip.DrawSize.Y - 5) - tooltipPos.Y = cursorCentre.Y - dY - currentTooltip.DrawSize.Y; - else - tooltipPos.Y = cursorCentre.Y + dY; - - return tooltipPos; - } - - private struct TimedPosition - { - public double Time; - public Vector2 Position; - } - - protected override void Update() - { - base.Update(); - - IHasTooltip target = findTooltipTarget(); - if (target != null && target != currentlyDisplayed) - { - currentlyDisplayed = target; - - RemoveInternal((Drawable)currentTooltip); - currentTooltip = getTooltip(target); - AddInternal((Drawable)currentTooltip); - - currentTooltip.Show(); - } - } - - private readonly List recentMousePositions = new List(); - private double lastRecordedPositionTime; - - /// - /// Determines which drawable should currently receive a tooltip, taking into account - /// and . Returns null if no valid - /// target is found. - /// - /// The tooltip target. null if no valid one is found. - private IHasTooltip findTooltipTarget() - { - // While we are dragging a tooltipped drawable we should show a tooltip for it. - IHasTooltip draggedTarget = inputManager.DraggedDrawable as IHasTooltip; - if (draggedTarget != null) - return hasValidTooltip(draggedTarget) ? draggedTarget : null; - - // Always keep 10 positions at equally-sized time intervals that add up to AppearDelay. - double positionRecordInterval = AppearDelay / 10; - if (Time.Current - lastRecordedPositionTime >= positionRecordInterval) - { - lastRecordedPositionTime = Time.Current; - recentMousePositions.Add(new TimedPosition - { - Time = Time.Current, - Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - }); - } - - recentMousePositions.RemoveAll(t => Time.Current - t.Time > AppearDelay); - - // For determining whether to show a tooltip we first select only those positions - // which happened within a shorter, alpha-adjusted appear delay. - double alphaModifiedAppearDelay = (1 - currentTooltip.Alpha) * AppearDelay; - var relevantPositions = recentMousePositions.Where(t => Time.Current - t.Time <= alphaModifiedAppearDelay); - - // We then check whether all relevant positions fall within a radius of AppearRadius within the - // first relevant position. If so, then the mouse has stayed within a small circular region of - // AppearRadius for the duration of the modified appear delay, and we therefore want to display - // the tooltip. - Vector2 first = relevantPositions.FirstOrDefault().Position; - float appearRadiusSq = AppearRadius * AppearRadius; - - if (relevantPositions.All(t => Vector2Extensions.DistanceSquared(t.Position, first) < appearRadiusSq)) - return FindTargets().FirstOrDefault(t => t.TooltipText != null); - - return null; - } - - /// - /// Refreshes the displayed tooltip. By default, this s the tooltip to the cursor position, updates its and calls its method. - /// - /// The tooltip that is refreshed. - /// The target of the tooltip. - protected virtual void RefreshTooltip(ITooltip tooltip, IHasTooltip tooltipTarget) - { - if (tooltipTarget != null) - { - tooltip.TooltipText = tooltipTarget.TooltipText; - tooltip.Refresh(); - } - - tooltip.Move(computeTooltipPosition()); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - RefreshTooltip(currentTooltip, currentlyDisplayed); - - if (currentlyDisplayed != null && ShallHideTooltip(currentlyDisplayed)) - hideTooltip(); - } - - private void hideTooltip() - { - currentTooltip.Hide(); - currentlyDisplayed = null; - } - - /// - /// Returns true if the currently visible tooltip should be hidden, false otherwise. By default, returns true if the target of the tooltip is neither hovered nor dragged. - /// - /// The target of the tooltip. - /// True if the currently visible tooltip should be hidden, false otherwise. - protected virtual bool ShallHideTooltip(IHasTooltip tooltipTarget) => !hasValidTooltip(tooltipTarget) || !tooltipTarget.IsHovered && !tooltipTarget.IsDragged; - - private ITooltip getTooltip(IHasTooltip target) => (target as IHasCustomTooltip)?.GetCustomTooltip() ?? defaultTooltip; - - /// - /// The default tooltip. Simply displays its text on a gray background and performs no easing. - /// - public class Tooltip : VisibilityContainer, ITooltip - { - private readonly SpriteText text; - - /// - /// The text to be displayed by this tooltip. This property is assigned to whenever the tooltip text changes. - /// - public virtual string TooltipText - { - set - { - text.Text = value; - } - } - - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - - private const float text_size = 16; - - /// - /// Constructs a new tooltip that starts out invisible. - /// - public Tooltip() - { - Alpha = 0; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gray, - }, - text = new SpriteText - { - TextSize = text_size, - Padding = new MarginPadding(5), - } - }; - } - - public virtual void Refresh() { } - - /// - /// Called whenever the tooltip appears. When overriding do not forget to fade in. - /// - protected override void PopIn() => this.FadeIn(); - - /// - /// Called whenever the tooltip disappears. When overriding do not forget to fade out. - /// - protected override void PopOut() => this.FadeOut(); - - /// - /// Called whenever the position of the tooltip changes. Can be overridden to customize - /// easing. - /// - /// The new position of the tooltip. - public virtual void Move(Vector2 pos) => Position = pos; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Graphics.Cursor +{ + /// + /// Displays Tooltips for all its children that inherit from the or interfaces. Keep in mind that only children with set to true will be checked for their tooltips. + /// + public class TooltipContainer : CursorEffectContainer, IHandleGlobalInput + { + private readonly CursorContainer cursorContainer; + private readonly ITooltip defaultTooltip; + + private ITooltip currentTooltip; + + private InputManager inputManager; + + /// + /// Duration the cursor has to stay in a circular region of + /// for the tooltip to appear. + /// + protected virtual double AppearDelay => 220; + + /// + /// Radius of the circular region the cursor has to stay in for + /// milliseconds for the tooltip to appear. + /// + protected virtual float AppearRadius => 20; + + private IHasTooltip currentlyDisplayed; + + /// + /// Creates a new tooltip. Can be overridden to supply custom subclass of . + /// + protected virtual ITooltip CreateTooltip() => new Tooltip(); + + private bool hasValidTooltip(IHasTooltip target) => !string.IsNullOrEmpty(target?.TooltipText); + + private readonly Container content; + protected override Container Content => content; + + /// + /// Creates a tooltip container where the tooltip is positioned at the bottom-right of + /// the of the given . + /// + /// The of which the + /// shall be used for positioning. If null is provided, then a small offset from the current mouse position is used. + public TooltipContainer(CursorContainer cursorContainer = null) + { + this.cursorContainer = cursorContainer; + AddInternal(content = new Container + { + RelativeSizeAxes = Axes.Both, + }); + AddInternal((Drawable)(currentTooltip = CreateTooltip())); + defaultTooltip = currentTooltip; + } + + protected override void OnSizingChanged() + { + base.OnSizingChanged(); + + if (content != null) + { + // reset to none to prevent exceptions + content.RelativeSizeAxes = Axes.None; + content.AutoSizeAxes = Axes.None; + + // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size + content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes; + content.AutoSizeAxes = AutoSizeAxes; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + private Vector2 computeTooltipPosition() + { + // Update the position of the displayed tooltip. + // Our goal is to find the bounding circle of the cursor in screen-space, and to + // position the top-left corner of the tooltip at the circle's southeast position. + float boundingRadius; + Vector2 cursorCentre; + + if (cursorContainer == null) + { + cursorCentre = ToLocalSpace(inputManager.CurrentState.Mouse.Position); + boundingRadius = 14f; + } + else + { + Quad cursorQuad = cursorContainer.ActiveCursor.ToSpaceOfOtherDrawable(cursorContainer.ActiveCursor.DrawRectangle, this); + cursorCentre = cursorQuad.Centre; + // We only need to check 2 of the 4 vertices, because we only allow affine transformations + // and the quad is therefore symmetric around the centre. + boundingRadius = Math.Max( + (cursorQuad.TopLeft - cursorCentre).Length, + (cursorQuad.TopRight - cursorCentre).Length); + } + + Vector2 southEast = new Vector2(1).Normalized(); + Vector2 tooltipPos = cursorCentre + southEast * boundingRadius; + + // Clamp position to tooltip container + tooltipPos.X = Math.Min(tooltipPos.X, DrawWidth - currentTooltip.DrawSize.X - 5); + float dX = Math.Max(0, tooltipPos.X - cursorCentre.X); + float dY = (float)Math.Sqrt(boundingRadius * boundingRadius - dX * dX); + + if (tooltipPos.Y > DrawHeight - currentTooltip.DrawSize.Y - 5) + tooltipPos.Y = cursorCentre.Y - dY - currentTooltip.DrawSize.Y; + else + tooltipPos.Y = cursorCentre.Y + dY; + + return tooltipPos; + } + + private struct TimedPosition + { + public double Time; + public Vector2 Position; + } + + protected override void Update() + { + base.Update(); + + IHasTooltip target = findTooltipTarget(); + if (target != null && target != currentlyDisplayed) + { + currentlyDisplayed = target; + + RemoveInternal((Drawable)currentTooltip); + currentTooltip = getTooltip(target); + AddInternal((Drawable)currentTooltip); + + currentTooltip.Show(); + } + } + + private readonly List recentMousePositions = new List(); + private double lastRecordedPositionTime; + + /// + /// Determines which drawable should currently receive a tooltip, taking into account + /// and . Returns null if no valid + /// target is found. + /// + /// The tooltip target. null if no valid one is found. + private IHasTooltip findTooltipTarget() + { + // While we are dragging a tooltipped drawable we should show a tooltip for it. + IHasTooltip draggedTarget = inputManager.DraggedDrawable as IHasTooltip; + if (draggedTarget != null) + return hasValidTooltip(draggedTarget) ? draggedTarget : null; + + // Always keep 10 positions at equally-sized time intervals that add up to AppearDelay. + double positionRecordInterval = AppearDelay / 10; + if (Time.Current - lastRecordedPositionTime >= positionRecordInterval) + { + lastRecordedPositionTime = Time.Current; + recentMousePositions.Add(new TimedPosition + { + Time = Time.Current, + Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) + }); + } + + recentMousePositions.RemoveAll(t => Time.Current - t.Time > AppearDelay); + + // For determining whether to show a tooltip we first select only those positions + // which happened within a shorter, alpha-adjusted appear delay. + double alphaModifiedAppearDelay = (1 - currentTooltip.Alpha) * AppearDelay; + var relevantPositions = recentMousePositions.Where(t => Time.Current - t.Time <= alphaModifiedAppearDelay); + + // We then check whether all relevant positions fall within a radius of AppearRadius within the + // first relevant position. If so, then the mouse has stayed within a small circular region of + // AppearRadius for the duration of the modified appear delay, and we therefore want to display + // the tooltip. + Vector2 first = relevantPositions.FirstOrDefault().Position; + float appearRadiusSq = AppearRadius * AppearRadius; + + if (relevantPositions.All(t => Vector2Extensions.DistanceSquared(t.Position, first) < appearRadiusSq)) + return FindTargets().FirstOrDefault(t => t.TooltipText != null); + + return null; + } + + /// + /// Refreshes the displayed tooltip. By default, this s the tooltip to the cursor position, updates its and calls its method. + /// + /// The tooltip that is refreshed. + /// The target of the tooltip. + protected virtual void RefreshTooltip(ITooltip tooltip, IHasTooltip tooltipTarget) + { + if (tooltipTarget != null) + { + tooltip.TooltipText = tooltipTarget.TooltipText; + tooltip.Refresh(); + } + + tooltip.Move(computeTooltipPosition()); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + RefreshTooltip(currentTooltip, currentlyDisplayed); + + if (currentlyDisplayed != null && ShallHideTooltip(currentlyDisplayed)) + hideTooltip(); + } + + private void hideTooltip() + { + currentTooltip.Hide(); + currentlyDisplayed = null; + } + + /// + /// Returns true if the currently visible tooltip should be hidden, false otherwise. By default, returns true if the target of the tooltip is neither hovered nor dragged. + /// + /// The target of the tooltip. + /// True if the currently visible tooltip should be hidden, false otherwise. + protected virtual bool ShallHideTooltip(IHasTooltip tooltipTarget) => !hasValidTooltip(tooltipTarget) || !tooltipTarget.IsHovered && !tooltipTarget.IsDragged; + + private ITooltip getTooltip(IHasTooltip target) => (target as IHasCustomTooltip)?.GetCustomTooltip() ?? defaultTooltip; + + /// + /// The default tooltip. Simply displays its text on a gray background and performs no easing. + /// + public class Tooltip : VisibilityContainer, ITooltip + { + private readonly SpriteText text; + + /// + /// The text to be displayed by this tooltip. This property is assigned to whenever the tooltip text changes. + /// + public virtual string TooltipText + { + set + { + text.Text = value; + } + } + + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + + private const float text_size = 16; + + /// + /// Constructs a new tooltip that starts out invisible. + /// + public Tooltip() + { + Alpha = 0; + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + text = new SpriteText + { + TextSize = text_size, + Padding = new MarginPadding(5), + } + }; + } + + public virtual void Refresh() { } + + /// + /// Called whenever the tooltip appears. When overriding do not forget to fade in. + /// + protected override void PopIn() => this.FadeIn(); + + /// + /// Called whenever the tooltip disappears. When overriding do not forget to fade out. + /// + protected override void PopOut() => this.FadeOut(); + + /// + /// Called whenever the position of the tooltip changes. Can be overridden to customize + /// easing. + /// + /// The new position of the tooltip. + public virtual void Move(Vector2 pos) => Position = pos; + } + } +} diff --git a/osu.Framework/Graphics/DrawInfo.cs b/osu.Framework/Graphics/DrawInfo.cs index 58be2c87f..cba682e74 100644 --- a/osu.Framework/Graphics/DrawInfo.cs +++ b/osu.Framework/Graphics/DrawInfo.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Extensions.MatrixExtensions; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Extensions.TypeExtensions; - -namespace osu.Framework.Graphics -{ - public struct DrawInfo : IEquatable - { - public Matrix3 Matrix; - public Matrix3 MatrixInverse; - public ColourInfo Colour; - public BlendingInfo Blending; - - public DrawInfo(Matrix3? matrix = null, Matrix3? matrixInverse = null, ColourInfo? colour = null, BlendingInfo? blending = null) - { - Matrix = matrix ?? Matrix3.Identity; - MatrixInverse = matrixInverse ?? Matrix3.Identity; - Colour = colour ?? ColourInfo.SingleColour(Color4.White); - Blending = blending ?? new BlendingInfo(); - } - - /// - /// Applies a transformation to the current DrawInfo. - /// - /// The amount by which to translate the current position. - /// The amount by which to scale. - /// The amount by which to rotate. - /// The shear amounts for both directions. - /// The center of rotation and scale. - public void ApplyTransform(Vector2 translation, Vector2 scale, float rotation, Vector2 shear, Vector2 origin) - { - if (translation != Vector2.Zero) - { - MatrixExtensions.TranslateFromLeft(ref Matrix, translation); - MatrixExtensions.TranslateFromRight(ref MatrixInverse, -translation); - } - - if (rotation != 0) - { - float radians = MathHelper.DegreesToRadians(rotation); - MatrixExtensions.RotateFromLeft(ref Matrix, radians); - MatrixExtensions.RotateFromRight(ref MatrixInverse, -radians); - } - - if (shear != Vector2.Zero) - { - MatrixExtensions.ShearFromLeft(ref Matrix, -shear); - MatrixExtensions.ShearFromRight(ref MatrixInverse, shear); - } - - if (scale != Vector2.One) - { - Vector2 inverseScale = new Vector2(1.0f / scale.X, 1.0f / scale.Y); - MatrixExtensions.ScaleFromLeft(ref Matrix, scale); - MatrixExtensions.ScaleFromRight(ref MatrixInverse, inverseScale); - } - - if (origin != Vector2.Zero) - { - MatrixExtensions.TranslateFromLeft(ref Matrix, -origin); - MatrixExtensions.TranslateFromRight(ref MatrixInverse, origin); - } - - //======================================================================================== - //== Uncomment the following 2 lines to use a ground-truth matrix inverse for debugging == - //======================================================================================== - //target.MatrixInverse = target.Matrix; - //MatrixExtensions.FastInvert(ref target.MatrixInverse); - } - - public bool Equals(DrawInfo other) - { - return Matrix.Equals(other.Matrix) && Colour.Equals(other.Colour) && Blending.Equals(other.Blending); - } - - public override string ToString() => $@"{GetType().ReadableName().Replace(@"DrawInfo", string.Empty)} DrawInfo"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Extensions.MatrixExtensions; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Framework.Graphics +{ + public struct DrawInfo : IEquatable + { + public Matrix3 Matrix; + public Matrix3 MatrixInverse; + public ColourInfo Colour; + public BlendingInfo Blending; + + public DrawInfo(Matrix3? matrix = null, Matrix3? matrixInverse = null, ColourInfo? colour = null, BlendingInfo? blending = null) + { + Matrix = matrix ?? Matrix3.Identity; + MatrixInverse = matrixInverse ?? Matrix3.Identity; + Colour = colour ?? ColourInfo.SingleColour(Color4.White); + Blending = blending ?? new BlendingInfo(); + } + + /// + /// Applies a transformation to the current DrawInfo. + /// + /// The amount by which to translate the current position. + /// The amount by which to scale. + /// The amount by which to rotate. + /// The shear amounts for both directions. + /// The center of rotation and scale. + public void ApplyTransform(Vector2 translation, Vector2 scale, float rotation, Vector2 shear, Vector2 origin) + { + if (translation != Vector2.Zero) + { + MatrixExtensions.TranslateFromLeft(ref Matrix, translation); + MatrixExtensions.TranslateFromRight(ref MatrixInverse, -translation); + } + + if (rotation != 0) + { + float radians = MathHelper.DegreesToRadians(rotation); + MatrixExtensions.RotateFromLeft(ref Matrix, radians); + MatrixExtensions.RotateFromRight(ref MatrixInverse, -radians); + } + + if (shear != Vector2.Zero) + { + MatrixExtensions.ShearFromLeft(ref Matrix, -shear); + MatrixExtensions.ShearFromRight(ref MatrixInverse, shear); + } + + if (scale != Vector2.One) + { + Vector2 inverseScale = new Vector2(1.0f / scale.X, 1.0f / scale.Y); + MatrixExtensions.ScaleFromLeft(ref Matrix, scale); + MatrixExtensions.ScaleFromRight(ref MatrixInverse, inverseScale); + } + + if (origin != Vector2.Zero) + { + MatrixExtensions.TranslateFromLeft(ref Matrix, -origin); + MatrixExtensions.TranslateFromRight(ref MatrixInverse, origin); + } + + //======================================================================================== + //== Uncomment the following 2 lines to use a ground-truth matrix inverse for debugging == + //======================================================================================== + //target.MatrixInverse = target.Matrix; + //MatrixExtensions.FastInvert(ref target.MatrixInverse); + } + + public bool Equals(DrawInfo other) + { + return Matrix.Equals(other.Matrix) && Colour.Equals(other.Colour) && Blending.Equals(other.Blending); + } + + public override string ToString() => $@"{GetType().ReadableName().Replace(@"DrawInfo", string.Empty)} DrawInfo"; + } +} diff --git a/osu.Framework/Graphics/DrawNode.cs b/osu.Framework/Graphics/DrawNode.cs index 6577046df..331bcf75d 100644 --- a/osu.Framework/Graphics/DrawNode.cs +++ b/osu.Framework/Graphics/DrawNode.cs @@ -1,40 +1,40 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.OpenGL; -using System; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics -{ - /// - /// Contains all the information required to draw a single . - /// A hierarchy of DrawNodes is passed to the draw thread for rendering every frame. - /// - public class DrawNode - { - /// - /// Contains a linear transformation, colour information, and blending information - /// of this draw node. - /// - public DrawInfo DrawInfo; - - /// - /// Identifies the state of this draw node with an invalidation state of its corresponding - /// . Whenever the invalidation state of this draw node disagrees - /// with the state of its it has to be updated. - /// - public long InvalidationID; - - /// - /// Draws this draw node to the screen. - /// - /// The action to be performed on each vertex of - /// the draw node in order to draw it if required. This is primarily used by - /// textured sprites. - public virtual void Draw(Action vertexAction) - { - GLWrapper.SetBlend(DrawInfo.Blending); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.OpenGL; +using System; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics +{ + /// + /// Contains all the information required to draw a single . + /// A hierarchy of DrawNodes is passed to the draw thread for rendering every frame. + /// + public class DrawNode + { + /// + /// Contains a linear transformation, colour information, and blending information + /// of this draw node. + /// + public DrawInfo DrawInfo; + + /// + /// Identifies the state of this draw node with an invalidation state of its corresponding + /// . Whenever the invalidation state of this draw node disagrees + /// with the state of its it has to be updated. + /// + public long InvalidationID; + + /// + /// Draws this draw node to the screen. + /// + /// The action to be performed on each vertex of + /// the draw node in order to draw it if required. This is primarily used by + /// textured sprites. + public virtual void Draw(Action vertexAction) + { + GLWrapper.SetBlend(DrawInfo.Blending); + } + } +} diff --git a/osu.Framework/Graphics/Drawable.cs b/osu.Framework/Graphics/Drawable.cs index 943e25702..137bb46fb 100644 --- a/osu.Framework/Graphics/Drawable.cs +++ b/osu.Framework/Graphics/Drawable.cs @@ -1,2340 +1,2340 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Caching; -using osu.Framework.Extensions; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Input; -using osu.Framework.Logging; -using osu.Framework.Statistics; -using osu.Framework.Threading; -using osu.Framework.Timing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Development; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics -{ - /// - /// Drawables are the basic building blocks of a scene graph in this framework. - /// Anything that is visible or that the user interacts with has to be a Drawable. - /// - /// For example: - /// - Boxes - /// - Sprites - /// - Collections of Drawables - /// - /// Drawables are always rectangular in shape in their local coordinate system, - /// which makes them quad-shaped in arbitrary (linearly transformed) coordinate systems. - /// - public abstract class Drawable : Transformable, IDisposable, IDrawable - { - #region Construction and disposal - - protected Drawable() - { - handleKeyboardInput = HandleInputCache.HandleKeyboardInput(this); - handleMouseInput = HandleInputCache.HandleMouseInput(this); - } - - ~Drawable() - { - dispose(false); - } - - /// - /// Disposes this drawable. - /// - public void Dispose() - { - dispose(true); - GC.SuppressFinalize(this); - } - - private bool isDisposed; - - /// - /// Disposes this drawable. - /// - protected virtual void Dispose(bool isDisposing) - { - } - - private void dispose(bool isDisposing) - { - if (isDisposed) - return; - - //we can't dispose if we are mid-load, else our children may get in a bad state. - loadTask?.Wait(); - - Dispose(isDisposing); - - Parent = null; - - scheduler = null; - - OnUpdate = null; - OnInvalidate = null; - - // If this Drawable is disposed, then we need to also - // stop remotely rendering it. - proxy?.Dispose(); - - isDisposed = true; - } - - /// - /// Whether this Drawable should be disposed when it is automatically removed from - /// its due to being false. - /// - public virtual bool DisposeOnDeathRemoval => false; - - #endregion - - #region Loading - - /// - /// Whether this Drawable is fully loaded. - /// Override to false for delaying the load further (e.g. using ). - /// - public bool IsLoaded => loadState >= LoadState.Loaded; - - private volatile LoadState loadState; - - /// - /// Describes the current state of this Drawable within the loading pipeline. - /// - public LoadState LoadState => loadState; - - private Task loadTask; - private readonly object loadLock = new object(); - - /// - /// Loads this Drawable asynchronously. - /// - /// The game to load this Drawable on. - /// - /// The target this Drawable may eventually be loaded into. - /// and are inherited from the target. - /// - /// Callback to be invoked on the update thread after loading is complete. - /// The task which is used for loading and callbacks. - internal Task LoadAsync(Game game, Drawable target, Action onLoaded = null) - { - if (loadState != LoadState.NotLoaded) - throw new InvalidOperationException($@"{nameof(LoadAsync)} may not be called more than once on the same Drawable."); - - loadState = LoadState.Loading; - - return loadTask = Task.Factory.StartNew(() => Load(target.Clock, target.Dependencies), TaskCreationOptions.LongRunning) - .ContinueWith(task => game.Schedule(() => - { - task.ThrowIfFaulted(typeof(RecursiveLoadException)); - onLoaded?.Invoke(); - loadTask = null; - })); - } - - private static readonly StopwatchClock perf = new StopwatchClock(true); - - /// - /// Create a local dependency container which will be used by ourselves and all our nested children. - /// If not overridden, the load-time parent's dependency tree will be used. - /// - /// The parent which should be passed through if we want fallback lookups to work. - /// A new dependency container to be stored for this Drawable. - protected virtual IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => parent; - - /// - /// Contains all dependencies that can be injected into this Drawable using . - /// Add or override dependencies by calling . - /// - public IReadOnlyDependencyContainer Dependencies { get; private set; } - - /// - /// Loads this drawable, including the gathering of dependencies and initialisation of required resources. - /// - /// The clock we should use by default. - /// The dependency tree we will inherit by default. May be extended via - internal void Load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies) - { - // Blocks when loading from another thread already. - double t0 = perf.CurrentTime; - lock (loadLock) - { - double lockDuration = perf.CurrentTime - t0; - if (perf.CurrentTime > 1000 && lockDuration > 50 && ThreadSafety.IsUpdateThread) - Logger.Log($@"Drawable [{ToString()}] load was blocked for {lockDuration:0.00}ms!", LoggingTarget.Performance); - - switch (loadState) - { - case LoadState.Ready: - case LoadState.Loaded: - return; - case LoadState.Loading: - break; - case LoadState.NotLoaded: - loadState = LoadState.Loading; - break; - default: - Trace.Assert(false, "Impossible loading state."); - break; - } - - UpdateClock(clock); - - double t1 = perf.CurrentTime; - - // get our dependencies from our parent, but allow local overriding of our inherited dependency container - Dependencies = CreateLocalDependencies(dependencies); - - Dependencies.Inject(this); - - LoadAsyncComplete(); - - double loadDuration = perf.CurrentTime - t1; - if (perf.CurrentTime > 1000 && loadDuration > 50 && ThreadSafety.IsUpdateThread) - Logger.Log($@"Drawable [{ToString()}] took {loadDuration:0.00}ms to load and was not async!", LoggingTarget.Performance); - loadState = LoadState.Ready; - } - } - - /// - /// Runs once on the update thread after loading has finished. - /// - private bool loadComplete() - { - if (loadState < LoadState.Ready) return false; - - MainThread = Thread.CurrentThread; - scheduler?.SetCurrentThread(MainThread); - - loadState = LoadState.Loaded; - Invalidate(); - LoadComplete(); - - OnLoadComplete?.Invoke(this); - OnLoadComplete = null; - return true; - } - - /// - /// Called after all async loading has completed. - /// - protected virtual void LoadAsyncComplete() - { - } - - /// - /// Play initial animation etc. - /// - protected virtual void LoadComplete() - { - } - - #endregion - - #region Sorting (CreationID / Depth) - - /// - /// Captures the order in which Drawables were added to a . Each Drawable - /// is assigned a monotonically increasing ID upon being added to a . This - /// ID is unique within the . - /// The primary use case of this ID is stable sorting of Drawables with equal . - /// - internal ulong ChildID { get; set; } - - /// - /// Whether this drawable has been added to a parent . Note that this does NOT imply that - /// has been set. - /// This is primarily used to block properties such as that strictly rely on the value of - /// to alert the user of an invalid operation. - /// - internal bool IsPartOfComposite => ChildID != 0; - - /// - /// Whether this drawable is part of its parent's . - /// - public bool IsAlive { get; internal set; } - - private float depth; - - /// - /// Controls which Drawables are behind or in front of other Drawables. - /// This amounts to sorting Drawables by their . - /// A Drawable with higher than another Drawable is - /// drawn behind the other Drawable. - /// - public float Depth - { - get { return depth; } - set - { - if (IsPartOfComposite) - throw new InvalidOperationException( - $"May not change {nameof(Depth)} while inside a parent {nameof(CompositeDrawable)}." + - $"Use the parent's {nameof(CompositeDrawable.ChangeInternalChildDepth)} or {nameof(Container.ChangeChildDepth)} instead."); - - depth = value; - } - } - - #endregion - - #region Periodic tasks (events, Scheduler, Transforms, Update) - - /// - /// This event is fired after the method is called at the end of - /// . It should be used when a simple action should be performed - /// at the end of every update call which does not warrant overriding the Drawable. - /// - public Action OnUpdate; - - /// - /// This event is fired after the method is called. - /// It should be used when a simple action should be performed - /// when the Drawable is loaded which does not warrant overriding the Drawable. - /// - public Action OnLoadComplete; - - /// - /// THIS EVENT PURELY EXISTS FOR THE SCENE GRAPH VISUALIZER. DO NOT USE. - /// This event is fired after the method is called. - /// - internal event Action OnInvalidate; - - private Scheduler scheduler; - internal Thread MainThread { get; private set; } - - /// - /// A lazily-initialized scheduler used to schedule tasks to be invoked in future s calls. - /// The tasks are invoked at the beginning of the method before anything else. - /// - protected Scheduler Scheduler => scheduler ?? (scheduler = new Scheduler(MainThread, Clock)); - - /// - /// Updates this Drawable and all Drawables further down the scene graph. - /// Called once every frame. - /// - /// False if the drawable should not be updated. - public virtual bool UpdateSubTree() - { - if (isDisposed) - throw new ObjectDisposedException(ToString(), "Disposed Drawables may never be in the scene graph."); - - if (ProcessCustomClock) - customClock?.ProcessFrame(); - - if (loadState < LoadState.Ready) - return false; - - if (loadState == LoadState.Ready) - loadComplete(); - - Debug.Assert(loadState == LoadState.Loaded); - - UpdateTransforms(); - - if (!IsPresent) - return true; - - if (scheduler != null) - { - int amountScheduledTasks = scheduler.Update(); - FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks); - } - - Update(); - OnUpdate?.Invoke(this); - return true; - } - - /// - /// Updates all masking calculations for this . - /// This occurs post- to ensure that all updates have taken place. - /// - /// The parent that triggered this update on this . - /// The that defines the masking bounds. - /// Whether masking calculations have taken place. - public virtual bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) - { - if (!IsPresent) - return false; - - if (HasProxy && source != proxy) - return false; - - IsMaskedAway = ComputeIsMaskedAway(maskingBounds); - return true; - } - - /// - /// Computes whether this is masked away. - /// - /// The that defines the masking bounds. - /// Whether this is currently masked away. - protected virtual bool ComputeIsMaskedAway(RectangleF maskingBounds) => !maskingBounds.IntersectsWith(ScreenSpaceDrawQuad.AABBFloat); - - /// - /// Performs a once-per-frame update specific to this Drawable. A more elegant alternative to - /// when deriving from . Note, that this - /// method is always called before Drawables further down the scene graph are updated. - /// - protected virtual void Update() - { - } - - #endregion - - #region Position / Size (with margin) - - private Vector2 position - { - get { return new Vector2(x, y); } - set - { - x = value.X; - y = value.Y; - } - } - - /// - /// Positional offset of to in the - /// 's coordinate system. May be in absolute or relative units - /// (controlled by ). - /// - public Vector2 Position - { - get { return position; } - - set - { - if (position == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Position)} must be finite, but is {value}."); - - position = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - private float x; - private float y; - - /// - /// X component of . - /// - public float X - { - get { return x; } - set - { - if (x == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(X)} must be finite, but is {value}."); - - x = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - /// - /// Y component of . - /// - public float Y - { - get { return y; } - set - { - if (y == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Y)} must be finite, but is {value}."); - - y = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - private Axes relativePositionAxes; - - /// - /// Controls which of are relative w.r.t. - /// 's size (from 0 to 1) rather than absolute. - /// The set in this property are ignored by automatically sizing - /// parents. - /// - /// - /// When setting this property, the is converted such that - /// remains invariant. - /// - public Axes RelativePositionAxes - { - get { return relativePositionAxes; } - set - { - if (value == relativePositionAxes) - return; - - // Convert coordinates from relative to absolute or vice versa - Vector2 conversion = relativeToAbsoluteFactor; - if ((value & Axes.X) > (relativePositionAxes & Axes.X)) - X = conversion.X == 0 ? 0 : X / conversion.X; - else if ((relativePositionAxes & Axes.X) > (value & Axes.X)) - X *= conversion.X; - - if ((value & Axes.Y) > (relativePositionAxes & Axes.Y)) - Y = conversion.Y == 0 ? 0 : Y / conversion.Y; - else if ((relativePositionAxes & Axes.Y) > (value & Axes.Y)) - Y *= conversion.Y; - - relativePositionAxes = value; - - // No invalidation necessary as DrawPosition remains invariant. - } - } - - /// - /// Absolute positional offset of to - /// in the 's coordinate system. - /// - public Vector2 DrawPosition - { - get - { - Vector2 offset = Vector2.Zero; - if (Parent != null && RelativePositionAxes != Axes.None) - { - offset = Parent.RelativeChildOffset; - - if ((RelativePositionAxes & Axes.X) == 0) - offset.X = 0; - - if ((RelativePositionAxes & Axes.Y) == 0) - offset.Y = 0; - } - - return applyRelativeAxes(RelativePositionAxes, Position - offset, FillMode.Stretch); - } - } - - private Vector2 size - { - get { return new Vector2(width, height); } - set - { - width = value.X; - height = value.Y; - } - } - - /// - /// Size of this Drawable in the 's coordinate system. - /// May be in absolute or relative units (controlled by ). - /// - public virtual Vector2 Size - { - get { return size; } - - set - { - if (size == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Size)} must be finite, but is {value}."); - - size = value; - - Invalidate(Invalidation.DrawSize); - } - } - - private float width; - private float height; - - /// - /// X component of . - /// - public virtual float Width - { - get { return width; } - set - { - if (width == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Width)} must be finite, but is {value}."); - - width = value; - - Invalidate(Invalidation.DrawSize); - } - } - - /// - /// Y component of . - /// - public virtual float Height - { - get { return height; } - set - { - if (height == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Height)} must be finite, but is {value}."); - - height = value; - - Invalidate(Invalidation.DrawSize); - } - } - - private Axes relativeSizeAxes; - - /// - /// Controls which are relative sizes w.r.t. 's size - /// (from 0 to 1) in the 's coordinate system, rather than absolute sizes. - /// The set in this property are ignored by automatically sizing - /// parents. - /// - /// - /// If an axis becomes relatively sized and its component of was previously 0, - /// then it automatically becomes 1. In all other cases is converted such that - /// remains invariant across changes of this property. - /// - public virtual Axes RelativeSizeAxes - { - get { return relativeSizeAxes; } - set - { - if (value == relativeSizeAxes) - return; - - // In some cases we cannot easily preserve our size, and so we simply invalidate and - // leave correct sizing to the user. - if (fillMode != FillMode.Stretch && (value == Axes.Both || relativeSizeAxes == Axes.Both)) - Invalidate(Invalidation.DrawSize); - else - { - // Convert coordinates from relative to absolute or vice versa - Vector2 conversion = relativeToAbsoluteFactor; - if ((value & Axes.X) > (relativeSizeAxes & Axes.X)) - Width = conversion.X == 0 ? 0 : Width / conversion.X; - else if ((relativeSizeAxes & Axes.X) > (value & Axes.X)) - Width *= conversion.X; - - if ((value & Axes.Y) > (relativeSizeAxes & Axes.Y)) - Height = conversion.Y == 0 ? 0 : Height / conversion.Y; - else if ((relativeSizeAxes & Axes.Y) > (value & Axes.Y)) - Height *= conversion.Y; - - // No invalidation is necessary as DrawSize remains invariant. - } - - relativeSizeAxes = value; - - if ((relativeSizeAxes & Axes.X) > 0 && Width == 0) Width = 1; - if ((relativeSizeAxes & Axes.Y) > 0 && Height == 0) Height = 1; - - OnSizingChanged(); - } - } - - private Cached drawSizeBacking; - - /// - /// Absolute size of this Drawable in the 's coordinate system. - /// - public Vector2 DrawSize => drawSizeBacking.IsValid ? drawSizeBacking : (drawSizeBacking.Value = applyRelativeAxes(RelativeSizeAxes, Size, FillMode)); - - /// - /// X component of . - /// - public float DrawWidth => DrawSize.X; - - /// - /// Y component of . - /// - public float DrawHeight => DrawSize.Y; - - private MarginPadding margin; - - /// - /// Size of an empty region around this Drawable used to manipulate - /// layout. Does not affect or the region of accepted input, - /// but does affect . - /// - public MarginPadding Margin - { - get { return margin; } - set - { - if (margin.Equals(value)) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Margin)} must be finite, but is {value}."); - - margin = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - /// - /// Absolute size of this Drawable's layout rectangle in the 's - /// coordinate system; i.e. with the addition of . - /// - public Vector2 LayoutSize => DrawSize + new Vector2(margin.TotalHorizontal, margin.TotalVertical); - - /// - /// Absolutely sized rectangle for drawing in the 's coordinate system. - /// Based on . - /// - public RectangleF DrawRectangle - { - get - { - Vector2 s = DrawSize; - return new RectangleF(0, 0, s.X, s.Y); - } - } - - /// - /// Absolutely sized rectangle for layout in the 's coordinate system. - /// Based on and . - /// - public RectangleF LayoutRectangle - { - get - { - Vector2 s = LayoutSize; - return new RectangleF(-margin.Left, -margin.Top, s.X, s.Y); - } - } - - /// - /// Helper function for converting potentially relative coordinates in the - /// 's space to absolute coordinates based on which - /// axes are relative. - /// - /// Describes which axes are relative. - /// The coordinates to convert. - /// The to be used. - /// Absolute coordinates in 's space. - private Vector2 applyRelativeAxes(Axes relativeAxes, Vector2 v, FillMode fillMode) - { - if (relativeAxes != Axes.None) - { - Vector2 conversion = relativeToAbsoluteFactor; - - if ((relativeAxes & Axes.X) > 0) - v.X *= conversion.X; - if ((relativeAxes & Axes.Y) > 0) - v.Y *= conversion.Y; - - // FillMode only makes sense if both axes are relatively sized as the general rule - // for n-dimensional aspect preservation is to simply take the minimum or the maximum - // scale among all active axes. For single axes the minimum / maximum is just the - // value itself. - if (relativeAxes == Axes.Both && fillMode != FillMode.Stretch) - { - if (fillMode == FillMode.Fill) - v = new Vector2(Math.Max(v.X, v.Y * fillAspectRatio)); - else if (fillMode == FillMode.Fit) - v = new Vector2(Math.Min(v.X, v.Y * fillAspectRatio)); - v.Y /= fillAspectRatio; - } - } - return v; - } - - /// - /// Conversion factor from relative to absolute coordinates in the 's space. - /// - private Vector2 relativeToAbsoluteFactor => Parent?.RelativeToAbsoluteFactor ?? Vector2.One; - - private Axes bypassAutoSizeAxes; - - /// - /// Controls which are ignored by parent automatic sizing. - /// Most notably, and do not affect - /// automatic sizing to avoid circular size dependencies. - /// - public Axes BypassAutoSizeAxes - { - get { return bypassAutoSizeAxes | relativeSizeAxes | relativePositionAxes; } - - set - { - if (value == bypassAutoSizeAxes) - return; - - bypassAutoSizeAxes = value; - Parent?.InvalidateFromChild(Invalidation.RequiredParentSizeToFit); - } - } - - /// - /// Computes the bounding box of this drawable in its parent's space. - /// - public virtual RectangleF BoundingBox => ToParentSpace(LayoutRectangle).AABBFloat; - - /// - /// Called whenever the of this drawable is changed, or when the are changed if this drawable is a . - /// - protected virtual void OnSizingChanged() { } - - #endregion - - #region Scale / Shear / Rotation - - private Vector2 scale = Vector2.One; - - /// - /// Base relative scaling factor around . - /// - public Vector2 Scale - { - get { return scale; } - - set - { - if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) - value.X = Precision.FLOAT_EPSILON; - if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) - value.Y = Precision.FLOAT_EPSILON; - - if (scale == value) - return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Scale)} must be finite, but is {value}."); - - scale = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - private float fillAspectRatio = 1; - - /// - /// The desired ratio of width to height when under the effect of a non-stretching - /// and being . - /// - public float FillAspectRatio - { - get { return fillAspectRatio; } - - set - { - if (fillAspectRatio == value) return; - - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be finite, but is {value}."); - if (value == 0) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be non-zero."); - - fillAspectRatio = value; - - if (fillMode != FillMode.Stretch && RelativeSizeAxes == Axes.Both) - Invalidate(Invalidation.DrawSize); - } - } - - private FillMode fillMode; - - /// - /// Controls the behavior of when it is set to . - /// Otherwise, this member has no effect. By default, stretching is used, which simply scales - /// this drawable's according to 's - /// disregarding this drawable's . Other values of preserve . - /// - public FillMode FillMode - { - get { return fillMode; } - - set - { - if (fillMode == value) return; - fillMode = value; - - Invalidate(Invalidation.DrawSize); - } - } - - /// - /// Relative scaling factor around . - /// - protected virtual Vector2 DrawScale => Scale; - - private Vector2 shear = Vector2.Zero; - - /// - /// Relative shearing factor. The X dimension is relative w.r.t. - /// and the Y dimension relative w.r.t. . - /// - public Vector2 Shear - { - get { return shear; } - - set - { - if (shear == value) return; - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Shear)} must be finite, but is {value}."); - - shear = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - private float rotation; - - /// - /// Rotation in degrees around . - /// - public float Rotation - { - get { return rotation; } - - set - { - if (value == rotation) return; - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Rotation)} must be finite, but is {value}."); - - rotation = value; - - Invalidate(Invalidation.MiscGeometry); - } - } - - #endregion - - #region Origin / Anchor - - private Anchor origin = Anchor.TopLeft; - - /// - /// The origin of the local coordinate system of this Drawable. - /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y) - /// or a fixed absolute position via . - /// - public virtual Anchor Origin - { - get { return origin; } - - set - { - if (origin == value) return; - - if (value == 0) - throw new ArgumentException("Cannot set origin to 0.", nameof(value)); - - origin = value; - Invalidate(Invalidation.MiscGeometry); - } - } - - - private Vector2 customOrigin; - - /// - /// The origin of the local coordinate system of this Drawable - /// in relative coordinates expressed in the coordinate system with origin at the - /// top left corner of the (not ). - /// - public Vector2 RelativeOriginPosition - { - get - { - if (Origin == Anchor.Custom) - throw new InvalidOperationException(@"Can not obtain relative origin position for custom origins."); - - Vector2 result = Vector2.Zero; - if ((origin & Anchor.x1) > 0) - result.X = 0.5f; - else if ((origin & Anchor.x2) > 0) - result.X = 1; - - if ((origin & Anchor.y1) > 0) - result.Y = 0.5f; - else if ((origin & Anchor.y2) > 0) - result.Y = 1; - - return result; - } - } - - /// - /// The origin of the local coordinate system of this Drawable - /// in absolute coordinates expressed in the coordinate system with origin at the - /// top left corner of the (not ). - /// - public virtual Vector2 OriginPosition - { - get - { - Vector2 result; - if (Origin == Anchor.Custom) - result = customOrigin; - else if (Origin == Anchor.TopLeft) - result = Vector2.Zero; - else - result = computeAnchorPosition(LayoutSize, Origin); - - return result - new Vector2(margin.Left, margin.Top); - } - - set - { - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(OriginPosition)} must be finite, but is {value}."); - - customOrigin = value; - Origin = Anchor.Custom; - } - } - - - private Anchor anchor = Anchor.TopLeft; - - /// - /// Specifies where is attached to the - /// in the coordinate system with origin at the top left corner of the - /// 's . - /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y) - /// or a fixed absolute position via . - /// - public Anchor Anchor - { - get { return anchor; } - - set - { - if (anchor == value) return; - - if (value == 0) - throw new ArgumentException("Cannot set anchor to 0.", nameof(value)); - - anchor = value; - Invalidate(Invalidation.MiscGeometry); - } - } - - - private Vector2 customRelativeAnchorPosition; - - /// - /// Specifies in relative coordinates where is attached - /// to the in the coordinate system with origin at the top - /// left corner of the 's , and - /// a value of referring to the bottom right corner of - /// the 's . - /// - public Vector2 RelativeAnchorPosition - { - get - { - if (Anchor == Anchor.Custom) - return customRelativeAnchorPosition; - - Vector2 result = Vector2.Zero; - if ((anchor & Anchor.x1) > 0) - result.X = 0.5f; - else if ((anchor & Anchor.x2) > 0) - result.X = 1; - - if ((anchor & Anchor.y1) > 0) - result.Y = 0.5f; - else if ((anchor & Anchor.y2) > 0) - result.Y = 1; - - return result; - } - - set - { - if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeAnchorPosition)} must be finite, but is {value}."); - - customRelativeAnchorPosition = value; - Anchor = Anchor.Custom; - } - } - - /// - /// Specifies in absolute coordinates where is attached - /// to the in the coordinate system with origin at the top - /// left corner of the 's . - /// - public Vector2 AnchorPosition => RelativeAnchorPosition * Parent?.ChildSize ?? Vector2.Zero; - - /// - /// Helper function to compute an absolute position given an absolute size and - /// a relative . - /// - /// Absolute size - /// Relative - /// Absolute position - private static Vector2 computeAnchorPosition(Vector2 size, Anchor anchor) - { - Vector2 result = Vector2.Zero; - - if ((anchor & Anchor.x1) > 0) - result.X = size.X / 2f; - else if ((anchor & Anchor.x2) > 0) - result.X = size.X; - - if ((anchor & Anchor.y1) > 0) - result.Y = size.Y / 2f; - else if ((anchor & Anchor.y2) > 0) - result.Y = size.Y; - - return result; - } - - #endregion - - #region Colour / Alpha / Blending - - private ColourInfo colour = Color4.White; - - /// - /// Colour of this in sRGB space. Can contain individual colours for all four - /// corners of this , which are then interpolated, but can also be assigned - /// just a single colour. Implicit casts from and from exist. - /// - public ColourInfo Colour - { - get { return colour; } - - set - { - if (colour.Equals(value)) return; - - colour = value; - - Invalidate(Invalidation.Colour); - } - } - - private float alpha = 1.0f; - - /// - /// Multiplicative alpha factor applied on top of and its existing - /// alpha channel(s). - /// - public float Alpha - { - get { return alpha; } - - set - { - if (alpha == value) return; - - Invalidate(Invalidation.Colour); - - alpha = value; - } - } - - private const float visibility_cutoff = 0.0001f; - - /// - /// Determines whether this Drawable is present based on its value. - /// Can be forced always on with . - /// - public virtual bool IsPresent => AlwaysPresent || Alpha > visibility_cutoff && Math.Abs(Scale.X) > Precision.FLOAT_EPSILON && Math.Abs(Scale.Y) > Precision.FLOAT_EPSILON; - - private bool alwaysPresent; - - /// - /// If true, forces to always be true. In other words, - /// this drawable is always considered for layout, input, and drawing, regardless - /// of alpha value. - /// - public bool AlwaysPresent - { - get { return alwaysPresent; } - - set - { - if (alwaysPresent == value) return; - - Invalidate(Invalidation.Colour); - - alwaysPresent = value; - } - } - - private BlendingParameters blending; - - /// - /// Determines how this Drawable is blended with other already drawn Drawables. - /// Inherits the 's by default. - /// - public BlendingParameters Blending - { - get { return blending; } - - set - { - if (blending.Equals(value)) - return; - - blending = value; - Invalidate(Invalidation.Colour); - } - } - #endregion - - #region Timekeeping - - private IFrameBasedClock customClock; - private IFrameBasedClock clock; - - /// - /// The clock of this drawable. Used for keeping track of time across - /// frames. By default is inherited from . - /// If set, then the provided value is used as a custom clock and the - /// 's clock is ignored. - /// - public override IFrameBasedClock Clock - { - get { return clock; } - set - { - customClock = value; - UpdateClock(customClock); - } - } - - /// - /// Updates the clock to be used. Has no effect if this drawable - /// uses a custom clock. - /// - /// The new clock to be used. - internal virtual void UpdateClock(IFrameBasedClock clock) - { - this.clock = customClock ?? clock; - scheduler?.UpdateClock(this.clock); - } - - /// - /// Whether should be automatically invoked on this 's - /// in . This should only be set to false in scenarios where the clock is updated elsewhere. - /// - public bool ProcessCustomClock = true; - - /// - /// The time at which this drawable becomes valid (and is considered for drawing). - /// - public virtual double LifetimeStart { get; set; } = double.MinValue; - - /// - /// The time at which this drawable is no longer valid (and is considered for disposal). - /// - public virtual double LifetimeEnd { get; set; } = double.MaxValue; - - /// - /// Whether this drawable should currently be alive. - /// This is queried by the framework to decide the state of this drawable for the next frame. - /// - protected internal virtual bool ShouldBeAlive - { - get - { - if (LifetimeStart == double.MinValue && LifetimeEnd == double.MaxValue) - return true; - - return Time.Current >= LifetimeStart && Time.Current < LifetimeEnd; - } - } - - /// - /// Whether to remove the drawable from its parent's children when it's not alive. - /// - public virtual bool RemoveWhenNotAlive => Parent == null || Time.Current > LifetimeStart; - - #endregion - - #region Parenting (scene graph operations, including ProxyDrawable) - - /// - /// Retrieve the first parent in the tree which derives from . - /// As this is performing an upward tree traversal, avoid calling every frame. - /// - /// The first parent . - protected InputManager GetContainingInputManager() - { - Drawable search = Parent; - while (search != null) - { - var test = search as InputManager; - if (test != null) return test; - - search = search.Parent; - } - return null; - } - - private CompositeDrawable parent; - - /// - /// The parent of this drawable in the scene graph. - /// - public CompositeDrawable Parent - { - get { return parent; } - internal set - { - if (isDisposed) - throw new ObjectDisposedException(ToString(), "Disposed Drawables may never get a parent and return to the scene graph."); - - if (value == null) - ChildID = 0; - - if (parent == value) return; - - if (value != null && parent != null) - throw new InvalidOperationException("May not add a drawable to multiple containers."); - - parent = value; - Invalidate(InvalidationFromParentSize | Invalidation.Colour); - - if (parent != null) - { - //we should already have a clock at this point (from our LoadRequested invocation) - //this just ensures we have the most recent parent clock. - //we may want to consider enforcing that parent.Clock == clock here. - UpdateClock(parent.Clock); - } - } - } - - /// - /// Refers to the original if this drawable was created via - /// . Otherwise refers to this. - /// - internal virtual Drawable Original => this; - - /// - /// True iff has been called before. - /// - internal bool HasProxy => proxy != null; - - private ProxyDrawable proxy; - - /// - /// Creates a proxy drawable which can be inserted elsewhere in the scene graph. - /// Will cause the original instance to not render itself. - /// Creating multiple proxies is not supported and will result in an - /// . - /// - public ProxyDrawable CreateProxy() - { - if (proxy != null) - throw new InvalidOperationException("Multiple proxies are not supported."); - return proxy = new ProxyDrawable(this); - } - - #endregion - - #region Caching & invalidation (for things too expensive to compute every frame) - - /// - /// Was this Drawable masked away completely during the last frame? - /// This is measured conservatively, i.e. it is only true when the Drawable was - /// actually masked away, but it may be false, even if the Drawable was masked away. - /// - internal bool IsMaskedAway { get; private set; } - - private Cached screenSpaceDrawQuadBacking; - - protected virtual Quad ComputeScreenSpaceDrawQuad() => ToScreenSpace(DrawRectangle); - - /// - /// The screen-space quad this drawable occupies. - /// - public virtual Quad ScreenSpaceDrawQuad => screenSpaceDrawQuadBacking.IsValid ? screenSpaceDrawQuadBacking : (screenSpaceDrawQuadBacking.Value = ComputeScreenSpaceDrawQuad()); - - private Cached drawInfoBacking; - - private DrawInfo computeDrawInfo() - { - DrawInfo di = Parent?.DrawInfo ?? new DrawInfo(null); - - Vector2 pos = DrawPosition + AnchorPosition; - Vector2 drawScale = DrawScale; - BlendingParameters localBlending = Blending; - - if (Parent != null) - { - pos += Parent.ChildOffset; - - if (localBlending.Mode == BlendingMode.Inherit) - localBlending.Mode = Parent.Blending.Mode; - - if (localBlending.RGBEquation == BlendingEquation.Inherit) - localBlending.RGBEquation = Parent.Blending.RGBEquation; - - if (localBlending.AlphaEquation == BlendingEquation.Inherit) - localBlending.AlphaEquation = Parent.Blending.AlphaEquation; - } - - di.ApplyTransform(pos, drawScale, Rotation, Shear, OriginPosition); - di.Blending = new BlendingInfo(localBlending); - - ColourInfo drawInfoColour = alpha != 1 ? colour.MultiplyAlpha(alpha) : colour; - - // No need for a Parent null check here, because null parents always have - // a single colour (white). - if (di.Colour.HasSingleColour) - di.Colour.ApplyChild(drawInfoColour); - else - { - Debug.Assert(Parent != null, - $"The {nameof(di)} of null parents should always have the single colour white, and therefore this branch should never be hit."); - - // Cannot use ToParentSpace here, because ToParentSpace depends on DrawInfo to be completed - // ReSharper disable once PossibleNullReferenceException - Quad interp = Quad.FromRectangle(DrawRectangle) * (di.Matrix * Parent.DrawInfo.MatrixInverse); - Vector2 parentSize = Parent.DrawSize; - - interp.TopLeft = Vector2.Divide(interp.TopLeft, parentSize); - interp.TopRight = Vector2.Divide(interp.TopRight, parentSize); - interp.BottomLeft = Vector2.Divide(interp.BottomLeft, parentSize); - interp.BottomRight = Vector2.Divide(interp.BottomRight, parentSize); - - di.Colour.ApplyChild(drawInfoColour, interp); - } - - return di; - } - - /// - /// Contains a linear transformation, colour information, and blending information - /// of this drawable. - /// - public virtual DrawInfo DrawInfo => drawInfoBacking.IsValid ? drawInfoBacking : (drawInfoBacking.Value = computeDrawInfo()); - - - private Cached requiredParentSizeToFitBacking; - - private Vector2 computeRequiredParentSizeToFit() - { - // Auxilary variables required for the computation - Vector2 ap = AnchorPosition; - Vector2 rap = RelativeAnchorPosition; - - Vector2 ratio1 = new Vector2( - rap.X <= 0 ? 0 : 1 / rap.X, - rap.Y <= 0 ? 0 : 1 / rap.Y); - - Vector2 ratio2 = new Vector2( - rap.X >= 1 ? 0 : 1 / (1 - rap.X), - rap.Y >= 1 ? 0 : 1 / (1 - rap.Y)); - - RectangleF bbox = BoundingBox; - - // Compute the required size of the parent such that we fit in snugly when positioned - // at our relative anchor in the parent. - Vector2 topLeftOffset = ap - bbox.TopLeft; - Vector2 topLeftSize1 = topLeftOffset * ratio1; - Vector2 topLeftSize2 = -topLeftOffset * ratio2; - - Vector2 bottomRightOffset = ap - bbox.BottomRight; - Vector2 bottomRightSize1 = bottomRightOffset * ratio1; - Vector2 bottomRightSize2 = -bottomRightOffset * ratio2; - - // Expand bounds according to clipped offset - return Vector2.ComponentMax( - Vector2.ComponentMax(topLeftSize1, topLeftSize2), - Vector2.ComponentMax(bottomRightSize1, bottomRightSize2)); - } - - /// - /// Returns the size of the smallest axis aligned box in parent space which - /// encompasses this drawable while preserving this drawable's - /// . - /// If a component of is smaller than zero - /// or larger than one, then it is impossible to preserve - /// while fitting into the parent, and thus returns - /// zero in that dimension; i.e. we no longer fit into the parent. - /// This behavior is prominent with non-centre and non-custom values. - /// - internal Vector2 RequiredParentSizeToFit => requiredParentSizeToFitBacking.IsValid ? requiredParentSizeToFitBacking : (requiredParentSizeToFitBacking.Value = computeRequiredParentSizeToFit()); - - - private static readonly AtomicCounter invalidation_counter = new AtomicCounter(); - - // Make sure we start out with a value of 1 such that ApplyDrawNode is always called at least once - private long invalidationID = invalidation_counter.Increment(); - - /// - /// Invalidates draw matrix and autosize caches. - /// - /// This does not ensure that the parent containers have been updated before us, thus operations involving - /// parent states (e.g. ) should not be executed in an overriden implementation. - /// - /// - /// If the invalidate was actually necessary. - public virtual bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if (invalidation == Invalidation.None || !IsLoaded) - return false; - - if (shallPropagate && Parent != null && source != Parent) - Parent.InvalidateFromChild(invalidation); - - bool alreadyInvalidated = true; - - // Either ScreenSize OR ScreenPosition OR Colour - if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) - { - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - alreadyInvalidated &= !requiredParentSizeToFitBacking.Invalidate(); - - alreadyInvalidated &= !screenSpaceDrawQuadBacking.Invalidate(); - alreadyInvalidated &= !drawInfoBacking.Invalidate(); - alreadyInvalidated &= !drawSizeBacking.Invalidate(); - } - - if (!alreadyInvalidated || (invalidation & Invalidation.DrawNode) > 0) - invalidationID = invalidation_counter.Increment(); - - OnInvalidate?.Invoke(this); - - return !alreadyInvalidated; - } - - public Invalidation InvalidationFromParentSize - { - get - { - Invalidation result = Invalidation.DrawInfo; - if (RelativeSizeAxes != Axes.None) - result |= Invalidation.DrawSize; - if (RelativePositionAxes != Axes.None) - result |= Invalidation.MiscGeometry; - return result; - } - } - - #endregion - - #region DrawNode - - private readonly DrawNode[] drawNodes = new DrawNode[3]; - - /// - /// Generates the DrawNode for ourselves. - /// - /// A complete and updated DrawNode, or null if the DrawNode would be invisible. - internal virtual DrawNode GenerateDrawNodeSubtree(int treeIndex) - { - DrawNode node = drawNodes[treeIndex]; - if (node == null) - { - drawNodes[treeIndex] = node = CreateDrawNode(); - FrameStatistics.Increment(StatisticsCounterType.DrawNodeCtor); - } - - if (invalidationID != node.InvalidationID) - { - ApplyDrawNode(node); - FrameStatistics.Increment(StatisticsCounterType.DrawNodeAppl); - } - - return node; - } - - /// - /// Fills a given draw node with all information required to draw this drawable. - /// - /// The node to fill with information. - protected virtual void ApplyDrawNode(DrawNode node) - { - node.DrawInfo = DrawInfo; - node.InvalidationID = invalidationID; - } - - /// - /// Creates a draw node capable of containing all information required to draw this drawable. - /// - /// The created draw node. - protected virtual DrawNode CreateDrawNode() => new DrawNode(); - - #endregion - - #region DrawInfo-based coordinate system conversions - - /// - /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space. - /// - /// A vector in local coordinates. - /// The drawable in which space we want to transform the vector to. - /// The vector in other's coordinates. - public Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other) - { - if (other == this) - return input; - - return Vector2Extensions.Transform(Vector2Extensions.Transform(input, DrawInfo.Matrix), other.DrawInfo.MatrixInverse); - } - - /// - /// Accepts a rectangle in local coordinates and converts it to coordinates in another Drawable's space. - /// - /// A rectangle in local coordinates. - /// The drawable in which space we want to transform the rectangle to. - /// The rectangle in other's coordinates. - public Quad ToSpaceOfOtherDrawable(RectangleF input, IDrawable other) - { - if (other == this) - return input; - - return Quad.FromRectangle(input) * (DrawInfo.Matrix * other.DrawInfo.MatrixInverse); - } - - /// - /// Accepts a vector in local coordinates and converts it to coordinates in Parent's space. - /// - /// A vector in local coordinates. - /// The vector in Parent's coordinates. - public Vector2 ToParentSpace(Vector2 input) => ToSpaceOfOtherDrawable(input, Parent); - - /// - /// Accepts a rectangle in local coordinates and converts it to a quad in Parent's space. - /// - /// A rectangle in local coordinates. - /// The quad in Parent's coordinates. - public Quad ToParentSpace(RectangleF input) => ToSpaceOfOtherDrawable(input, Parent); - - /// - /// Accepts a vector in local coordinates and converts it to coordinates in screen space. - /// - /// A vector in local coordinates. - /// The vector in screen coordinates. - public Vector2 ToScreenSpace(Vector2 input) - { - return Vector2Extensions.Transform(input, DrawInfo.Matrix); - } - - /// - /// Accepts a rectangle in local coordinates and converts it to a quad in screen space. - /// - /// A rectangle in local coordinates. - /// The quad in screen coordinates. - public Quad ToScreenSpace(RectangleF input) - { - return Quad.FromRectangle(input) * DrawInfo.Matrix; - } - - /// - /// Accepts a vector in screen coordinates and converts it to coordinates in local space. - /// - /// A vector in screen coordinates. - /// The vector in local coordinates. - public Vector2 ToLocalSpace(Vector2 screenSpacePos) - { - return Vector2Extensions.Transform(screenSpacePos, DrawInfo.MatrixInverse); - } - - /// - /// Accepts a quad in screen coordinates and converts it to coordinates in local space. - /// - /// A quad in screen coordinates. - /// The quad in local coordinates. - public Quad ToLocalSpace(Quad screenSpaceQuad) - { - return screenSpaceQuad * DrawInfo.MatrixInverse; - } - - #endregion - - #region Interaction / Input - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnHover(InputState screenSpaceState) => OnHover(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered once when this Drawable becomes hovered. - /// - /// The state at which the Drawable becomes hovered. - /// True if this Drawable would like to handle the hover. If so, then - /// no further Drawables up the scene graph will receive hovering events. If - /// false, however, then will still be - /// received once hover is lost. - protected virtual bool OnHover(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public void TriggerOnHoverLost(InputState screenSpaceState) => OnHoverLost(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever this drawable is no longer hovered. - /// - /// The state at which hover is lost. - protected virtual void OnHoverLost(InputState state) - { - } - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnMouseDown(InputState screenSpaceState = null, MouseDownEventArgs args = null) => OnMouseDown(createCloneInParentSpace(screenSpaceState), args); - - /// - /// Triggered whenever a mouse button is pressed on top of this Drawable. - /// - /// The state after the press. - /// Specific arguments for mouse down event. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnMouseDown(InputState state, MouseDownEventArgs args) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnMouseUp(InputState screenSpaceState = null, MouseUpEventArgs args = null) => OnMouseUp(createCloneInParentSpace(screenSpaceState), args); - - /// - /// Triggered whenever a mouse button is released on top of this Drawable. - /// - /// The state after the release. - /// Specific arguments for mouse up event. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnMouseUp(InputState state, MouseUpEventArgs args) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnClick(InputState screenSpaceState = null) => OnClick(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever a mouse click occurs on top of this Drawable. - /// - /// The state after the click. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnClick(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnDoubleClick(InputState screenSpaceState) => OnDoubleClick(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever a mouse double click occurs on top of this Drawable. - /// - /// The state after the double click. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnDoubleClick(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnDragStart(InputState screenSpaceState) => OnDragStart(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever this Drawable is initially dragged by a held mouse click - /// and subsequent movement. - /// - /// The state after the mouse was moved. - /// True if this Drawable accepts being dragged. If so, then future - /// and - /// events will be received. Otherwise, the event is propagated up the scene - /// graph to the next eligible Drawable. - protected virtual bool OnDragStart(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnDrag(InputState screenSpaceState) => OnDrag(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever the mouse is moved while dragging. - /// Only is received if a drag was previously initiated by returning true - /// from . - /// - /// The state after the mouse was moved. - /// Currently unused. - protected virtual bool OnDrag(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnDragEnd(InputState screenSpaceState) => OnDragEnd(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever a drag ended. Only is received if a drag was previously - /// initiated by returning true from . - /// - /// The state after the drag ended. - /// Currently unused. - protected virtual bool OnDragEnd(InputState state) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnWheel(InputState screenSpaceState) => OnWheel(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever the mouse wheel was turned over this Drawable. - /// - /// The state after the wheel was turned. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnWheel(InputState state) => false; - - /// - /// Triggers with a local version of the given - /// - /// The input state. - public void TriggerOnFocus(InputState screenSpaceState = null) => OnFocus(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever this Drawable gains focus. - /// Focused Drawables receive keyboard input before all other Drawables, - /// and thus handle it first. - /// - /// The state after focus when focus can be gained. - protected virtual void OnFocus(InputState state) - { - } - - /// - /// Triggers with a local version of the given - /// - /// The input state. - public void TriggerOnFocusLost(InputState screenSpaceState = null) => OnFocusLost(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever this Drawable lost focus. - /// - /// The state after focus was lost. - protected virtual void OnFocusLost(InputState state) - { - } - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnKeyDown(InputState screenSpaceState, KeyDownEventArgs args) => OnKeyDown(createCloneInParentSpace(screenSpaceState), args); - - /// - /// Triggered whenever a key was pressed. - /// - /// The state after the key was pressed. - /// Specific arguments for key down event. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnKeyDown(InputState state, KeyDownEventArgs args) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnKeyUp(InputState screenSpaceState, KeyUpEventArgs args) => OnKeyUp(createCloneInParentSpace(screenSpaceState), args); - - /// - /// Triggered whenever a key was released. - /// - /// The state after the key was released. - /// Specific arguments for key up event. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnKeyUp(InputState state, KeyUpEventArgs args) => false; - - /// - /// Triggers with a local version of the given . - /// - public bool TriggerOnMouseMove(InputState screenSpaceState) => OnMouseMove(createCloneInParentSpace(screenSpaceState)); - - /// - /// Triggered whenever the mouse moved over this Drawable. - /// - /// The state after the mouse moved. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - protected virtual bool OnMouseMove(InputState state) => false; - - private readonly bool handleKeyboardInput, handleMouseInput; - - /// - /// Whether this handles keyboard input. - /// This value is true by default if any keyboard related "On-" input methods are overridden. - /// - public virtual bool HandleKeyboardInput => handleKeyboardInput; - - /// - /// Whether this handles mouse input. - /// This value is true by default if any mouse related "On-" input methods are overridden. - /// - public virtual bool HandleMouseInput => handleMouseInput; - - /// - /// Nested class which is used for caching , values obtained via reflection. - /// - private static class HandleInputCache - { - private static readonly ConcurrentDictionary mouse_cached_values = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary keyboard_cached_values = new ConcurrentDictionary(); - - private static readonly string[] mouse_input_methods = { - nameof(OnHover), - nameof(OnHoverLost), - nameof(OnMouseDown), - nameof(OnMouseUp), - nameof(OnClick), - nameof(OnDoubleClick), - nameof(OnDragStart), - nameof(OnDrag), - nameof(OnDragEnd), - nameof(OnWheel), - nameof(OnFocus), - nameof(OnFocusLost), - nameof(OnMouseMove) - }; - - private static readonly string[] keyboard_input_methods = { - nameof(OnFocus), - nameof(OnFocusLost), - nameof(OnKeyDown), - nameof(OnKeyUp) - }; - - public static bool HandleKeyboardInput(Drawable drawable) => get(drawable, keyboard_cached_values, keyboard_input_methods); - - public static bool HandleMouseInput(Drawable drawable) => get(drawable, mouse_cached_values, mouse_input_methods); - - private static bool get(Drawable drawable, ConcurrentDictionary cache, string[] inputMethods) - { - var type = drawable.GetType(); - if (cache.TryGetValue(type, out var value)) - return value; - - foreach (var inputMethod in inputMethods) - { - // check for any input method overrides which are at a higher level than drawable. - var method = type.GetMethod(inputMethod, BindingFlags.Instance | BindingFlags.NonPublic); - - Debug.Assert(method != null); - - // ReSharper disable once PossibleNullReferenceException - if (method.DeclaringType != typeof(Drawable)) - { - cache.TryAdd(type, true); - return true; - } - } - - cache.TryAdd(type, false); - return false; - } - } - - /// - /// Check whether we have active focus. - /// - public bool HasFocus { get; internal set; } - - /// - /// If true, we are eagerly requesting focus. If nothing else above us has (or is requesting focus) we will get it. - /// - public virtual bool RequestsFocus => false; - - /// - /// If true, we will gain focus (receiving priority on keybaord input) (and receive an event) on returning true in . - /// - public virtual bool AcceptsFocus => false; - - /// - /// Whether this Drawable is currently hovered over. - /// - public bool IsHovered { get; internal set; } - - /// - /// Whether this Drawable is currently being dragged. - /// - public bool IsDragged { get; internal set; } - - /// - /// Determines whether this drawable receives mouse input when the mouse is at the - /// given screen-space position. - /// - /// The screen-space position where input could be received. - /// True iff input is received at the given screen-space position. - public virtual bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Contains(screenSpacePos); - - /// - /// Computes whether a given screen-space position is contained within this drawable. - /// Mouse input events are only received when this function is true, or when the drawable - /// is in focus. - /// - /// The screen space position to be checked against this drawable. - public virtual bool Contains(Vector2 screenSpacePos) => DrawRectangle.Contains(ToLocalSpace(screenSpacePos)); - - /// - /// Whether this Drawable can keyboard receive input, taking into account all optimizations and masking. - /// - public bool CanReceiveKeyboardInput => HandleKeyboardInput && IsPresent && !IsMaskedAway; - - /// - /// Whether this Drawable can mouse receive input, taking into account all optimizations and masking. - /// - public bool CanReceiveMouseInput => HandleMouseInput && IsPresent && !IsMaskedAway; - - /// - /// Creates a new InputState with mouse coodinates converted to the coordinate space of our parent. - /// - /// The screen-space input state to be cloned and transformed. - /// The cloned and transformed state. - private InputState createCloneInParentSpace(InputState screenSpaceState) - { - if (screenSpaceState == null) return null; - - var clone = screenSpaceState.Clone(); - clone.Mouse = new LocalMouseState(screenSpaceState.Mouse.NativeState, this); - return clone; - } - - /// - /// This method is responsible for building a queue of Drawables to receive keyboard input - /// in reverse order. This method is overridden by to be called on all - /// children such that the entire scene graph is covered. - /// - /// The input queue to be built. - /// Whether we have added ourself to the queue. - internal virtual bool BuildKeyboardInputQueue(List queue) - { - if (!CanReceiveKeyboardInput) - return false; - - queue.Add(this); - return true; - } - - /// - /// This method is responsible for building a queue of Drawables to receive mouse input - /// in reverse order. This method is overridden by to be called on all - /// children such that the entire scene graph is covered. - /// - /// The current position of the mouse cursor in screen space. - /// The input queue to be built. - /// Whether we have added ourself to the queue. - internal virtual bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) - { - if (!CanReceiveMouseInput || !ReceiveMouseInputAt(screenSpaceMousePos)) - return false; - - queue.Add(this); - return true; - } - - private struct LocalMouseState : IMouseState - { - public IMouseState NativeState { get; } - - public IMouseState LastState { get; set; } - - private readonly Drawable us; - - public LocalMouseState(IMouseState state, Drawable us) - { - NativeState = state; - LastState = null; - this.us = us; - } - - public IReadOnlyList Buttons => NativeState.Buttons; - - public Vector2 Delta => Position - LastPosition; - - public Vector2 Position => us.Parent?.ToLocalSpace(NativeState.Position) ?? NativeState.Position; - - public Vector2 LastPosition => us.Parent?.ToLocalSpace(NativeState.LastPosition) ?? NativeState.LastPosition; - - public Vector2? PositionMouseDown - { - get { return NativeState.PositionMouseDown == null ? null : us.Parent?.ToLocalSpace(NativeState.PositionMouseDown.Value) ?? NativeState.PositionMouseDown; } - set { throw new NotImplementedException(); } - } - - public bool HasMainButtonPressed => NativeState.HasMainButtonPressed; - - public bool HasAnyButtonPressed => NativeState.HasAnyButtonPressed; - - public int Wheel => NativeState.Wheel; - public int WheelDelta => NativeState.WheelDelta; - - public bool IsPressed(MouseButton button) => NativeState.IsPressed(button); - - public void SetPressed(MouseButton button, bool pressed) => NativeState.SetPressed(button, pressed); - - public IMouseState Clone() - { - return (LocalMouseState)MemberwiseClone(); - } - } - - #endregion - - #region Transforms - - protected internal ScheduledDelegate Schedule(Action action) => Scheduler.AddDelayed(action, TransformDelay); - - /// - /// Make this drawable automatically clean itself up after all transforms have finished playing. - /// Can be delayed using Delay(). - /// - public void Expire(bool calculateLifetimeStart = false) - { - if (clock == null) - { - LifetimeEnd = double.MinValue; - return; - } - - LifetimeEnd = LatestTransformEndTime; - - if (calculateLifetimeStart) - { - double min = double.MaxValue; - foreach (Transform t in Transforms) - if (t.StartTime < min) min = t.StartTime; - LifetimeStart = min < int.MaxValue ? min : int.MinValue; - } - } - - /// - /// Hide sprite instantly. - /// - public virtual void Hide() => this.FadeOut(); - - /// - /// Show sprite instantly. - /// - public virtual void Show() => this.FadeIn(); - - #endregion - - #region Effects - - /// - /// Returns the drawable created by applying the given effect to this drawable. This method may add this drawable to a container. - /// If this drawable should be the child of another container, make sure to add the created drawable to the container instead of this drawable. - /// - /// The type of the drawable that results from applying the given effect. - /// The effect to apply to this drawable. - /// The action that should get called to initialize the created drawable before it is returned. - /// The drawable created by applying the given effect to this drawable. - public T WithEffect(IEffect effect, Action initializationAction = null) where T : Drawable - { - var result = effect.ApplyTo(this); - initializationAction?.Invoke(result); - return result; - } - - #endregion - - /// - /// A name used to identify this Drawable internally. - /// - public string Name = string.Empty; - - public override string ToString() - { - string shortClass = GetType().ReadableName(); - - if (!string.IsNullOrEmpty(Name)) - shortClass = $@"{Name} ({shortClass})"; - - return $@"{shortClass} ({DrawPosition.X:#,0},{DrawPosition.Y:#,0}) {DrawSize.X:#,0}x{DrawSize.Y:#,0}"; - } - } - - /// - /// Specifies which type of properties are being invalidated. - /// - [Flags] - public enum Invalidation - { - /// - /// has changed. No change to or - /// is assumed unless indicated by additional flags. - /// - DrawInfo = 1 << 0, - /// - /// has changed. - /// - DrawSize = 1 << 1, - /// - /// Captures all other geometry changes than , such as - /// , , and . - /// - MiscGeometry = 1 << 2, - /// - /// Our colour changed. - /// - Colour = 1 << 3, - /// - /// has to be invoked on all old draw nodes. - /// - DrawNode = 1 << 4, - - /// - /// No invalidation. - /// - None = 0, - /// - /// has to be recomputed. - /// - RequiredParentSizeToFit = MiscGeometry | DrawSize, - /// - /// All possible things are affected. - /// - All = DrawNode | RequiredParentSizeToFit | Colour | DrawInfo, - } - - /// - /// General enum to specify an "anchor" or "origin" point from the standard 9 points on a rectangle. - /// x and y counterparts can be accessed using bitwise flags. - /// - [Flags] - public enum Anchor - { - TopLeft = y0 | x0, - TopCentre = y0 | x1, - TopRight = y0 | x2, - - CentreLeft = y1 | x0, - Centre = y1 | x1, - CentreRight = y1 | x2, - - BottomLeft = y2 | x0, - BottomCentre = y2 | x1, - BottomRight = y2 | x2, - - /// - /// The vertical counterpart is at "Top" position. - /// - y0 = 1 << 0, - - /// - /// The vertical counterpart is at "Centre" position. - /// - y1 = 1 << 1, - - /// - /// The vertical counterpart is at "Bottom" position. - /// - y2 = 1 << 2, - - /// - /// The horizontal counterpart is at "Left" position. - /// - x0 = 1 << 3, - - /// - /// The horizontal counterpart is at "Centre" position. - /// - x1 = 1 << 4, - - /// - /// The horizontal counterpart is at "Right" position. - /// - x2 = 1 << 5, - - /// - /// The user is manually updating the outcome, so we shouldn't. - /// - Custom = 1 << 6, - } - - [Flags] - public enum Axes - { - None = 0, - - X = 1 << 0, - Y = 1 << 1, - - Both = X | Y, - } - - public enum Direction - { - Horizontal, - Vertical, - } - - public enum RotationDirection - { - Clockwise, - CounterClockwise, - } - - /// - /// Possible states of a within the loading pipeline. - /// - public enum LoadState - { - /// - /// Not loaded, and no load has been initiated yet. - /// - NotLoaded, - /// - /// Currently loading (possibly and usually on a background - /// thread via ). - /// - Loading, - /// - /// Loading is complete, but has not yet been finalized on the update thread - /// ( has not been called yet, which - /// always runs on the update thread and requires ). - /// - Ready, - /// - /// Loading is fully completed and the Drawable is now part of the scene graph. - /// - Loaded - } - - /// - /// Controls the behavior of when it is set to . - /// - public enum FillMode - { - /// - /// Completely fill the parent with a relative size of 1 at the cost of stretching the aspect ratio (default). - /// - Stretch, - /// - /// Always maintains aspect ratio while filling the portion of the parent's size denoted by the relative size. - /// A relative size of 1 results in completely filling the parent by scaling the smaller axis of the drawable to fill the parent. - /// - Fill, - /// - /// Always maintains aspect ratio while fitting into the portion of the parent's size denoted by the relative size. - /// A relative size of 1 results in fitting exactly into the parent by scaling the larger axis of the drawable to fit into the parent. - /// - Fit, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Input; +using osu.Framework.Logging; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using osu.Framework.Timing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Development; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics +{ + /// + /// Drawables are the basic building blocks of a scene graph in this framework. + /// Anything that is visible or that the user interacts with has to be a Drawable. + /// + /// For example: + /// - Boxes + /// - Sprites + /// - Collections of Drawables + /// + /// Drawables are always rectangular in shape in their local coordinate system, + /// which makes them quad-shaped in arbitrary (linearly transformed) coordinate systems. + /// + public abstract class Drawable : Transformable, IDisposable, IDrawable + { + #region Construction and disposal + + protected Drawable() + { + handleKeyboardInput = HandleInputCache.HandleKeyboardInput(this); + handleMouseInput = HandleInputCache.HandleMouseInput(this); + } + + ~Drawable() + { + dispose(false); + } + + /// + /// Disposes this drawable. + /// + public void Dispose() + { + dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + /// + /// Disposes this drawable. + /// + protected virtual void Dispose(bool isDisposing) + { + } + + private void dispose(bool isDisposing) + { + if (isDisposed) + return; + + //we can't dispose if we are mid-load, else our children may get in a bad state. + loadTask?.Wait(); + + Dispose(isDisposing); + + Parent = null; + + scheduler = null; + + OnUpdate = null; + OnInvalidate = null; + + // If this Drawable is disposed, then we need to also + // stop remotely rendering it. + proxy?.Dispose(); + + isDisposed = true; + } + + /// + /// Whether this Drawable should be disposed when it is automatically removed from + /// its due to being false. + /// + public virtual bool DisposeOnDeathRemoval => false; + + #endregion + + #region Loading + + /// + /// Whether this Drawable is fully loaded. + /// Override to false for delaying the load further (e.g. using ). + /// + public bool IsLoaded => loadState >= LoadState.Loaded; + + private volatile LoadState loadState; + + /// + /// Describes the current state of this Drawable within the loading pipeline. + /// + public LoadState LoadState => loadState; + + private Task loadTask; + private readonly object loadLock = new object(); + + /// + /// Loads this Drawable asynchronously. + /// + /// The game to load this Drawable on. + /// + /// The target this Drawable may eventually be loaded into. + /// and are inherited from the target. + /// + /// Callback to be invoked on the update thread after loading is complete. + /// The task which is used for loading and callbacks. + internal Task LoadAsync(Game game, Drawable target, Action onLoaded = null) + { + if (loadState != LoadState.NotLoaded) + throw new InvalidOperationException($@"{nameof(LoadAsync)} may not be called more than once on the same Drawable."); + + loadState = LoadState.Loading; + + return loadTask = Task.Factory.StartNew(() => Load(target.Clock, target.Dependencies), TaskCreationOptions.LongRunning) + .ContinueWith(task => game.Schedule(() => + { + task.ThrowIfFaulted(typeof(RecursiveLoadException)); + onLoaded?.Invoke(); + loadTask = null; + })); + } + + private static readonly StopwatchClock perf = new StopwatchClock(true); + + /// + /// Create a local dependency container which will be used by ourselves and all our nested children. + /// If not overridden, the load-time parent's dependency tree will be used. + /// + /// The parent which should be passed through if we want fallback lookups to work. + /// A new dependency container to be stored for this Drawable. + protected virtual IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => parent; + + /// + /// Contains all dependencies that can be injected into this Drawable using . + /// Add or override dependencies by calling . + /// + public IReadOnlyDependencyContainer Dependencies { get; private set; } + + /// + /// Loads this drawable, including the gathering of dependencies and initialisation of required resources. + /// + /// The clock we should use by default. + /// The dependency tree we will inherit by default. May be extended via + internal void Load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies) + { + // Blocks when loading from another thread already. + double t0 = perf.CurrentTime; + lock (loadLock) + { + double lockDuration = perf.CurrentTime - t0; + if (perf.CurrentTime > 1000 && lockDuration > 50 && ThreadSafety.IsUpdateThread) + Logger.Log($@"Drawable [{ToString()}] load was blocked for {lockDuration:0.00}ms!", LoggingTarget.Performance); + + switch (loadState) + { + case LoadState.Ready: + case LoadState.Loaded: + return; + case LoadState.Loading: + break; + case LoadState.NotLoaded: + loadState = LoadState.Loading; + break; + default: + Trace.Assert(false, "Impossible loading state."); + break; + } + + UpdateClock(clock); + + double t1 = perf.CurrentTime; + + // get our dependencies from our parent, but allow local overriding of our inherited dependency container + Dependencies = CreateLocalDependencies(dependencies); + + Dependencies.Inject(this); + + LoadAsyncComplete(); + + double loadDuration = perf.CurrentTime - t1; + if (perf.CurrentTime > 1000 && loadDuration > 50 && ThreadSafety.IsUpdateThread) + Logger.Log($@"Drawable [{ToString()}] took {loadDuration:0.00}ms to load and was not async!", LoggingTarget.Performance); + loadState = LoadState.Ready; + } + } + + /// + /// Runs once on the update thread after loading has finished. + /// + private bool loadComplete() + { + if (loadState < LoadState.Ready) return false; + + MainThread = Thread.CurrentThread; + scheduler?.SetCurrentThread(MainThread); + + loadState = LoadState.Loaded; + Invalidate(); + LoadComplete(); + + OnLoadComplete?.Invoke(this); + OnLoadComplete = null; + return true; + } + + /// + /// Called after all async loading has completed. + /// + protected virtual void LoadAsyncComplete() + { + } + + /// + /// Play initial animation etc. + /// + protected virtual void LoadComplete() + { + } + + #endregion + + #region Sorting (CreationID / Depth) + + /// + /// Captures the order in which Drawables were added to a . Each Drawable + /// is assigned a monotonically increasing ID upon being added to a . This + /// ID is unique within the . + /// The primary use case of this ID is stable sorting of Drawables with equal . + /// + internal ulong ChildID { get; set; } + + /// + /// Whether this drawable has been added to a parent . Note that this does NOT imply that + /// has been set. + /// This is primarily used to block properties such as that strictly rely on the value of + /// to alert the user of an invalid operation. + /// + internal bool IsPartOfComposite => ChildID != 0; + + /// + /// Whether this drawable is part of its parent's . + /// + public bool IsAlive { get; internal set; } + + private float depth; + + /// + /// Controls which Drawables are behind or in front of other Drawables. + /// This amounts to sorting Drawables by their . + /// A Drawable with higher than another Drawable is + /// drawn behind the other Drawable. + /// + public float Depth + { + get { return depth; } + set + { + if (IsPartOfComposite) + throw new InvalidOperationException( + $"May not change {nameof(Depth)} while inside a parent {nameof(CompositeDrawable)}." + + $"Use the parent's {nameof(CompositeDrawable.ChangeInternalChildDepth)} or {nameof(Container.ChangeChildDepth)} instead."); + + depth = value; + } + } + + #endregion + + #region Periodic tasks (events, Scheduler, Transforms, Update) + + /// + /// This event is fired after the method is called at the end of + /// . It should be used when a simple action should be performed + /// at the end of every update call which does not warrant overriding the Drawable. + /// + public Action OnUpdate; + + /// + /// This event is fired after the method is called. + /// It should be used when a simple action should be performed + /// when the Drawable is loaded which does not warrant overriding the Drawable. + /// + public Action OnLoadComplete; + + /// + /// THIS EVENT PURELY EXISTS FOR THE SCENE GRAPH VISUALIZER. DO NOT USE. + /// This event is fired after the method is called. + /// + internal event Action OnInvalidate; + + private Scheduler scheduler; + internal Thread MainThread { get; private set; } + + /// + /// A lazily-initialized scheduler used to schedule tasks to be invoked in future s calls. + /// The tasks are invoked at the beginning of the method before anything else. + /// + protected Scheduler Scheduler => scheduler ?? (scheduler = new Scheduler(MainThread, Clock)); + + /// + /// Updates this Drawable and all Drawables further down the scene graph. + /// Called once every frame. + /// + /// False if the drawable should not be updated. + public virtual bool UpdateSubTree() + { + if (isDisposed) + throw new ObjectDisposedException(ToString(), "Disposed Drawables may never be in the scene graph."); + + if (ProcessCustomClock) + customClock?.ProcessFrame(); + + if (loadState < LoadState.Ready) + return false; + + if (loadState == LoadState.Ready) + loadComplete(); + + Debug.Assert(loadState == LoadState.Loaded); + + UpdateTransforms(); + + if (!IsPresent) + return true; + + if (scheduler != null) + { + int amountScheduledTasks = scheduler.Update(); + FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks); + } + + Update(); + OnUpdate?.Invoke(this); + return true; + } + + /// + /// Updates all masking calculations for this . + /// This occurs post- to ensure that all updates have taken place. + /// + /// The parent that triggered this update on this . + /// The that defines the masking bounds. + /// Whether masking calculations have taken place. + public virtual bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + { + if (!IsPresent) + return false; + + if (HasProxy && source != proxy) + return false; + + IsMaskedAway = ComputeIsMaskedAway(maskingBounds); + return true; + } + + /// + /// Computes whether this is masked away. + /// + /// The that defines the masking bounds. + /// Whether this is currently masked away. + protected virtual bool ComputeIsMaskedAway(RectangleF maskingBounds) => !maskingBounds.IntersectsWith(ScreenSpaceDrawQuad.AABBFloat); + + /// + /// Performs a once-per-frame update specific to this Drawable. A more elegant alternative to + /// when deriving from . Note, that this + /// method is always called before Drawables further down the scene graph are updated. + /// + protected virtual void Update() + { + } + + #endregion + + #region Position / Size (with margin) + + private Vector2 position + { + get { return new Vector2(x, y); } + set + { + x = value.X; + y = value.Y; + } + } + + /// + /// Positional offset of to in the + /// 's coordinate system. May be in absolute or relative units + /// (controlled by ). + /// + public Vector2 Position + { + get { return position; } + + set + { + if (position == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Position)} must be finite, but is {value}."); + + position = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + private float x; + private float y; + + /// + /// X component of . + /// + public float X + { + get { return x; } + set + { + if (x == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(X)} must be finite, but is {value}."); + + x = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + /// + /// Y component of . + /// + public float Y + { + get { return y; } + set + { + if (y == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Y)} must be finite, but is {value}."); + + y = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + private Axes relativePositionAxes; + + /// + /// Controls which of are relative w.r.t. + /// 's size (from 0 to 1) rather than absolute. + /// The set in this property are ignored by automatically sizing + /// parents. + /// + /// + /// When setting this property, the is converted such that + /// remains invariant. + /// + public Axes RelativePositionAxes + { + get { return relativePositionAxes; } + set + { + if (value == relativePositionAxes) + return; + + // Convert coordinates from relative to absolute or vice versa + Vector2 conversion = relativeToAbsoluteFactor; + if ((value & Axes.X) > (relativePositionAxes & Axes.X)) + X = conversion.X == 0 ? 0 : X / conversion.X; + else if ((relativePositionAxes & Axes.X) > (value & Axes.X)) + X *= conversion.X; + + if ((value & Axes.Y) > (relativePositionAxes & Axes.Y)) + Y = conversion.Y == 0 ? 0 : Y / conversion.Y; + else if ((relativePositionAxes & Axes.Y) > (value & Axes.Y)) + Y *= conversion.Y; + + relativePositionAxes = value; + + // No invalidation necessary as DrawPosition remains invariant. + } + } + + /// + /// Absolute positional offset of to + /// in the 's coordinate system. + /// + public Vector2 DrawPosition + { + get + { + Vector2 offset = Vector2.Zero; + if (Parent != null && RelativePositionAxes != Axes.None) + { + offset = Parent.RelativeChildOffset; + + if ((RelativePositionAxes & Axes.X) == 0) + offset.X = 0; + + if ((RelativePositionAxes & Axes.Y) == 0) + offset.Y = 0; + } + + return applyRelativeAxes(RelativePositionAxes, Position - offset, FillMode.Stretch); + } + } + + private Vector2 size + { + get { return new Vector2(width, height); } + set + { + width = value.X; + height = value.Y; + } + } + + /// + /// Size of this Drawable in the 's coordinate system. + /// May be in absolute or relative units (controlled by ). + /// + public virtual Vector2 Size + { + get { return size; } + + set + { + if (size == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Size)} must be finite, but is {value}."); + + size = value; + + Invalidate(Invalidation.DrawSize); + } + } + + private float width; + private float height; + + /// + /// X component of . + /// + public virtual float Width + { + get { return width; } + set + { + if (width == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Width)} must be finite, but is {value}."); + + width = value; + + Invalidate(Invalidation.DrawSize); + } + } + + /// + /// Y component of . + /// + public virtual float Height + { + get { return height; } + set + { + if (height == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Height)} must be finite, but is {value}."); + + height = value; + + Invalidate(Invalidation.DrawSize); + } + } + + private Axes relativeSizeAxes; + + /// + /// Controls which are relative sizes w.r.t. 's size + /// (from 0 to 1) in the 's coordinate system, rather than absolute sizes. + /// The set in this property are ignored by automatically sizing + /// parents. + /// + /// + /// If an axis becomes relatively sized and its component of was previously 0, + /// then it automatically becomes 1. In all other cases is converted such that + /// remains invariant across changes of this property. + /// + public virtual Axes RelativeSizeAxes + { + get { return relativeSizeAxes; } + set + { + if (value == relativeSizeAxes) + return; + + // In some cases we cannot easily preserve our size, and so we simply invalidate and + // leave correct sizing to the user. + if (fillMode != FillMode.Stretch && (value == Axes.Both || relativeSizeAxes == Axes.Both)) + Invalidate(Invalidation.DrawSize); + else + { + // Convert coordinates from relative to absolute or vice versa + Vector2 conversion = relativeToAbsoluteFactor; + if ((value & Axes.X) > (relativeSizeAxes & Axes.X)) + Width = conversion.X == 0 ? 0 : Width / conversion.X; + else if ((relativeSizeAxes & Axes.X) > (value & Axes.X)) + Width *= conversion.X; + + if ((value & Axes.Y) > (relativeSizeAxes & Axes.Y)) + Height = conversion.Y == 0 ? 0 : Height / conversion.Y; + else if ((relativeSizeAxes & Axes.Y) > (value & Axes.Y)) + Height *= conversion.Y; + + // No invalidation is necessary as DrawSize remains invariant. + } + + relativeSizeAxes = value; + + if ((relativeSizeAxes & Axes.X) > 0 && Width == 0) Width = 1; + if ((relativeSizeAxes & Axes.Y) > 0 && Height == 0) Height = 1; + + OnSizingChanged(); + } + } + + private Cached drawSizeBacking; + + /// + /// Absolute size of this Drawable in the 's coordinate system. + /// + public Vector2 DrawSize => drawSizeBacking.IsValid ? drawSizeBacking : (drawSizeBacking.Value = applyRelativeAxes(RelativeSizeAxes, Size, FillMode)); + + /// + /// X component of . + /// + public float DrawWidth => DrawSize.X; + + /// + /// Y component of . + /// + public float DrawHeight => DrawSize.Y; + + private MarginPadding margin; + + /// + /// Size of an empty region around this Drawable used to manipulate + /// layout. Does not affect or the region of accepted input, + /// but does affect . + /// + public MarginPadding Margin + { + get { return margin; } + set + { + if (margin.Equals(value)) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Margin)} must be finite, but is {value}."); + + margin = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + /// + /// Absolute size of this Drawable's layout rectangle in the 's + /// coordinate system; i.e. with the addition of . + /// + public Vector2 LayoutSize => DrawSize + new Vector2(margin.TotalHorizontal, margin.TotalVertical); + + /// + /// Absolutely sized rectangle for drawing in the 's coordinate system. + /// Based on . + /// + public RectangleF DrawRectangle + { + get + { + Vector2 s = DrawSize; + return new RectangleF(0, 0, s.X, s.Y); + } + } + + /// + /// Absolutely sized rectangle for layout in the 's coordinate system. + /// Based on and . + /// + public RectangleF LayoutRectangle + { + get + { + Vector2 s = LayoutSize; + return new RectangleF(-margin.Left, -margin.Top, s.X, s.Y); + } + } + + /// + /// Helper function for converting potentially relative coordinates in the + /// 's space to absolute coordinates based on which + /// axes are relative. + /// + /// Describes which axes are relative. + /// The coordinates to convert. + /// The to be used. + /// Absolute coordinates in 's space. + private Vector2 applyRelativeAxes(Axes relativeAxes, Vector2 v, FillMode fillMode) + { + if (relativeAxes != Axes.None) + { + Vector2 conversion = relativeToAbsoluteFactor; + + if ((relativeAxes & Axes.X) > 0) + v.X *= conversion.X; + if ((relativeAxes & Axes.Y) > 0) + v.Y *= conversion.Y; + + // FillMode only makes sense if both axes are relatively sized as the general rule + // for n-dimensional aspect preservation is to simply take the minimum or the maximum + // scale among all active axes. For single axes the minimum / maximum is just the + // value itself. + if (relativeAxes == Axes.Both && fillMode != FillMode.Stretch) + { + if (fillMode == FillMode.Fill) + v = new Vector2(Math.Max(v.X, v.Y * fillAspectRatio)); + else if (fillMode == FillMode.Fit) + v = new Vector2(Math.Min(v.X, v.Y * fillAspectRatio)); + v.Y /= fillAspectRatio; + } + } + return v; + } + + /// + /// Conversion factor from relative to absolute coordinates in the 's space. + /// + private Vector2 relativeToAbsoluteFactor => Parent?.RelativeToAbsoluteFactor ?? Vector2.One; + + private Axes bypassAutoSizeAxes; + + /// + /// Controls which are ignored by parent automatic sizing. + /// Most notably, and do not affect + /// automatic sizing to avoid circular size dependencies. + /// + public Axes BypassAutoSizeAxes + { + get { return bypassAutoSizeAxes | relativeSizeAxes | relativePositionAxes; } + + set + { + if (value == bypassAutoSizeAxes) + return; + + bypassAutoSizeAxes = value; + Parent?.InvalidateFromChild(Invalidation.RequiredParentSizeToFit); + } + } + + /// + /// Computes the bounding box of this drawable in its parent's space. + /// + public virtual RectangleF BoundingBox => ToParentSpace(LayoutRectangle).AABBFloat; + + /// + /// Called whenever the of this drawable is changed, or when the are changed if this drawable is a . + /// + protected virtual void OnSizingChanged() { } + + #endregion + + #region Scale / Shear / Rotation + + private Vector2 scale = Vector2.One; + + /// + /// Base relative scaling factor around . + /// + public Vector2 Scale + { + get { return scale; } + + set + { + if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) + value.X = Precision.FLOAT_EPSILON; + if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) + value.Y = Precision.FLOAT_EPSILON; + + if (scale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Scale)} must be finite, but is {value}."); + + scale = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + private float fillAspectRatio = 1; + + /// + /// The desired ratio of width to height when under the effect of a non-stretching + /// and being . + /// + public float FillAspectRatio + { + get { return fillAspectRatio; } + + set + { + if (fillAspectRatio == value) return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be finite, but is {value}."); + if (value == 0) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be non-zero."); + + fillAspectRatio = value; + + if (fillMode != FillMode.Stretch && RelativeSizeAxes == Axes.Both) + Invalidate(Invalidation.DrawSize); + } + } + + private FillMode fillMode; + + /// + /// Controls the behavior of when it is set to . + /// Otherwise, this member has no effect. By default, stretching is used, which simply scales + /// this drawable's according to 's + /// disregarding this drawable's . Other values of preserve . + /// + public FillMode FillMode + { + get { return fillMode; } + + set + { + if (fillMode == value) return; + fillMode = value; + + Invalidate(Invalidation.DrawSize); + } + } + + /// + /// Relative scaling factor around . + /// + protected virtual Vector2 DrawScale => Scale; + + private Vector2 shear = Vector2.Zero; + + /// + /// Relative shearing factor. The X dimension is relative w.r.t. + /// and the Y dimension relative w.r.t. . + /// + public Vector2 Shear + { + get { return shear; } + + set + { + if (shear == value) return; + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Shear)} must be finite, but is {value}."); + + shear = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + private float rotation; + + /// + /// Rotation in degrees around . + /// + public float Rotation + { + get { return rotation; } + + set + { + if (value == rotation) return; + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Rotation)} must be finite, but is {value}."); + + rotation = value; + + Invalidate(Invalidation.MiscGeometry); + } + } + + #endregion + + #region Origin / Anchor + + private Anchor origin = Anchor.TopLeft; + + /// + /// The origin of the local coordinate system of this Drawable. + /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y) + /// or a fixed absolute position via . + /// + public virtual Anchor Origin + { + get { return origin; } + + set + { + if (origin == value) return; + + if (value == 0) + throw new ArgumentException("Cannot set origin to 0.", nameof(value)); + + origin = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + + private Vector2 customOrigin; + + /// + /// The origin of the local coordinate system of this Drawable + /// in relative coordinates expressed in the coordinate system with origin at the + /// top left corner of the (not ). + /// + public Vector2 RelativeOriginPosition + { + get + { + if (Origin == Anchor.Custom) + throw new InvalidOperationException(@"Can not obtain relative origin position for custom origins."); + + Vector2 result = Vector2.Zero; + if ((origin & Anchor.x1) > 0) + result.X = 0.5f; + else if ((origin & Anchor.x2) > 0) + result.X = 1; + + if ((origin & Anchor.y1) > 0) + result.Y = 0.5f; + else if ((origin & Anchor.y2) > 0) + result.Y = 1; + + return result; + } + } + + /// + /// The origin of the local coordinate system of this Drawable + /// in absolute coordinates expressed in the coordinate system with origin at the + /// top left corner of the (not ). + /// + public virtual Vector2 OriginPosition + { + get + { + Vector2 result; + if (Origin == Anchor.Custom) + result = customOrigin; + else if (Origin == Anchor.TopLeft) + result = Vector2.Zero; + else + result = computeAnchorPosition(LayoutSize, Origin); + + return result - new Vector2(margin.Left, margin.Top); + } + + set + { + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(OriginPosition)} must be finite, but is {value}."); + + customOrigin = value; + Origin = Anchor.Custom; + } + } + + + private Anchor anchor = Anchor.TopLeft; + + /// + /// Specifies where is attached to the + /// in the coordinate system with origin at the top left corner of the + /// 's . + /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y) + /// or a fixed absolute position via . + /// + public Anchor Anchor + { + get { return anchor; } + + set + { + if (anchor == value) return; + + if (value == 0) + throw new ArgumentException("Cannot set anchor to 0.", nameof(value)); + + anchor = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + + private Vector2 customRelativeAnchorPosition; + + /// + /// Specifies in relative coordinates where is attached + /// to the in the coordinate system with origin at the top + /// left corner of the 's , and + /// a value of referring to the bottom right corner of + /// the 's . + /// + public Vector2 RelativeAnchorPosition + { + get + { + if (Anchor == Anchor.Custom) + return customRelativeAnchorPosition; + + Vector2 result = Vector2.Zero; + if ((anchor & Anchor.x1) > 0) + result.X = 0.5f; + else if ((anchor & Anchor.x2) > 0) + result.X = 1; + + if ((anchor & Anchor.y1) > 0) + result.Y = 0.5f; + else if ((anchor & Anchor.y2) > 0) + result.Y = 1; + + return result; + } + + set + { + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeAnchorPosition)} must be finite, but is {value}."); + + customRelativeAnchorPosition = value; + Anchor = Anchor.Custom; + } + } + + /// + /// Specifies in absolute coordinates where is attached + /// to the in the coordinate system with origin at the top + /// left corner of the 's . + /// + public Vector2 AnchorPosition => RelativeAnchorPosition * Parent?.ChildSize ?? Vector2.Zero; + + /// + /// Helper function to compute an absolute position given an absolute size and + /// a relative . + /// + /// Absolute size + /// Relative + /// Absolute position + private static Vector2 computeAnchorPosition(Vector2 size, Anchor anchor) + { + Vector2 result = Vector2.Zero; + + if ((anchor & Anchor.x1) > 0) + result.X = size.X / 2f; + else if ((anchor & Anchor.x2) > 0) + result.X = size.X; + + if ((anchor & Anchor.y1) > 0) + result.Y = size.Y / 2f; + else if ((anchor & Anchor.y2) > 0) + result.Y = size.Y; + + return result; + } + + #endregion + + #region Colour / Alpha / Blending + + private ColourInfo colour = Color4.White; + + /// + /// Colour of this in sRGB space. Can contain individual colours for all four + /// corners of this , which are then interpolated, but can also be assigned + /// just a single colour. Implicit casts from and from exist. + /// + public ColourInfo Colour + { + get { return colour; } + + set + { + if (colour.Equals(value)) return; + + colour = value; + + Invalidate(Invalidation.Colour); + } + } + + private float alpha = 1.0f; + + /// + /// Multiplicative alpha factor applied on top of and its existing + /// alpha channel(s). + /// + public float Alpha + { + get { return alpha; } + + set + { + if (alpha == value) return; + + Invalidate(Invalidation.Colour); + + alpha = value; + } + } + + private const float visibility_cutoff = 0.0001f; + + /// + /// Determines whether this Drawable is present based on its value. + /// Can be forced always on with . + /// + public virtual bool IsPresent => AlwaysPresent || Alpha > visibility_cutoff && Math.Abs(Scale.X) > Precision.FLOAT_EPSILON && Math.Abs(Scale.Y) > Precision.FLOAT_EPSILON; + + private bool alwaysPresent; + + /// + /// If true, forces to always be true. In other words, + /// this drawable is always considered for layout, input, and drawing, regardless + /// of alpha value. + /// + public bool AlwaysPresent + { + get { return alwaysPresent; } + + set + { + if (alwaysPresent == value) return; + + Invalidate(Invalidation.Colour); + + alwaysPresent = value; + } + } + + private BlendingParameters blending; + + /// + /// Determines how this Drawable is blended with other already drawn Drawables. + /// Inherits the 's by default. + /// + public BlendingParameters Blending + { + get { return blending; } + + set + { + if (blending.Equals(value)) + return; + + blending = value; + Invalidate(Invalidation.Colour); + } + } + #endregion + + #region Timekeeping + + private IFrameBasedClock customClock; + private IFrameBasedClock clock; + + /// + /// The clock of this drawable. Used for keeping track of time across + /// frames. By default is inherited from . + /// If set, then the provided value is used as a custom clock and the + /// 's clock is ignored. + /// + public override IFrameBasedClock Clock + { + get { return clock; } + set + { + customClock = value; + UpdateClock(customClock); + } + } + + /// + /// Updates the clock to be used. Has no effect if this drawable + /// uses a custom clock. + /// + /// The new clock to be used. + internal virtual void UpdateClock(IFrameBasedClock clock) + { + this.clock = customClock ?? clock; + scheduler?.UpdateClock(this.clock); + } + + /// + /// Whether should be automatically invoked on this 's + /// in . This should only be set to false in scenarios where the clock is updated elsewhere. + /// + public bool ProcessCustomClock = true; + + /// + /// The time at which this drawable becomes valid (and is considered for drawing). + /// + public virtual double LifetimeStart { get; set; } = double.MinValue; + + /// + /// The time at which this drawable is no longer valid (and is considered for disposal). + /// + public virtual double LifetimeEnd { get; set; } = double.MaxValue; + + /// + /// Whether this drawable should currently be alive. + /// This is queried by the framework to decide the state of this drawable for the next frame. + /// + protected internal virtual bool ShouldBeAlive + { + get + { + if (LifetimeStart == double.MinValue && LifetimeEnd == double.MaxValue) + return true; + + return Time.Current >= LifetimeStart && Time.Current < LifetimeEnd; + } + } + + /// + /// Whether to remove the drawable from its parent's children when it's not alive. + /// + public virtual bool RemoveWhenNotAlive => Parent == null || Time.Current > LifetimeStart; + + #endregion + + #region Parenting (scene graph operations, including ProxyDrawable) + + /// + /// Retrieve the first parent in the tree which derives from . + /// As this is performing an upward tree traversal, avoid calling every frame. + /// + /// The first parent . + protected InputManager GetContainingInputManager() + { + Drawable search = Parent; + while (search != null) + { + var test = search as InputManager; + if (test != null) return test; + + search = search.Parent; + } + return null; + } + + private CompositeDrawable parent; + + /// + /// The parent of this drawable in the scene graph. + /// + public CompositeDrawable Parent + { + get { return parent; } + internal set + { + if (isDisposed) + throw new ObjectDisposedException(ToString(), "Disposed Drawables may never get a parent and return to the scene graph."); + + if (value == null) + ChildID = 0; + + if (parent == value) return; + + if (value != null && parent != null) + throw new InvalidOperationException("May not add a drawable to multiple containers."); + + parent = value; + Invalidate(InvalidationFromParentSize | Invalidation.Colour); + + if (parent != null) + { + //we should already have a clock at this point (from our LoadRequested invocation) + //this just ensures we have the most recent parent clock. + //we may want to consider enforcing that parent.Clock == clock here. + UpdateClock(parent.Clock); + } + } + } + + /// + /// Refers to the original if this drawable was created via + /// . Otherwise refers to this. + /// + internal virtual Drawable Original => this; + + /// + /// True iff has been called before. + /// + internal bool HasProxy => proxy != null; + + private ProxyDrawable proxy; + + /// + /// Creates a proxy drawable which can be inserted elsewhere in the scene graph. + /// Will cause the original instance to not render itself. + /// Creating multiple proxies is not supported and will result in an + /// . + /// + public ProxyDrawable CreateProxy() + { + if (proxy != null) + throw new InvalidOperationException("Multiple proxies are not supported."); + return proxy = new ProxyDrawable(this); + } + + #endregion + + #region Caching & invalidation (for things too expensive to compute every frame) + + /// + /// Was this Drawable masked away completely during the last frame? + /// This is measured conservatively, i.e. it is only true when the Drawable was + /// actually masked away, but it may be false, even if the Drawable was masked away. + /// + internal bool IsMaskedAway { get; private set; } + + private Cached screenSpaceDrawQuadBacking; + + protected virtual Quad ComputeScreenSpaceDrawQuad() => ToScreenSpace(DrawRectangle); + + /// + /// The screen-space quad this drawable occupies. + /// + public virtual Quad ScreenSpaceDrawQuad => screenSpaceDrawQuadBacking.IsValid ? screenSpaceDrawQuadBacking : (screenSpaceDrawQuadBacking.Value = ComputeScreenSpaceDrawQuad()); + + private Cached drawInfoBacking; + + private DrawInfo computeDrawInfo() + { + DrawInfo di = Parent?.DrawInfo ?? new DrawInfo(null); + + Vector2 pos = DrawPosition + AnchorPosition; + Vector2 drawScale = DrawScale; + BlendingParameters localBlending = Blending; + + if (Parent != null) + { + pos += Parent.ChildOffset; + + if (localBlending.Mode == BlendingMode.Inherit) + localBlending.Mode = Parent.Blending.Mode; + + if (localBlending.RGBEquation == BlendingEquation.Inherit) + localBlending.RGBEquation = Parent.Blending.RGBEquation; + + if (localBlending.AlphaEquation == BlendingEquation.Inherit) + localBlending.AlphaEquation = Parent.Blending.AlphaEquation; + } + + di.ApplyTransform(pos, drawScale, Rotation, Shear, OriginPosition); + di.Blending = new BlendingInfo(localBlending); + + ColourInfo drawInfoColour = alpha != 1 ? colour.MultiplyAlpha(alpha) : colour; + + // No need for a Parent null check here, because null parents always have + // a single colour (white). + if (di.Colour.HasSingleColour) + di.Colour.ApplyChild(drawInfoColour); + else + { + Debug.Assert(Parent != null, + $"The {nameof(di)} of null parents should always have the single colour white, and therefore this branch should never be hit."); + + // Cannot use ToParentSpace here, because ToParentSpace depends on DrawInfo to be completed + // ReSharper disable once PossibleNullReferenceException + Quad interp = Quad.FromRectangle(DrawRectangle) * (di.Matrix * Parent.DrawInfo.MatrixInverse); + Vector2 parentSize = Parent.DrawSize; + + interp.TopLeft = Vector2.Divide(interp.TopLeft, parentSize); + interp.TopRight = Vector2.Divide(interp.TopRight, parentSize); + interp.BottomLeft = Vector2.Divide(interp.BottomLeft, parentSize); + interp.BottomRight = Vector2.Divide(interp.BottomRight, parentSize); + + di.Colour.ApplyChild(drawInfoColour, interp); + } + + return di; + } + + /// + /// Contains a linear transformation, colour information, and blending information + /// of this drawable. + /// + public virtual DrawInfo DrawInfo => drawInfoBacking.IsValid ? drawInfoBacking : (drawInfoBacking.Value = computeDrawInfo()); + + + private Cached requiredParentSizeToFitBacking; + + private Vector2 computeRequiredParentSizeToFit() + { + // Auxilary variables required for the computation + Vector2 ap = AnchorPosition; + Vector2 rap = RelativeAnchorPosition; + + Vector2 ratio1 = new Vector2( + rap.X <= 0 ? 0 : 1 / rap.X, + rap.Y <= 0 ? 0 : 1 / rap.Y); + + Vector2 ratio2 = new Vector2( + rap.X >= 1 ? 0 : 1 / (1 - rap.X), + rap.Y >= 1 ? 0 : 1 / (1 - rap.Y)); + + RectangleF bbox = BoundingBox; + + // Compute the required size of the parent such that we fit in snugly when positioned + // at our relative anchor in the parent. + Vector2 topLeftOffset = ap - bbox.TopLeft; + Vector2 topLeftSize1 = topLeftOffset * ratio1; + Vector2 topLeftSize2 = -topLeftOffset * ratio2; + + Vector2 bottomRightOffset = ap - bbox.BottomRight; + Vector2 bottomRightSize1 = bottomRightOffset * ratio1; + Vector2 bottomRightSize2 = -bottomRightOffset * ratio2; + + // Expand bounds according to clipped offset + return Vector2.ComponentMax( + Vector2.ComponentMax(topLeftSize1, topLeftSize2), + Vector2.ComponentMax(bottomRightSize1, bottomRightSize2)); + } + + /// + /// Returns the size of the smallest axis aligned box in parent space which + /// encompasses this drawable while preserving this drawable's + /// . + /// If a component of is smaller than zero + /// or larger than one, then it is impossible to preserve + /// while fitting into the parent, and thus returns + /// zero in that dimension; i.e. we no longer fit into the parent. + /// This behavior is prominent with non-centre and non-custom values. + /// + internal Vector2 RequiredParentSizeToFit => requiredParentSizeToFitBacking.IsValid ? requiredParentSizeToFitBacking : (requiredParentSizeToFitBacking.Value = computeRequiredParentSizeToFit()); + + + private static readonly AtomicCounter invalidation_counter = new AtomicCounter(); + + // Make sure we start out with a value of 1 such that ApplyDrawNode is always called at least once + private long invalidationID = invalidation_counter.Increment(); + + /// + /// Invalidates draw matrix and autosize caches. + /// + /// This does not ensure that the parent containers have been updated before us, thus operations involving + /// parent states (e.g. ) should not be executed in an overriden implementation. + /// + /// + /// If the invalidate was actually necessary. + public virtual bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if (invalidation == Invalidation.None || !IsLoaded) + return false; + + if (shallPropagate && Parent != null && source != Parent) + Parent.InvalidateFromChild(invalidation); + + bool alreadyInvalidated = true; + + // Either ScreenSize OR ScreenPosition OR Colour + if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Colour)) > 0) + { + if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) + alreadyInvalidated &= !requiredParentSizeToFitBacking.Invalidate(); + + alreadyInvalidated &= !screenSpaceDrawQuadBacking.Invalidate(); + alreadyInvalidated &= !drawInfoBacking.Invalidate(); + alreadyInvalidated &= !drawSizeBacking.Invalidate(); + } + + if (!alreadyInvalidated || (invalidation & Invalidation.DrawNode) > 0) + invalidationID = invalidation_counter.Increment(); + + OnInvalidate?.Invoke(this); + + return !alreadyInvalidated; + } + + public Invalidation InvalidationFromParentSize + { + get + { + Invalidation result = Invalidation.DrawInfo; + if (RelativeSizeAxes != Axes.None) + result |= Invalidation.DrawSize; + if (RelativePositionAxes != Axes.None) + result |= Invalidation.MiscGeometry; + return result; + } + } + + #endregion + + #region DrawNode + + private readonly DrawNode[] drawNodes = new DrawNode[3]; + + /// + /// Generates the DrawNode for ourselves. + /// + /// A complete and updated DrawNode, or null if the DrawNode would be invisible. + internal virtual DrawNode GenerateDrawNodeSubtree(int treeIndex) + { + DrawNode node = drawNodes[treeIndex]; + if (node == null) + { + drawNodes[treeIndex] = node = CreateDrawNode(); + FrameStatistics.Increment(StatisticsCounterType.DrawNodeCtor); + } + + if (invalidationID != node.InvalidationID) + { + ApplyDrawNode(node); + FrameStatistics.Increment(StatisticsCounterType.DrawNodeAppl); + } + + return node; + } + + /// + /// Fills a given draw node with all information required to draw this drawable. + /// + /// The node to fill with information. + protected virtual void ApplyDrawNode(DrawNode node) + { + node.DrawInfo = DrawInfo; + node.InvalidationID = invalidationID; + } + + /// + /// Creates a draw node capable of containing all information required to draw this drawable. + /// + /// The created draw node. + protected virtual DrawNode CreateDrawNode() => new DrawNode(); + + #endregion + + #region DrawInfo-based coordinate system conversions + + /// + /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space. + /// + /// A vector in local coordinates. + /// The drawable in which space we want to transform the vector to. + /// The vector in other's coordinates. + public Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other) + { + if (other == this) + return input; + + return Vector2Extensions.Transform(Vector2Extensions.Transform(input, DrawInfo.Matrix), other.DrawInfo.MatrixInverse); + } + + /// + /// Accepts a rectangle in local coordinates and converts it to coordinates in another Drawable's space. + /// + /// A rectangle in local coordinates. + /// The drawable in which space we want to transform the rectangle to. + /// The rectangle in other's coordinates. + public Quad ToSpaceOfOtherDrawable(RectangleF input, IDrawable other) + { + if (other == this) + return input; + + return Quad.FromRectangle(input) * (DrawInfo.Matrix * other.DrawInfo.MatrixInverse); + } + + /// + /// Accepts a vector in local coordinates and converts it to coordinates in Parent's space. + /// + /// A vector in local coordinates. + /// The vector in Parent's coordinates. + public Vector2 ToParentSpace(Vector2 input) => ToSpaceOfOtherDrawable(input, Parent); + + /// + /// Accepts a rectangle in local coordinates and converts it to a quad in Parent's space. + /// + /// A rectangle in local coordinates. + /// The quad in Parent's coordinates. + public Quad ToParentSpace(RectangleF input) => ToSpaceOfOtherDrawable(input, Parent); + + /// + /// Accepts a vector in local coordinates and converts it to coordinates in screen space. + /// + /// A vector in local coordinates. + /// The vector in screen coordinates. + public Vector2 ToScreenSpace(Vector2 input) + { + return Vector2Extensions.Transform(input, DrawInfo.Matrix); + } + + /// + /// Accepts a rectangle in local coordinates and converts it to a quad in screen space. + /// + /// A rectangle in local coordinates. + /// The quad in screen coordinates. + public Quad ToScreenSpace(RectangleF input) + { + return Quad.FromRectangle(input) * DrawInfo.Matrix; + } + + /// + /// Accepts a vector in screen coordinates and converts it to coordinates in local space. + /// + /// A vector in screen coordinates. + /// The vector in local coordinates. + public Vector2 ToLocalSpace(Vector2 screenSpacePos) + { + return Vector2Extensions.Transform(screenSpacePos, DrawInfo.MatrixInverse); + } + + /// + /// Accepts a quad in screen coordinates and converts it to coordinates in local space. + /// + /// A quad in screen coordinates. + /// The quad in local coordinates. + public Quad ToLocalSpace(Quad screenSpaceQuad) + { + return screenSpaceQuad * DrawInfo.MatrixInverse; + } + + #endregion + + #region Interaction / Input + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnHover(InputState screenSpaceState) => OnHover(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered once when this Drawable becomes hovered. + /// + /// The state at which the Drawable becomes hovered. + /// True if this Drawable would like to handle the hover. If so, then + /// no further Drawables up the scene graph will receive hovering events. If + /// false, however, then will still be + /// received once hover is lost. + protected virtual bool OnHover(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public void TriggerOnHoverLost(InputState screenSpaceState) => OnHoverLost(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever this drawable is no longer hovered. + /// + /// The state at which hover is lost. + protected virtual void OnHoverLost(InputState state) + { + } + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnMouseDown(InputState screenSpaceState = null, MouseDownEventArgs args = null) => OnMouseDown(createCloneInParentSpace(screenSpaceState), args); + + /// + /// Triggered whenever a mouse button is pressed on top of this Drawable. + /// + /// The state after the press. + /// Specific arguments for mouse down event. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnMouseDown(InputState state, MouseDownEventArgs args) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnMouseUp(InputState screenSpaceState = null, MouseUpEventArgs args = null) => OnMouseUp(createCloneInParentSpace(screenSpaceState), args); + + /// + /// Triggered whenever a mouse button is released on top of this Drawable. + /// + /// The state after the release. + /// Specific arguments for mouse up event. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnMouseUp(InputState state, MouseUpEventArgs args) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnClick(InputState screenSpaceState = null) => OnClick(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever a mouse click occurs on top of this Drawable. + /// + /// The state after the click. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnClick(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnDoubleClick(InputState screenSpaceState) => OnDoubleClick(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever a mouse double click occurs on top of this Drawable. + /// + /// The state after the double click. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnDoubleClick(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnDragStart(InputState screenSpaceState) => OnDragStart(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever this Drawable is initially dragged by a held mouse click + /// and subsequent movement. + /// + /// The state after the mouse was moved. + /// True if this Drawable accepts being dragged. If so, then future + /// and + /// events will be received. Otherwise, the event is propagated up the scene + /// graph to the next eligible Drawable. + protected virtual bool OnDragStart(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnDrag(InputState screenSpaceState) => OnDrag(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever the mouse is moved while dragging. + /// Only is received if a drag was previously initiated by returning true + /// from . + /// + /// The state after the mouse was moved. + /// Currently unused. + protected virtual bool OnDrag(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnDragEnd(InputState screenSpaceState) => OnDragEnd(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever a drag ended. Only is received if a drag was previously + /// initiated by returning true from . + /// + /// The state after the drag ended. + /// Currently unused. + protected virtual bool OnDragEnd(InputState state) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnWheel(InputState screenSpaceState) => OnWheel(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever the mouse wheel was turned over this Drawable. + /// + /// The state after the wheel was turned. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnWheel(InputState state) => false; + + /// + /// Triggers with a local version of the given + /// + /// The input state. + public void TriggerOnFocus(InputState screenSpaceState = null) => OnFocus(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever this Drawable gains focus. + /// Focused Drawables receive keyboard input before all other Drawables, + /// and thus handle it first. + /// + /// The state after focus when focus can be gained. + protected virtual void OnFocus(InputState state) + { + } + + /// + /// Triggers with a local version of the given + /// + /// The input state. + public void TriggerOnFocusLost(InputState screenSpaceState = null) => OnFocusLost(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever this Drawable lost focus. + /// + /// The state after focus was lost. + protected virtual void OnFocusLost(InputState state) + { + } + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnKeyDown(InputState screenSpaceState, KeyDownEventArgs args) => OnKeyDown(createCloneInParentSpace(screenSpaceState), args); + + /// + /// Triggered whenever a key was pressed. + /// + /// The state after the key was pressed. + /// Specific arguments for key down event. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnKeyDown(InputState state, KeyDownEventArgs args) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnKeyUp(InputState screenSpaceState, KeyUpEventArgs args) => OnKeyUp(createCloneInParentSpace(screenSpaceState), args); + + /// + /// Triggered whenever a key was released. + /// + /// The state after the key was released. + /// Specific arguments for key up event. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnKeyUp(InputState state, KeyUpEventArgs args) => false; + + /// + /// Triggers with a local version of the given . + /// + public bool TriggerOnMouseMove(InputState screenSpaceState) => OnMouseMove(createCloneInParentSpace(screenSpaceState)); + + /// + /// Triggered whenever the mouse moved over this Drawable. + /// + /// The state after the mouse moved. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + protected virtual bool OnMouseMove(InputState state) => false; + + private readonly bool handleKeyboardInput, handleMouseInput; + + /// + /// Whether this handles keyboard input. + /// This value is true by default if any keyboard related "On-" input methods are overridden. + /// + public virtual bool HandleKeyboardInput => handleKeyboardInput; + + /// + /// Whether this handles mouse input. + /// This value is true by default if any mouse related "On-" input methods are overridden. + /// + public virtual bool HandleMouseInput => handleMouseInput; + + /// + /// Nested class which is used for caching , values obtained via reflection. + /// + private static class HandleInputCache + { + private static readonly ConcurrentDictionary mouse_cached_values = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary keyboard_cached_values = new ConcurrentDictionary(); + + private static readonly string[] mouse_input_methods = { + nameof(OnHover), + nameof(OnHoverLost), + nameof(OnMouseDown), + nameof(OnMouseUp), + nameof(OnClick), + nameof(OnDoubleClick), + nameof(OnDragStart), + nameof(OnDrag), + nameof(OnDragEnd), + nameof(OnWheel), + nameof(OnFocus), + nameof(OnFocusLost), + nameof(OnMouseMove) + }; + + private static readonly string[] keyboard_input_methods = { + nameof(OnFocus), + nameof(OnFocusLost), + nameof(OnKeyDown), + nameof(OnKeyUp) + }; + + public static bool HandleKeyboardInput(Drawable drawable) => get(drawable, keyboard_cached_values, keyboard_input_methods); + + public static bool HandleMouseInput(Drawable drawable) => get(drawable, mouse_cached_values, mouse_input_methods); + + private static bool get(Drawable drawable, ConcurrentDictionary cache, string[] inputMethods) + { + var type = drawable.GetType(); + if (cache.TryGetValue(type, out var value)) + return value; + + foreach (var inputMethod in inputMethods) + { + // check for any input method overrides which are at a higher level than drawable. + var method = type.GetMethod(inputMethod, BindingFlags.Instance | BindingFlags.NonPublic); + + Debug.Assert(method != null); + + // ReSharper disable once PossibleNullReferenceException + if (method.DeclaringType != typeof(Drawable)) + { + cache.TryAdd(type, true); + return true; + } + } + + cache.TryAdd(type, false); + return false; + } + } + + /// + /// Check whether we have active focus. + /// + public bool HasFocus { get; internal set; } + + /// + /// If true, we are eagerly requesting focus. If nothing else above us has (or is requesting focus) we will get it. + /// + public virtual bool RequestsFocus => false; + + /// + /// If true, we will gain focus (receiving priority on keybaord input) (and receive an event) on returning true in . + /// + public virtual bool AcceptsFocus => false; + + /// + /// Whether this Drawable is currently hovered over. + /// + public bool IsHovered { get; internal set; } + + /// + /// Whether this Drawable is currently being dragged. + /// + public bool IsDragged { get; internal set; } + + /// + /// Determines whether this drawable receives mouse input when the mouse is at the + /// given screen-space position. + /// + /// The screen-space position where input could be received. + /// True iff input is received at the given screen-space position. + public virtual bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Contains(screenSpacePos); + + /// + /// Computes whether a given screen-space position is contained within this drawable. + /// Mouse input events are only received when this function is true, or when the drawable + /// is in focus. + /// + /// The screen space position to be checked against this drawable. + public virtual bool Contains(Vector2 screenSpacePos) => DrawRectangle.Contains(ToLocalSpace(screenSpacePos)); + + /// + /// Whether this Drawable can keyboard receive input, taking into account all optimizations and masking. + /// + public bool CanReceiveKeyboardInput => HandleKeyboardInput && IsPresent && !IsMaskedAway; + + /// + /// Whether this Drawable can mouse receive input, taking into account all optimizations and masking. + /// + public bool CanReceiveMouseInput => HandleMouseInput && IsPresent && !IsMaskedAway; + + /// + /// Creates a new InputState with mouse coodinates converted to the coordinate space of our parent. + /// + /// The screen-space input state to be cloned and transformed. + /// The cloned and transformed state. + private InputState createCloneInParentSpace(InputState screenSpaceState) + { + if (screenSpaceState == null) return null; + + var clone = screenSpaceState.Clone(); + clone.Mouse = new LocalMouseState(screenSpaceState.Mouse.NativeState, this); + return clone; + } + + /// + /// This method is responsible for building a queue of Drawables to receive keyboard input + /// in reverse order. This method is overridden by to be called on all + /// children such that the entire scene graph is covered. + /// + /// The input queue to be built. + /// Whether we have added ourself to the queue. + internal virtual bool BuildKeyboardInputQueue(List queue) + { + if (!CanReceiveKeyboardInput) + return false; + + queue.Add(this); + return true; + } + + /// + /// This method is responsible for building a queue of Drawables to receive mouse input + /// in reverse order. This method is overridden by to be called on all + /// children such that the entire scene graph is covered. + /// + /// The current position of the mouse cursor in screen space. + /// The input queue to be built. + /// Whether we have added ourself to the queue. + internal virtual bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) + { + if (!CanReceiveMouseInput || !ReceiveMouseInputAt(screenSpaceMousePos)) + return false; + + queue.Add(this); + return true; + } + + private struct LocalMouseState : IMouseState + { + public IMouseState NativeState { get; } + + public IMouseState LastState { get; set; } + + private readonly Drawable us; + + public LocalMouseState(IMouseState state, Drawable us) + { + NativeState = state; + LastState = null; + this.us = us; + } + + public IReadOnlyList Buttons => NativeState.Buttons; + + public Vector2 Delta => Position - LastPosition; + + public Vector2 Position => us.Parent?.ToLocalSpace(NativeState.Position) ?? NativeState.Position; + + public Vector2 LastPosition => us.Parent?.ToLocalSpace(NativeState.LastPosition) ?? NativeState.LastPosition; + + public Vector2? PositionMouseDown + { + get { return NativeState.PositionMouseDown == null ? null : us.Parent?.ToLocalSpace(NativeState.PositionMouseDown.Value) ?? NativeState.PositionMouseDown; } + set { throw new NotImplementedException(); } + } + + public bool HasMainButtonPressed => NativeState.HasMainButtonPressed; + + public bool HasAnyButtonPressed => NativeState.HasAnyButtonPressed; + + public int Wheel => NativeState.Wheel; + public int WheelDelta => NativeState.WheelDelta; + + public bool IsPressed(MouseButton button) => NativeState.IsPressed(button); + + public void SetPressed(MouseButton button, bool pressed) => NativeState.SetPressed(button, pressed); + + public IMouseState Clone() + { + return (LocalMouseState)MemberwiseClone(); + } + } + + #endregion + + #region Transforms + + protected internal ScheduledDelegate Schedule(Action action) => Scheduler.AddDelayed(action, TransformDelay); + + /// + /// Make this drawable automatically clean itself up after all transforms have finished playing. + /// Can be delayed using Delay(). + /// + public void Expire(bool calculateLifetimeStart = false) + { + if (clock == null) + { + LifetimeEnd = double.MinValue; + return; + } + + LifetimeEnd = LatestTransformEndTime; + + if (calculateLifetimeStart) + { + double min = double.MaxValue; + foreach (Transform t in Transforms) + if (t.StartTime < min) min = t.StartTime; + LifetimeStart = min < int.MaxValue ? min : int.MinValue; + } + } + + /// + /// Hide sprite instantly. + /// + public virtual void Hide() => this.FadeOut(); + + /// + /// Show sprite instantly. + /// + public virtual void Show() => this.FadeIn(); + + #endregion + + #region Effects + + /// + /// Returns the drawable created by applying the given effect to this drawable. This method may add this drawable to a container. + /// If this drawable should be the child of another container, make sure to add the created drawable to the container instead of this drawable. + /// + /// The type of the drawable that results from applying the given effect. + /// The effect to apply to this drawable. + /// The action that should get called to initialize the created drawable before it is returned. + /// The drawable created by applying the given effect to this drawable. + public T WithEffect(IEffect effect, Action initializationAction = null) where T : Drawable + { + var result = effect.ApplyTo(this); + initializationAction?.Invoke(result); + return result; + } + + #endregion + + /// + /// A name used to identify this Drawable internally. + /// + public string Name = string.Empty; + + public override string ToString() + { + string shortClass = GetType().ReadableName(); + + if (!string.IsNullOrEmpty(Name)) + shortClass = $@"{Name} ({shortClass})"; + + return $@"{shortClass} ({DrawPosition.X:#,0},{DrawPosition.Y:#,0}) {DrawSize.X:#,0}x{DrawSize.Y:#,0}"; + } + } + + /// + /// Specifies which type of properties are being invalidated. + /// + [Flags] + public enum Invalidation + { + /// + /// has changed. No change to or + /// is assumed unless indicated by additional flags. + /// + DrawInfo = 1 << 0, + /// + /// has changed. + /// + DrawSize = 1 << 1, + /// + /// Captures all other geometry changes than , such as + /// , , and . + /// + MiscGeometry = 1 << 2, + /// + /// Our colour changed. + /// + Colour = 1 << 3, + /// + /// has to be invoked on all old draw nodes. + /// + DrawNode = 1 << 4, + + /// + /// No invalidation. + /// + None = 0, + /// + /// has to be recomputed. + /// + RequiredParentSizeToFit = MiscGeometry | DrawSize, + /// + /// All possible things are affected. + /// + All = DrawNode | RequiredParentSizeToFit | Colour | DrawInfo, + } + + /// + /// General enum to specify an "anchor" or "origin" point from the standard 9 points on a rectangle. + /// x and y counterparts can be accessed using bitwise flags. + /// + [Flags] + public enum Anchor + { + TopLeft = y0 | x0, + TopCentre = y0 | x1, + TopRight = y0 | x2, + + CentreLeft = y1 | x0, + Centre = y1 | x1, + CentreRight = y1 | x2, + + BottomLeft = y2 | x0, + BottomCentre = y2 | x1, + BottomRight = y2 | x2, + + /// + /// The vertical counterpart is at "Top" position. + /// + y0 = 1 << 0, + + /// + /// The vertical counterpart is at "Centre" position. + /// + y1 = 1 << 1, + + /// + /// The vertical counterpart is at "Bottom" position. + /// + y2 = 1 << 2, + + /// + /// The horizontal counterpart is at "Left" position. + /// + x0 = 1 << 3, + + /// + /// The horizontal counterpart is at "Centre" position. + /// + x1 = 1 << 4, + + /// + /// The horizontal counterpart is at "Right" position. + /// + x2 = 1 << 5, + + /// + /// The user is manually updating the outcome, so we shouldn't. + /// + Custom = 1 << 6, + } + + [Flags] + public enum Axes + { + None = 0, + + X = 1 << 0, + Y = 1 << 1, + + Both = X | Y, + } + + public enum Direction + { + Horizontal, + Vertical, + } + + public enum RotationDirection + { + Clockwise, + CounterClockwise, + } + + /// + /// Possible states of a within the loading pipeline. + /// + public enum LoadState + { + /// + /// Not loaded, and no load has been initiated yet. + /// + NotLoaded, + /// + /// Currently loading (possibly and usually on a background + /// thread via ). + /// + Loading, + /// + /// Loading is complete, but has not yet been finalized on the update thread + /// ( has not been called yet, which + /// always runs on the update thread and requires ). + /// + Ready, + /// + /// Loading is fully completed and the Drawable is now part of the scene graph. + /// + Loaded + } + + /// + /// Controls the behavior of when it is set to . + /// + public enum FillMode + { + /// + /// Completely fill the parent with a relative size of 1 at the cost of stretching the aspect ratio (default). + /// + Stretch, + /// + /// Always maintains aspect ratio while filling the portion of the parent's size denoted by the relative size. + /// A relative size of 1 results in completely filling the parent by scaling the smaller axis of the drawable to fill the parent. + /// + Fill, + /// + /// Always maintains aspect ratio while fitting into the portion of the parent's size denoted by the relative size. + /// A relative size of 1 results in fitting exactly into the parent by scaling the larger axis of the drawable to fit into the parent. + /// + Fit, + } +} diff --git a/osu.Framework/Graphics/Easing.cs b/osu.Framework/Graphics/Easing.cs index 4f75364cc..3c1a6fba3 100644 --- a/osu.Framework/Graphics/Easing.cs +++ b/osu.Framework/Graphics/Easing.cs @@ -1,48 +1,48 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics -{ - /// - /// See http://easings.net/ for more samples. - /// - public enum Easing - { - None, - Out, - In, - InQuad, - OutQuad, - InOutQuad, - InCubic, - OutCubic, - InOutCubic, - InQuart, - OutQuart, - InOutQuart, - InQuint, - OutQuint, - InOutQuint, - InSine, - OutSine, - InOutSine, - InExpo, - OutExpo, - InOutExpo, - InCirc, - OutCirc, - InOutCirc, - InElastic, - OutElastic, - OutElasticHalf, - OutElasticQuarter, - InOutElastic, - InBack, - OutBack, - InOutBack, - InBounce, - OutBounce, - InOutBounce, - OutPow10, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics +{ + /// + /// See http://easings.net/ for more samples. + /// + public enum Easing + { + None, + Out, + In, + InQuad, + OutQuad, + InOutQuad, + InCubic, + OutCubic, + InOutCubic, + InQuart, + OutQuart, + InOutQuart, + InQuint, + OutQuint, + InOutQuint, + InSine, + OutSine, + InOutSine, + InExpo, + OutExpo, + InOutExpo, + InCirc, + OutCirc, + InOutCirc, + InElastic, + OutElastic, + OutElasticHalf, + OutElasticQuarter, + InOutElastic, + InBack, + OutBack, + InOutBack, + InBounce, + OutBounce, + InOutBounce, + OutPow10, + } +} diff --git a/osu.Framework/Graphics/Effects/BlurEffect.cs b/osu.Framework/Graphics/Effects/BlurEffect.cs index 1c0c905e3..12c750d5c 100644 --- a/osu.Framework/Graphics/Effects/BlurEffect.cs +++ b/osu.Framework/Graphics/Effects/BlurEffect.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics.Effects -{ - /// - /// A blur effect that wraps a drawable in a which applies a blur effect to it. - /// - public class BlurEffect : IEffect - { - /// - /// The strength of the blur. Default is 1. - /// - public float Strength = 1f; - - /// - /// The sigma of the blur. Default is (2, 2). - /// - public Vector2 Sigma = new Vector2(2f, 2f); - - /// - /// The rotation of the blur. Default is 0. - /// - public float Rotation; - - /// - /// The colour of the blur. Default is . - /// - public ColourInfo Colour = Color4.White; - - /// - /// The blending mode of the blur. Default is inheriting from the target drawable. - /// - public BlendingParameters Blending; - - /// - /// Whether to draw the blur in front or behind the original. Default is behind. - /// - public EffectPlacement Placement; - - /// - /// Whether to draw the original target in addition to its blurred version. - /// - public bool DrawOriginal; - - /// - /// Whether to automatically pad by the blur extent such that no clipping occurs at the sides of the effect. Default is false. - /// - public bool PadExtent; - - /// - /// Whether the resulting should cache its drawn framebuffer. - /// - public bool CacheDrawnEffect; - - public BufferedContainer ApplyTo(Drawable drawable) - { - return new BufferedContainer - { - BlurSigma = Sigma, - BlurRotation = Rotation, - EffectColour = Colour.MultiplyAlpha(Strength), - EffectBlending = Blending, - EffectPlacement = Placement, - - DrawOriginal = DrawOriginal, - - CacheDrawnFrameBuffer = CacheDrawnEffect, - - Padding = !PadExtent ? new MarginPadding() : new MarginPadding - { - Horizontal = Blur.KernelSize(Sigma.X), - Vertical = Blur.KernelSize(Sigma.Y), - }, - }.Wrap(drawable); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics.Effects +{ + /// + /// A blur effect that wraps a drawable in a which applies a blur effect to it. + /// + public class BlurEffect : IEffect + { + /// + /// The strength of the blur. Default is 1. + /// + public float Strength = 1f; + + /// + /// The sigma of the blur. Default is (2, 2). + /// + public Vector2 Sigma = new Vector2(2f, 2f); + + /// + /// The rotation of the blur. Default is 0. + /// + public float Rotation; + + /// + /// The colour of the blur. Default is . + /// + public ColourInfo Colour = Color4.White; + + /// + /// The blending mode of the blur. Default is inheriting from the target drawable. + /// + public BlendingParameters Blending; + + /// + /// Whether to draw the blur in front or behind the original. Default is behind. + /// + public EffectPlacement Placement; + + /// + /// Whether to draw the original target in addition to its blurred version. + /// + public bool DrawOriginal; + + /// + /// Whether to automatically pad by the blur extent such that no clipping occurs at the sides of the effect. Default is false. + /// + public bool PadExtent; + + /// + /// Whether the resulting should cache its drawn framebuffer. + /// + public bool CacheDrawnEffect; + + public BufferedContainer ApplyTo(Drawable drawable) + { + return new BufferedContainer + { + BlurSigma = Sigma, + BlurRotation = Rotation, + EffectColour = Colour.MultiplyAlpha(Strength), + EffectBlending = Blending, + EffectPlacement = Placement, + + DrawOriginal = DrawOriginal, + + CacheDrawnFrameBuffer = CacheDrawnEffect, + + Padding = !PadExtent ? new MarginPadding() : new MarginPadding + { + Horizontal = Blur.KernelSize(Sigma.X), + Vertical = Blur.KernelSize(Sigma.Y), + }, + }.Wrap(drawable); + } + } +} diff --git a/osu.Framework/Graphics/Effects/EdgeEffect.cs b/osu.Framework/Graphics/Effects/EdgeEffect.cs index 5b7665b95..bc0710334 100644 --- a/osu.Framework/Graphics/Effects/EdgeEffect.cs +++ b/osu.Framework/Graphics/Effects/EdgeEffect.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Graphics.Effects -{ - /// - /// An effect applied around the edge of the target drawable. - /// - public class EdgeEffect : IEffect - { - /// - /// The parameters of the edge effect. - /// - public EdgeEffectParameters Parameters; - - /// - /// Determines how large a radius is masked away around the corners. Default is 0. - /// - public float CornerRadius; - - public Container ApplyTo(Drawable drawable) - { - return new Container - { - Masking = true, - EdgeEffect = Parameters, - CornerRadius = CornerRadius, - }.Wrap(drawable); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Graphics.Effects +{ + /// + /// An effect applied around the edge of the target drawable. + /// + public class EdgeEffect : IEffect + { + /// + /// The parameters of the edge effect. + /// + public EdgeEffectParameters Parameters; + + /// + /// Determines how large a radius is masked away around the corners. Default is 0. + /// + public float CornerRadius; + + public Container ApplyTo(Drawable drawable) + { + return new Container + { + Masking = true, + EdgeEffect = Parameters, + CornerRadius = CornerRadius, + }.Wrap(drawable); + } + } +} diff --git a/osu.Framework/Graphics/Effects/EffectExtensions.cs b/osu.Framework/Graphics/Effects/EffectExtensions.cs index 55d5eaeb6..688b524d9 100644 --- a/osu.Framework/Graphics/Effects/EffectExtensions.cs +++ b/osu.Framework/Graphics/Effects/EffectExtensions.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Graphics.Effects -{ - /// - /// This class holds extension methods for effects. - /// - public static class EffectExtensions - { - /// - /// Applies the given effect to the given drawable and optionally initializes the created drawable with the given initializationAction. - /// - /// The type of the drawable that results from applying the given effect. - /// The effect to apply to the drawable. - /// The drawable to apply the effect to. - /// The action that should get called to initialize the created drawable before it is returned. - /// The drawable created by applying the given effect to this drawable. - public static T ApplyTo(this IEffect effect, Drawable drawable, Action initializationAction = null) where T : Drawable - => drawable.WithEffect(effect, initializationAction); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Graphics.Effects +{ + /// + /// This class holds extension methods for effects. + /// + public static class EffectExtensions + { + /// + /// Applies the given effect to the given drawable and optionally initializes the created drawable with the given initializationAction. + /// + /// The type of the drawable that results from applying the given effect. + /// The effect to apply to the drawable. + /// The drawable to apply the effect to. + /// The action that should get called to initialize the created drawable before it is returned. + /// The drawable created by applying the given effect to this drawable. + public static T ApplyTo(this IEffect effect, Drawable drawable, Action initializationAction = null) where T : Drawable + => drawable.WithEffect(effect, initializationAction); + } +} diff --git a/osu.Framework/Graphics/Effects/GlowEffect.cs b/osu.Framework/Graphics/Effects/GlowEffect.cs index 9e56c455e..e423a370e 100644 --- a/osu.Framework/Graphics/Effects/GlowEffect.cs +++ b/osu.Framework/Graphics/Effects/GlowEffect.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Graphics.Effects -{ - /// - /// Creates a glow around the drawable this effect gets applied to. - /// - public class GlowEffect : IEffect - { - /// - /// The strength of the glow. A higher strength means that the glow fades outward slower. Default is 1. - /// - public float Strength = 1f; - - /// - /// The sigma value for the blur of the glow. This controls how spread out the glow is. Default is 5 in both X and Y. - /// - public Vector2 BlurSigma = new Vector2(5); - - /// - /// The color of the outline. Default is . - /// - public ColourInfo Colour = Color4.White; - - /// - /// The blending mode of the glow. Default is additive. - /// - public BlendingParameters Blending = BlendingMode.Additive; - - /// - /// Whether to draw the glow or the glowing - /// . Default is . - /// - public EffectPlacement Placement = EffectPlacement.InFront; - - /// - /// Whether to automatically pad by the glow extent such that no clipping occurs at the sides of the effect. Default is false. - /// - public bool PadExtent; - - /// - /// True if the effect should be cached. This is an optimization, but can cause issues if the drawable changes the way it looks without changing its size. - /// Turned off by default. - /// - public bool CacheDrawnEffect; - - public BufferedContainer ApplyTo(Drawable drawable) => drawable.WithEffect(new BlurEffect - { - Strength = Strength, - Sigma = BlurSigma, - Colour = Colour, - Blending = Blending, - Placement = Placement, - PadExtent = PadExtent, - CacheDrawnEffect = CacheDrawnEffect, - - DrawOriginal = true, - }); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Graphics.Effects +{ + /// + /// Creates a glow around the drawable this effect gets applied to. + /// + public class GlowEffect : IEffect + { + /// + /// The strength of the glow. A higher strength means that the glow fades outward slower. Default is 1. + /// + public float Strength = 1f; + + /// + /// The sigma value for the blur of the glow. This controls how spread out the glow is. Default is 5 in both X and Y. + /// + public Vector2 BlurSigma = new Vector2(5); + + /// + /// The color of the outline. Default is . + /// + public ColourInfo Colour = Color4.White; + + /// + /// The blending mode of the glow. Default is additive. + /// + public BlendingParameters Blending = BlendingMode.Additive; + + /// + /// Whether to draw the glow or the glowing + /// . Default is . + /// + public EffectPlacement Placement = EffectPlacement.InFront; + + /// + /// Whether to automatically pad by the glow extent such that no clipping occurs at the sides of the effect. Default is false. + /// + public bool PadExtent; + + /// + /// True if the effect should be cached. This is an optimization, but can cause issues if the drawable changes the way it looks without changing its size. + /// Turned off by default. + /// + public bool CacheDrawnEffect; + + public BufferedContainer ApplyTo(Drawable drawable) => drawable.WithEffect(new BlurEffect + { + Strength = Strength, + Sigma = BlurSigma, + Colour = Colour, + Blending = Blending, + Placement = Placement, + PadExtent = PadExtent, + CacheDrawnEffect = CacheDrawnEffect, + + DrawOriginal = true, + }); + } +} diff --git a/osu.Framework/Graphics/Effects/IEffect.cs b/osu.Framework/Graphics/Effects/IEffect.cs index 177693b11..9bbd1168f 100644 --- a/osu.Framework/Graphics/Effects/IEffect.cs +++ b/osu.Framework/Graphics/Effects/IEffect.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - - -namespace osu.Framework.Graphics.Effects -{ - /// - /// Represents an effect that can be applied to a drawable. - /// - /// The type of the drawable that is created as a result of applying the effect to a drawable. - public interface IEffect where T : Drawable - { - /// - /// Applies this effect to the given drawable. - /// - /// The drawable to apply this effect to. - /// A new drawable derived from the given drawable with the effect applied. - T ApplyTo(Drawable drawable); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + + +namespace osu.Framework.Graphics.Effects +{ + /// + /// Represents an effect that can be applied to a drawable. + /// + /// The type of the drawable that is created as a result of applying the effect to a drawable. + public interface IEffect where T : Drawable + { + /// + /// Applies this effect to the given drawable. + /// + /// The drawable to apply this effect to. + /// A new drawable derived from the given drawable with the effect applied. + T ApplyTo(Drawable drawable); + } +} diff --git a/osu.Framework/Graphics/Effects/OutlineEffect.cs b/osu.Framework/Graphics/Effects/OutlineEffect.cs index 1604dfae2..e695a4a39 100644 --- a/osu.Framework/Graphics/Effects/OutlineEffect.cs +++ b/osu.Framework/Graphics/Effects/OutlineEffect.cs @@ -1,55 +1,55 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Graphics.Effects -{ - /// - /// Creates an outline around the drawable this effect gets applied to. - /// - public class OutlineEffect : IEffect - { - /// - /// The strength of the outline. A higher strength means that the blur effect used to draw the outline fades slower. - /// Default is 1. - /// - public float Strength = 1f; - - /// - /// The sigma value for the blur effect used to draw the outline. This controls over how many pixels the outline gets spread. - /// Default is . - /// - public Vector2 BlurSigma = Vector2.One; - - /// - /// The color of the outline. Default is . - /// - public ColourInfo Colour = Color4.Black; - - /// - /// Whether to automatically pad by the blur extent such that no clipping occurs at the sides of the effect. Default is false. - /// - public bool PadExtent; - - /// - /// True if the effect should be cached. This is an optimization, but can cause issues if the drawable changes the way it looks without changing its size. - /// Turned off by default. - /// - public bool CacheDrawnEffect; - - public BufferedContainer ApplyTo(Drawable drawable) => drawable.WithEffect(new BlurEffect - { - Strength = Strength, - Sigma = BlurSigma, - Colour = Colour, - PadExtent = PadExtent, - CacheDrawnEffect = CacheDrawnEffect, - - DrawOriginal = true, - }); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Graphics.Effects +{ + /// + /// Creates an outline around the drawable this effect gets applied to. + /// + public class OutlineEffect : IEffect + { + /// + /// The strength of the outline. A higher strength means that the blur effect used to draw the outline fades slower. + /// Default is 1. + /// + public float Strength = 1f; + + /// + /// The sigma value for the blur effect used to draw the outline. This controls over how many pixels the outline gets spread. + /// Default is . + /// + public Vector2 BlurSigma = Vector2.One; + + /// + /// The color of the outline. Default is . + /// + public ColourInfo Colour = Color4.Black; + + /// + /// Whether to automatically pad by the blur extent such that no clipping occurs at the sides of the effect. Default is false. + /// + public bool PadExtent; + + /// + /// True if the effect should be cached. This is an optimization, but can cause issues if the drawable changes the way it looks without changing its size. + /// Turned off by default. + /// + public bool CacheDrawnEffect; + + public BufferedContainer ApplyTo(Drawable drawable) => drawable.WithEffect(new BlurEffect + { + Strength = Strength, + Sigma = BlurSigma, + Colour = Colour, + PadExtent = PadExtent, + CacheDrawnEffect = CacheDrawnEffect, + + DrawOriginal = true, + }); + } +} diff --git a/osu.Framework/Graphics/IDrawable.cs b/osu.Framework/Graphics/IDrawable.cs index b53171120..ffca9fe8c 100644 --- a/osu.Framework/Graphics/IDrawable.cs +++ b/osu.Framework/Graphics/IDrawable.cs @@ -1,101 +1,101 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Timing; - -namespace osu.Framework.Graphics -{ - /// - /// Exposes various properties that are part of the public interface of . - /// This interface should generally NOT be implemented by other classes than , but only used to - /// specify that an object is of type . - /// It is mostly useful in cases where you need to specify additional constraints on a , but also do not want to force inheriting from - /// any particular subclass of . - /// - public interface IDrawable : ITransformable - { - /// - /// Absolute size of this Drawable in the 's coordinate system. - /// - Vector2 DrawSize { get; } - - /// - /// Contains a linear transformation, colour information, and blending information - /// of this drawable. - /// - DrawInfo DrawInfo { get; } - - /// - /// The screen-space quad this drawable occupies. - /// - Quad ScreenSpaceDrawQuad { get; } - - /// - /// The parent of this drawable in the scene graph. - /// - CompositeDrawable Parent { get; } - - /// - /// Whether this drawable is present for any sort of user-interaction. - /// If this is false, then this drawable will not be drawn, it will not handle input, - /// and it will not affect layouting (e.g. autosizing and flow). - /// - bool IsPresent { get; } - - /// - /// The clock of this drawable. Used for keeping track of time across frames. - /// - IFrameBasedClock Clock { get; } - - /// - /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space. - /// - /// A vector in local coordinates. - /// The drawable in which space we want to transform the vector to. - /// The vector in other's coordinates. - Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other); - - /// - /// Convert a position to the local coordinate system from either native or local to another drawable. - /// This is *not* the same space as the Position member variable (use Parent.GetLocalPosition() in this case). - /// - /// The input position. - /// The output position. - Vector2 ToLocalSpace(Vector2 screenSpacePos); - - /// - /// Determines how this Drawable is blended with other already drawn Drawables. - /// - BlendingParameters Blending { get; } - - /// - /// Whether this Drawable is currently hovered over. - /// - bool IsHovered { get; } - - /// - /// Whether this Drawable is currently dragged. - /// - bool IsDragged { get; } - - /// - /// Multiplicative alpha factor applied on top of and its existing - /// alpha channel(s). - /// - float Alpha { get; } - - /// - /// Show sprite instantly. - /// - void Show(); - - /// - /// Hide sprite instantly. - /// - void Hide(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Timing; + +namespace osu.Framework.Graphics +{ + /// + /// Exposes various properties that are part of the public interface of . + /// This interface should generally NOT be implemented by other classes than , but only used to + /// specify that an object is of type . + /// It is mostly useful in cases where you need to specify additional constraints on a , but also do not want to force inheriting from + /// any particular subclass of . + /// + public interface IDrawable : ITransformable + { + /// + /// Absolute size of this Drawable in the 's coordinate system. + /// + Vector2 DrawSize { get; } + + /// + /// Contains a linear transformation, colour information, and blending information + /// of this drawable. + /// + DrawInfo DrawInfo { get; } + + /// + /// The screen-space quad this drawable occupies. + /// + Quad ScreenSpaceDrawQuad { get; } + + /// + /// The parent of this drawable in the scene graph. + /// + CompositeDrawable Parent { get; } + + /// + /// Whether this drawable is present for any sort of user-interaction. + /// If this is false, then this drawable will not be drawn, it will not handle input, + /// and it will not affect layouting (e.g. autosizing and flow). + /// + bool IsPresent { get; } + + /// + /// The clock of this drawable. Used for keeping track of time across frames. + /// + IFrameBasedClock Clock { get; } + + /// + /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space. + /// + /// A vector in local coordinates. + /// The drawable in which space we want to transform the vector to. + /// The vector in other's coordinates. + Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other); + + /// + /// Convert a position to the local coordinate system from either native or local to another drawable. + /// This is *not* the same space as the Position member variable (use Parent.GetLocalPosition() in this case). + /// + /// The input position. + /// The output position. + Vector2 ToLocalSpace(Vector2 screenSpacePos); + + /// + /// Determines how this Drawable is blended with other already drawn Drawables. + /// + BlendingParameters Blending { get; } + + /// + /// Whether this Drawable is currently hovered over. + /// + bool IsHovered { get; } + + /// + /// Whether this Drawable is currently dragged. + /// + bool IsDragged { get; } + + /// + /// Multiplicative alpha factor applied on top of and its existing + /// alpha channel(s). + /// + float Alpha { get; } + + /// + /// Show sprite instantly. + /// + void Show(); + + /// + /// Hide sprite instantly. + /// + void Hide(); + } +} diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs index 627f147aa..19930dc45 100644 --- a/osu.Framework/Graphics/Lines/Path.cs +++ b/osu.Framework/Graphics/Lines/Path.cs @@ -1,200 +1,200 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Textures; -using OpenTK; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Allocation; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Caching; - -namespace osu.Framework.Graphics.Lines -{ - public class Path : Drawable - { - private List positions = new List(); - - public List Positions - { - set - { - if (positions == value) return; - - positions = value; - recomputeBounds(); - - segmentsCache.Invalidate(); - Invalidate(Invalidation.DrawNode); - } - } - - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) - { - var localPos = ToLocalSpace(screenSpacePos); - var pathWidthSquared = PathWidth * PathWidth; - - return segments.Any(s => s.DistanceSquaredToPoint(localPos) <= pathWidthSquared); - } - - public Vector2 PositionInBoundingBox(Vector2 pos) => pos - new Vector2(minX, minY); - - public void ClearVertices() - { - if (positions.Count == 0) - return; - - positions.Clear(); - resetBounds(); - - if ((RelativeSizeAxes & Axes.X) == 0) Width = 0; - if ((RelativeSizeAxes & Axes.Y) == 0) Height = 0; - - segmentsCache.Invalidate(); - Invalidate(Invalidation.DrawNode); - } - - public void AddVertex(Vector2 pos) - { - positions.Add(pos); - expandBounds(pos); - - segmentsCache.Invalidate(); - Invalidate(Invalidation.DrawNode); - } - - private float minX; - private float minY; - private float maxX; - private float maxY; - - private RectangleF bounds => new RectangleF(minX, minY, maxX - minX, maxY - minY); - - private void expandBounds(Vector2 pos) - { - if (pos.X - PathWidth < minX) minX = pos.X - PathWidth; - if (pos.Y - PathWidth < minY) minY = pos.Y - PathWidth; - if (pos.X + PathWidth > maxX) maxX = pos.X + PathWidth; - if (pos.Y + PathWidth > maxY) maxY = pos.Y + PathWidth; - - RectangleF b = bounds; - if ((RelativeSizeAxes & Axes.X) == 0) Width = b.Width; - if ((RelativeSizeAxes & Axes.Y) == 0) Height = b.Height; - } - - private void resetBounds() - { - minX = minY = maxX = maxY = 0; - } - - private void recomputeBounds() - { - resetBounds(); - foreach (Vector2 pos in positions) - expandBounds(pos); - } - - private float pathWidth = 10f; - - public float PathWidth - { - get { return pathWidth; } - set - { - if (pathWidth == value) return; - - pathWidth = value; - recomputeBounds(); - - segmentsCache.Invalidate(); - Invalidate(Invalidation.DrawNode); - } - } - - private readonly List segmentsBacking = new List(); - private Cached segmentsCache = new Cached(); - private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments(); - - private List generateSegments() - { - segmentsBacking.Clear(); - - if (positions.Count > 1) - { - Vector2 offset = new Vector2(minX, minY); - for (int i = 0; i < positions.Count - 1; ++i) - segmentsBacking.Add(new Line(positions[i] - offset, positions[i + 1] - offset)); - } - - segmentsCache.Validate(); - return segmentsBacking; - } - - private Shader roundedTextureShader; - private Shader textureShader; - - private readonly PathDrawNodeSharedData pathDrawNodeSharedData = new PathDrawNodeSharedData(); - - public bool CanDisposeTexture { get; protected set; } - - #region Disposal - - protected override void Dispose(bool isDisposing) - { - if (CanDisposeTexture) - { - texture?.Dispose(); - texture = null; - } - - base.Dispose(isDisposing); - } - - #endregion - - protected override DrawNode CreateDrawNode() => new PathDrawNode(); - - protected override void ApplyDrawNode(DrawNode node) - { - PathDrawNode n = (PathDrawNode)node; - - n.Texture = Texture; - n.TextureShader = textureShader; - n.RoundedTextureShader = roundedTextureShader; - n.Width = PathWidth; - n.DrawSize = DrawSize; - - n.Shared = pathDrawNodeSharedData; - - n.Segments = segments.ToList(); - - base.ApplyDrawNode(node); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED); - textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE); - } - - private Texture texture = Texture.WhitePixel; - - public Texture Texture - { - get { return texture; } - set - { - if (value == texture) - return; - - if (texture != null && CanDisposeTexture) - texture.Dispose(); - - texture = value; - Invalidate(Invalidation.DrawNode); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Textures; +using OpenTK; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Allocation; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Caching; + +namespace osu.Framework.Graphics.Lines +{ + public class Path : Drawable + { + private List positions = new List(); + + public List Positions + { + set + { + if (positions == value) return; + + positions = value; + recomputeBounds(); + + segmentsCache.Invalidate(); + Invalidate(Invalidation.DrawNode); + } + } + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) + { + var localPos = ToLocalSpace(screenSpacePos); + var pathWidthSquared = PathWidth * PathWidth; + + return segments.Any(s => s.DistanceSquaredToPoint(localPos) <= pathWidthSquared); + } + + public Vector2 PositionInBoundingBox(Vector2 pos) => pos - new Vector2(minX, minY); + + public void ClearVertices() + { + if (positions.Count == 0) + return; + + positions.Clear(); + resetBounds(); + + if ((RelativeSizeAxes & Axes.X) == 0) Width = 0; + if ((RelativeSizeAxes & Axes.Y) == 0) Height = 0; + + segmentsCache.Invalidate(); + Invalidate(Invalidation.DrawNode); + } + + public void AddVertex(Vector2 pos) + { + positions.Add(pos); + expandBounds(pos); + + segmentsCache.Invalidate(); + Invalidate(Invalidation.DrawNode); + } + + private float minX; + private float minY; + private float maxX; + private float maxY; + + private RectangleF bounds => new RectangleF(minX, minY, maxX - minX, maxY - minY); + + private void expandBounds(Vector2 pos) + { + if (pos.X - PathWidth < minX) minX = pos.X - PathWidth; + if (pos.Y - PathWidth < minY) minY = pos.Y - PathWidth; + if (pos.X + PathWidth > maxX) maxX = pos.X + PathWidth; + if (pos.Y + PathWidth > maxY) maxY = pos.Y + PathWidth; + + RectangleF b = bounds; + if ((RelativeSizeAxes & Axes.X) == 0) Width = b.Width; + if ((RelativeSizeAxes & Axes.Y) == 0) Height = b.Height; + } + + private void resetBounds() + { + minX = minY = maxX = maxY = 0; + } + + private void recomputeBounds() + { + resetBounds(); + foreach (Vector2 pos in positions) + expandBounds(pos); + } + + private float pathWidth = 10f; + + public float PathWidth + { + get { return pathWidth; } + set + { + if (pathWidth == value) return; + + pathWidth = value; + recomputeBounds(); + + segmentsCache.Invalidate(); + Invalidate(Invalidation.DrawNode); + } + } + + private readonly List segmentsBacking = new List(); + private Cached segmentsCache = new Cached(); + private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments(); + + private List generateSegments() + { + segmentsBacking.Clear(); + + if (positions.Count > 1) + { + Vector2 offset = new Vector2(minX, minY); + for (int i = 0; i < positions.Count - 1; ++i) + segmentsBacking.Add(new Line(positions[i] - offset, positions[i + 1] - offset)); + } + + segmentsCache.Validate(); + return segmentsBacking; + } + + private Shader roundedTextureShader; + private Shader textureShader; + + private readonly PathDrawNodeSharedData pathDrawNodeSharedData = new PathDrawNodeSharedData(); + + public bool CanDisposeTexture { get; protected set; } + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + if (CanDisposeTexture) + { + texture?.Dispose(); + texture = null; + } + + base.Dispose(isDisposing); + } + + #endregion + + protected override DrawNode CreateDrawNode() => new PathDrawNode(); + + protected override void ApplyDrawNode(DrawNode node) + { + PathDrawNode n = (PathDrawNode)node; + + n.Texture = Texture; + n.TextureShader = textureShader; + n.RoundedTextureShader = roundedTextureShader; + n.Width = PathWidth; + n.DrawSize = DrawSize; + + n.Shared = pathDrawNodeSharedData; + + n.Segments = segments.ToList(); + + base.ApplyDrawNode(node); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED); + textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE); + } + + private Texture texture = Texture.WhitePixel; + + public Texture Texture + { + get { return texture; } + set + { + if (value == texture) + return; + + if (texture != null && CanDisposeTexture) + texture.Dispose(); + + texture = value; + Invalidate(Invalidation.DrawNode); + } + } + } +} diff --git a/osu.Framework/Graphics/Lines/PathDrawNode.cs b/osu.Framework/Graphics/Lines/PathDrawNode.cs index 72a1f0e0a..4812019fe 100644 --- a/osu.Framework/Graphics/Lines/PathDrawNode.cs +++ b/osu.Framework/Graphics/Lines/PathDrawNode.cs @@ -1,210 +1,210 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL; -using OpenTK; -using System; -using System.Collections.Generic; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL.Vertices; -using OpenTK.Graphics; - -namespace osu.Framework.Graphics.Lines -{ - public class PathDrawNodeSharedData - { - // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices - // per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong - // grouping of vertices into primitives. - public LinearBatch HalfCircleBatch = new LinearBatch(PathDrawNode.MAXRES * 100 * 3, 10, PrimitiveType.Triangles); - public QuadBatch QuadBatch = new QuadBatch(200, 10); - } - - public class PathDrawNode : DrawNode - { - public const int MAXRES = 24; - public List Segments; - - public Vector2 DrawSize; - public float Width; - public Texture Texture; - - public Shader TextureShader; - public Shader RoundedTextureShader; - - public PathDrawNodeSharedData Shared; - - private bool needsRoundedShader => GLWrapper.IsMaskingActive; - - private Vector2 pointOnCircle(float angle) => new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)); - - private Vector2 relativePosition(Vector2 localPos) => Vector2.Divide(localPos, DrawSize); - - private Color4 colourAt(Vector2 localPos) => DrawInfo.Colour.HasSingleColour - ? (Color4)DrawInfo.Colour - : DrawInfo.Colour.Interpolate(relativePosition(localPos)).Linear; - - private void addLineCap(Vector2 origin, float theta, float thetaDiff) - { - const float step = MathHelper.Pi / MAXRES; - - float dir = Math.Sign(thetaDiff); - thetaDiff = dir * thetaDiff; - - int amountPoints = (int)Math.Ceiling(thetaDiff / step); - - if (dir < 0) - theta += MathHelper.Pi; - - Vector2 current = origin + pointOnCircle(theta) * Width; - Color4 currentColour = colourAt(current); - current = Vector2Extensions.Transform(current, DrawInfo.Matrix); - - Vector2 screenOrigin = Vector2Extensions.Transform(origin, DrawInfo.Matrix); - Color4 originColour = colourAt(origin); - - for (int i = 1; i <= amountPoints; i++) - { - // Center point - Shared.HalfCircleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(screenOrigin.X, screenOrigin.Y, 1), - TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), - Colour = originColour - }); - - // First outer point - Shared.HalfCircleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(current.X, current.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = currentColour - }); - - float angularOffset = Math.Min(i * step, thetaDiff); - current = origin + pointOnCircle(theta + dir * angularOffset) * Width; - currentColour = colourAt(current); - current = Vector2Extensions.Transform(current, DrawInfo.Matrix); - - // Second outer point - Shared.HalfCircleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(current.X, current.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = currentColour - }); - } - } - - private void addLineQuads(Line line) - { - Vector2 ortho = line.OrthogonalDirection; - Line lineLeft = new Line(line.StartPoint + ortho * Width, line.EndPoint + ortho * Width); - Line lineRight = new Line(line.StartPoint - ortho * Width, line.EndPoint - ortho * Width); - - Line screenLineLeft = new Line(Vector2Extensions.Transform(lineLeft.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(lineLeft.EndPoint, DrawInfo.Matrix)); - Line screenLineRight = new Line(Vector2Extensions.Transform(lineRight.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(lineRight.EndPoint, DrawInfo.Matrix)); - Line screenLine = new Line(Vector2Extensions.Transform(line.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(line.EndPoint, DrawInfo.Matrix)); - - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = new Vector3(screenLineRight.EndPoint.X, screenLineRight.EndPoint.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = colourAt(lineRight.EndPoint) - }); - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = new Vector3(screenLineRight.StartPoint.X, screenLineRight.StartPoint.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = colourAt(lineRight.StartPoint) - }); - - // Each "quad" of the slider is actually rendered as 2 quads, being split in half along the approximating line. - // On this line the depth is 1 instead of 0, which is done properly handle self-overlap using the depth buffer. - // Thus the middle vertices need to be added twice (once for each quad). - Vector3 firstMiddlePoint = new Vector3(screenLine.StartPoint.X, screenLine.StartPoint.Y, 1); - Vector3 secondMiddlePoint = new Vector3(screenLine.EndPoint.X, screenLine.EndPoint.Y, 1); - Color4 firstMiddleColour = colourAt(line.StartPoint); - Color4 secondMiddleColour = colourAt(line.EndPoint); - - for (int i = 0; i < 2; ++i) - { - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = firstMiddlePoint, - TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), - Colour = firstMiddleColour - }); - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = secondMiddlePoint, - TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), - Colour = secondMiddleColour - }); - } - - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = new Vector3(screenLineLeft.EndPoint.X, screenLineLeft.EndPoint.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = colourAt(lineLeft.EndPoint) - }); - Shared.QuadBatch.Add(new TexturedVertex3D - { - Position = new Vector3(screenLineLeft.StartPoint.X, screenLineLeft.StartPoint.Y, 0), - TexturePosition = new Vector2(0, 0), - Colour = colourAt(lineLeft.StartPoint) - }); - } - - private void updateVertexBuffer() - { - Line line = Segments[0]; - float theta = line.Theta; - addLineCap(line.StartPoint, theta + MathHelper.Pi, MathHelper.Pi); - - for (int i = 1; i < Segments.Count; ++i) - { - Line nextLine = Segments[i]; - float nextTheta = nextLine.Theta; - addLineCap(line.EndPoint, theta, nextTheta - theta); - - line = nextLine; - theta = nextTheta; - } - - addLineCap(line.EndPoint, theta, MathHelper.Pi); - - - foreach (Line segment in Segments) - addLineQuads(segment); - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - if (Texture == null || Texture.IsDisposed || Segments.Count == 0) - return; - - GLWrapper.SetDepthTest(true); - - Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; - - shader.Bind(); - - Texture.TextureGL.WrapMode = TextureWrapMode.ClampToEdge; - Texture.TextureGL.Bind(); - - updateVertexBuffer(); - - shader.Unbind(); - - GLWrapper.SetDepthTest(false); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL; +using OpenTK; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.OpenGL.Vertices; +using OpenTK.Graphics; + +namespace osu.Framework.Graphics.Lines +{ + public class PathDrawNodeSharedData + { + // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices + // per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong + // grouping of vertices into primitives. + public LinearBatch HalfCircleBatch = new LinearBatch(PathDrawNode.MAXRES * 100 * 3, 10, PrimitiveType.Triangles); + public QuadBatch QuadBatch = new QuadBatch(200, 10); + } + + public class PathDrawNode : DrawNode + { + public const int MAXRES = 24; + public List Segments; + + public Vector2 DrawSize; + public float Width; + public Texture Texture; + + public Shader TextureShader; + public Shader RoundedTextureShader; + + public PathDrawNodeSharedData Shared; + + private bool needsRoundedShader => GLWrapper.IsMaskingActive; + + private Vector2 pointOnCircle(float angle) => new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)); + + private Vector2 relativePosition(Vector2 localPos) => Vector2.Divide(localPos, DrawSize); + + private Color4 colourAt(Vector2 localPos) => DrawInfo.Colour.HasSingleColour + ? (Color4)DrawInfo.Colour + : DrawInfo.Colour.Interpolate(relativePosition(localPos)).Linear; + + private void addLineCap(Vector2 origin, float theta, float thetaDiff) + { + const float step = MathHelper.Pi / MAXRES; + + float dir = Math.Sign(thetaDiff); + thetaDiff = dir * thetaDiff; + + int amountPoints = (int)Math.Ceiling(thetaDiff / step); + + if (dir < 0) + theta += MathHelper.Pi; + + Vector2 current = origin + pointOnCircle(theta) * Width; + Color4 currentColour = colourAt(current); + current = Vector2Extensions.Transform(current, DrawInfo.Matrix); + + Vector2 screenOrigin = Vector2Extensions.Transform(origin, DrawInfo.Matrix); + Color4 originColour = colourAt(origin); + + for (int i = 1; i <= amountPoints; i++) + { + // Center point + Shared.HalfCircleBatch.Add(new TexturedVertex3D + { + Position = new Vector3(screenOrigin.X, screenOrigin.Y, 1), + TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), + Colour = originColour + }); + + // First outer point + Shared.HalfCircleBatch.Add(new TexturedVertex3D + { + Position = new Vector3(current.X, current.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = currentColour + }); + + float angularOffset = Math.Min(i * step, thetaDiff); + current = origin + pointOnCircle(theta + dir * angularOffset) * Width; + currentColour = colourAt(current); + current = Vector2Extensions.Transform(current, DrawInfo.Matrix); + + // Second outer point + Shared.HalfCircleBatch.Add(new TexturedVertex3D + { + Position = new Vector3(current.X, current.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = currentColour + }); + } + } + + private void addLineQuads(Line line) + { + Vector2 ortho = line.OrthogonalDirection; + Line lineLeft = new Line(line.StartPoint + ortho * Width, line.EndPoint + ortho * Width); + Line lineRight = new Line(line.StartPoint - ortho * Width, line.EndPoint - ortho * Width); + + Line screenLineLeft = new Line(Vector2Extensions.Transform(lineLeft.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(lineLeft.EndPoint, DrawInfo.Matrix)); + Line screenLineRight = new Line(Vector2Extensions.Transform(lineRight.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(lineRight.EndPoint, DrawInfo.Matrix)); + Line screenLine = new Line(Vector2Extensions.Transform(line.StartPoint, DrawInfo.Matrix), Vector2Extensions.Transform(line.EndPoint, DrawInfo.Matrix)); + + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = new Vector3(screenLineRight.EndPoint.X, screenLineRight.EndPoint.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = colourAt(lineRight.EndPoint) + }); + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = new Vector3(screenLineRight.StartPoint.X, screenLineRight.StartPoint.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = colourAt(lineRight.StartPoint) + }); + + // Each "quad" of the slider is actually rendered as 2 quads, being split in half along the approximating line. + // On this line the depth is 1 instead of 0, which is done properly handle self-overlap using the depth buffer. + // Thus the middle vertices need to be added twice (once for each quad). + Vector3 firstMiddlePoint = new Vector3(screenLine.StartPoint.X, screenLine.StartPoint.Y, 1); + Vector3 secondMiddlePoint = new Vector3(screenLine.EndPoint.X, screenLine.EndPoint.Y, 1); + Color4 firstMiddleColour = colourAt(line.StartPoint); + Color4 secondMiddleColour = colourAt(line.EndPoint); + + for (int i = 0; i < 2; ++i) + { + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = firstMiddlePoint, + TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), + Colour = firstMiddleColour + }); + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = secondMiddlePoint, + TexturePosition = new Vector2(1 - 1 / Texture.Width, 0), + Colour = secondMiddleColour + }); + } + + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = new Vector3(screenLineLeft.EndPoint.X, screenLineLeft.EndPoint.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = colourAt(lineLeft.EndPoint) + }); + Shared.QuadBatch.Add(new TexturedVertex3D + { + Position = new Vector3(screenLineLeft.StartPoint.X, screenLineLeft.StartPoint.Y, 0), + TexturePosition = new Vector2(0, 0), + Colour = colourAt(lineLeft.StartPoint) + }); + } + + private void updateVertexBuffer() + { + Line line = Segments[0]; + float theta = line.Theta; + addLineCap(line.StartPoint, theta + MathHelper.Pi, MathHelper.Pi); + + for (int i = 1; i < Segments.Count; ++i) + { + Line nextLine = Segments[i]; + float nextTheta = nextLine.Theta; + addLineCap(line.EndPoint, theta, nextTheta - theta); + + line = nextLine; + theta = nextTheta; + } + + addLineCap(line.EndPoint, theta, MathHelper.Pi); + + + foreach (Line segment in Segments) + addLineQuads(segment); + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + if (Texture == null || Texture.IsDisposed || Segments.Count == 0) + return; + + GLWrapper.SetDepthTest(true); + + Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; + + shader.Bind(); + + Texture.TextureGL.WrapMode = TextureWrapMode.ClampToEdge; + Texture.TextureGL.Bind(); + + updateVertexBuffer(); + + shader.Unbind(); + + GLWrapper.SetDepthTest(false); + } + } +} diff --git a/osu.Framework/Graphics/MarginPadding.cs b/osu.Framework/Graphics/MarginPadding.cs index 64d359894..fdb33b5e7 100644 --- a/osu.Framework/Graphics/MarginPadding.cs +++ b/osu.Framework/Graphics/MarginPadding.cs @@ -1,101 +1,101 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; - -namespace osu.Framework.Graphics -{ - /// - /// Holds data about the margin or padding of a . - /// The margin describes the size of an empty area around its , while the padding describes the size of an empty area inside its container. - /// - public struct MarginPadding : IEquatable - { - /// - /// The absolute size of the space that should be left empty above the if used as margin, or - /// the absolute size of the space that should be left empty from the top of the container if used as padding. - /// - public float Top; - - /// - /// The absolute size of the space that should be left empty to the left of the if used as margin, or - /// the absolute size of the space that should be left empty from the left of the container if used as padding. - /// - public float Left; - - /// - /// The absolute size of the space that should be left empty below the if used as margin, or - /// the absolute size of the space that should be left empty from the bottom of the container if used as padding. - /// - public float Bottom; - - /// - /// The absolute size of the space that should be left empty to the right of the if used as margin, or - /// the absolute size of the space that should be left empty from the right of the container if used as padding. - /// - public float Right; - - /// - /// Gets the total absolute size of the empty space horizontally around the if used as margin, or - /// the absolute size of the space left empty from the right and left of the container if used as padding. - /// Effectively + . - /// - public float TotalHorizontal => Left + Right; - - /// - /// Sets the values of both and to the assigned value. - /// - public float Horizontal - { - set { Left = Right = value; } - } - - /// - /// Gets the total absolute size of the empty space vertically around the or - /// the absolute size of the space left empty from the top and bottom of the container if used as padding. - /// Effectively + . - /// - public float TotalVertical => Top + Bottom; - - /// - /// Sets the values of both and to the assigned value. - /// - public float Vertical - { - set { Top = Bottom = value; } - } - - /// - /// Gets the total absolute size of the empty space horizontally (x coordinate) and vertically (y coordinate) around the or inside the container if used as padding. - /// - public Vector2 Total => new Vector2(TotalHorizontal, TotalVertical); - - /// - /// Initializes all four sides (, , and ) to the given value. - /// - /// The absolute size of the space that should be left around every side of the . - public MarginPadding(float allSides) - { - Top = Left = Bottom = Right = allSides; - } - - public bool Equals(MarginPadding other) - { - return Top == other.Top && Left == other.Left && Bottom == other.Bottom && Right == other.Right; - } - - public override string ToString() => $@"({Top}, {Left}, {Bottom}, {Right})"; - - public static MarginPadding operator -(MarginPadding mp) - { - return new MarginPadding - { - Left = -mp.Left, - Top = -mp.Top, - Right = -mp.Right, - Bottom = -mp.Bottom, - }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; + +namespace osu.Framework.Graphics +{ + /// + /// Holds data about the margin or padding of a . + /// The margin describes the size of an empty area around its , while the padding describes the size of an empty area inside its container. + /// + public struct MarginPadding : IEquatable + { + /// + /// The absolute size of the space that should be left empty above the if used as margin, or + /// the absolute size of the space that should be left empty from the top of the container if used as padding. + /// + public float Top; + + /// + /// The absolute size of the space that should be left empty to the left of the if used as margin, or + /// the absolute size of the space that should be left empty from the left of the container if used as padding. + /// + public float Left; + + /// + /// The absolute size of the space that should be left empty below the if used as margin, or + /// the absolute size of the space that should be left empty from the bottom of the container if used as padding. + /// + public float Bottom; + + /// + /// The absolute size of the space that should be left empty to the right of the if used as margin, or + /// the absolute size of the space that should be left empty from the right of the container if used as padding. + /// + public float Right; + + /// + /// Gets the total absolute size of the empty space horizontally around the if used as margin, or + /// the absolute size of the space left empty from the right and left of the container if used as padding. + /// Effectively + . + /// + public float TotalHorizontal => Left + Right; + + /// + /// Sets the values of both and to the assigned value. + /// + public float Horizontal + { + set { Left = Right = value; } + } + + /// + /// Gets the total absolute size of the empty space vertically around the or + /// the absolute size of the space left empty from the top and bottom of the container if used as padding. + /// Effectively + . + /// + public float TotalVertical => Top + Bottom; + + /// + /// Sets the values of both and to the assigned value. + /// + public float Vertical + { + set { Top = Bottom = value; } + } + + /// + /// Gets the total absolute size of the empty space horizontally (x coordinate) and vertically (y coordinate) around the or inside the container if used as padding. + /// + public Vector2 Total => new Vector2(TotalHorizontal, TotalVertical); + + /// + /// Initializes all four sides (, , and ) to the given value. + /// + /// The absolute size of the space that should be left around every side of the . + public MarginPadding(float allSides) + { + Top = Left = Bottom = Right = allSides; + } + + public bool Equals(MarginPadding other) + { + return Top == other.Top && Left == other.Left && Bottom == other.Bottom && Right == other.Right; + } + + public override string ToString() => $@"({Top}, {Left}, {Bottom}, {Right})"; + + public static MarginPadding operator -(MarginPadding mp) + { + return new MarginPadding + { + Left = -mp.Left, + Top = -mp.Top, + Right = -mp.Right, + Bottom = -mp.Bottom, + }; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/FrameBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/FrameBuffer.cs index a3a7031b1..d57335d27 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/FrameBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/FrameBuffer.cs @@ -1,146 +1,146 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Graphics.OpenGL.Textures; -using OpenTK; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Buffers -{ - public class FrameBuffer : IDisposable - { - private int lastFramebuffer; - private int frameBuffer = -1; - - public TextureGL Texture { get; private set; } - - private bool isBound => lastFramebuffer != -1; - - private readonly List attachedRenderBuffers = new List(); - - #region Disposal - - ~FrameBuffer() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (isDisposed) - return; - isDisposed = true; - - GLWrapper.ScheduleDisposal(delegate - { - Unbind(); - GLWrapper.DeleteFramebuffer(frameBuffer); - frameBuffer = -1; - }); - } - - #endregion - - public bool IsInitialized { get; private set; } - - public void Initialize(bool withTexture = true, All filteringMode = All.Linear) - { - frameBuffer = GL.GenFramebuffer(); - - if (withTexture) - { - Texture = new TextureGLSingle(1, 1, true, filteringMode); - Texture.SetData(new TextureUpload(Array.Empty())); - Texture.Upload(); - - Bind(); - - GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget2d.Texture2D, Texture.TextureId, 0); - GLWrapper.BindTexture(null); - - Unbind(); - } - - IsInitialized = true; - } - - private Vector2 size = Vector2.One; - - /// - /// Sets the size of the texture of this framebuffer. - /// - public Vector2 Size - { - get { return size; } - set - { - if (value == size) - return; - size = value; - - Texture.Width = (int)Math.Ceiling(size.X); - Texture.Height = (int)Math.Ceiling(size.Y); - Texture.SetData(new TextureUpload(Array.Empty())); - Texture.Upload(); - } - } - - /// - /// Attaches a RenderBuffer to this framebuffer. - /// - /// The type of RenderBuffer to attach. - public void Attach(RenderbufferInternalFormat format) - { - if (attachedRenderBuffers.Exists(r => r.Format == format)) - return; - - attachedRenderBuffers.Add(new RenderBuffer(format)); - } - - /// - /// Binds the framebuffer. - /// Does not clear the buffer or reset the viewport/ortho. - /// - public void Bind() - { - if (frameBuffer == -1) - return; - - if (lastFramebuffer == frameBuffer) - return; - - // Bind framebuffer and all its renderbuffers - lastFramebuffer = GLWrapper.BindFrameBuffer(frameBuffer); - foreach (var r in attachedRenderBuffers) - { - r.Size = Size; - r.Bind(frameBuffer); - } - } - - /// - /// Unbinds the framebuffer. - /// - public void Unbind() - { - if (!isBound) - return; - - GLWrapper.BindFrameBuffer(lastFramebuffer); - foreach (var r in attachedRenderBuffers) - r.Unbind(); - - lastFramebuffer = -1; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.OpenGL.Textures; +using OpenTK; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + public class FrameBuffer : IDisposable + { + private int lastFramebuffer; + private int frameBuffer = -1; + + public TextureGL Texture { get; private set; } + + private bool isBound => lastFramebuffer != -1; + + private readonly List attachedRenderBuffers = new List(); + + #region Disposal + + ~FrameBuffer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + + GLWrapper.ScheduleDisposal(delegate + { + Unbind(); + GLWrapper.DeleteFramebuffer(frameBuffer); + frameBuffer = -1; + }); + } + + #endregion + + public bool IsInitialized { get; private set; } + + public void Initialize(bool withTexture = true, All filteringMode = All.Linear) + { + frameBuffer = GL.GenFramebuffer(); + + if (withTexture) + { + Texture = new TextureGLSingle(1, 1, true, filteringMode); + Texture.SetData(new TextureUpload(Array.Empty())); + Texture.Upload(); + + Bind(); + + GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget2d.Texture2D, Texture.TextureId, 0); + GLWrapper.BindTexture(null); + + Unbind(); + } + + IsInitialized = true; + } + + private Vector2 size = Vector2.One; + + /// + /// Sets the size of the texture of this framebuffer. + /// + public Vector2 Size + { + get { return size; } + set + { + if (value == size) + return; + size = value; + + Texture.Width = (int)Math.Ceiling(size.X); + Texture.Height = (int)Math.Ceiling(size.Y); + Texture.SetData(new TextureUpload(Array.Empty())); + Texture.Upload(); + } + } + + /// + /// Attaches a RenderBuffer to this framebuffer. + /// + /// The type of RenderBuffer to attach. + public void Attach(RenderbufferInternalFormat format) + { + if (attachedRenderBuffers.Exists(r => r.Format == format)) + return; + + attachedRenderBuffers.Add(new RenderBuffer(format)); + } + + /// + /// Binds the framebuffer. + /// Does not clear the buffer or reset the viewport/ortho. + /// + public void Bind() + { + if (frameBuffer == -1) + return; + + if (lastFramebuffer == frameBuffer) + return; + + // Bind framebuffer and all its renderbuffers + lastFramebuffer = GLWrapper.BindFrameBuffer(frameBuffer); + foreach (var r in attachedRenderBuffers) + { + r.Size = Size; + r.Bind(frameBuffer); + } + } + + /// + /// Unbinds the framebuffer. + /// + public void Unbind() + { + if (!isBound) + return; + + GLWrapper.BindFrameBuffer(lastFramebuffer); + foreach (var r in attachedRenderBuffers) + r.Unbind(); + + lastFramebuffer = -1; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/LinearVertexBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/LinearVertexBuffer.cs index ba4ccb82b..ebcb1604c 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/LinearVertexBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/LinearVertexBuffer.cs @@ -1,56 +1,56 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Vertices; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Buffers -{ - internal static class LinearIndexData - { - static LinearIndexData() - { - GL.GenBuffers(1, out EBO_ID); - } - - public static readonly int EBO_ID; - public static int MaxAmountIndices; - } - - /// - /// This type of vertex buffer lets the ith vertex be referenced by the ith index. - /// - public class LinearVertexBuffer : VertexBuffer - where T : struct, IEquatable, IVertex - { - public LinearVertexBuffer(int amountVertices, PrimitiveType type, BufferUsageHint usage) - : base(amountVertices, usage) - { - Type = type; - - if (amountVertices > LinearIndexData.MaxAmountIndices) - { - ushort[] indices = new ushort[amountVertices]; - - for (ushort i = 0; i < amountVertices; i++) - indices[i] = i; - - GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, LinearIndexData.EBO_ID); - GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(amountVertices * sizeof(ushort)), indices, BufferUsageHint.StaticDraw); - - LinearIndexData.MaxAmountIndices = amountVertices; - } - } - - public override void Bind(bool forRendering) - { - base.Bind(forRendering); - - if (forRendering) - GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, LinearIndexData.EBO_ID); - } - - protected override PrimitiveType Type { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Vertices; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + internal static class LinearIndexData + { + static LinearIndexData() + { + GL.GenBuffers(1, out EBO_ID); + } + + public static readonly int EBO_ID; + public static int MaxAmountIndices; + } + + /// + /// This type of vertex buffer lets the ith vertex be referenced by the ith index. + /// + public class LinearVertexBuffer : VertexBuffer + where T : struct, IEquatable, IVertex + { + public LinearVertexBuffer(int amountVertices, PrimitiveType type, BufferUsageHint usage) + : base(amountVertices, usage) + { + Type = type; + + if (amountVertices > LinearIndexData.MaxAmountIndices) + { + ushort[] indices = new ushort[amountVertices]; + + for (ushort i = 0; i < amountVertices; i++) + indices[i] = i; + + GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, LinearIndexData.EBO_ID); + GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(amountVertices * sizeof(ushort)), indices, BufferUsageHint.StaticDraw); + + LinearIndexData.MaxAmountIndices = amountVertices; + } + } + + public override void Bind(bool forRendering) + { + base.Bind(forRendering); + + if (forRendering) + GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, LinearIndexData.EBO_ID); + } + + protected override PrimitiveType Type { get; } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/QuadVertexBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/QuadVertexBuffer.cs index a226dd264..401f78d72 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/QuadVertexBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/QuadVertexBuffer.cs @@ -1,63 +1,63 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Vertices; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Buffers -{ - internal static class QuadIndexData - { - static QuadIndexData() - { - GL.GenBuffers(1, out EBO_ID); - } - - public static readonly int EBO_ID; - public static int MaxAmountIndices; - } - - public class QuadVertexBuffer : VertexBuffer - where T : struct, IEquatable, IVertex - { - public QuadVertexBuffer(int amountQuads, BufferUsageHint usage) - : base(amountQuads * 4, usage) - { - int amountIndices = amountQuads * 6; - if (amountIndices > QuadIndexData.MaxAmountIndices) - { - ushort[] indices = new ushort[amountIndices]; - - for (ushort i = 0, j = 0; j < amountIndices; i += 4, j += 6) - { - indices[j] = i; - indices[j + 1] = (ushort)(i + 1); - indices[j + 2] = (ushort)(i + 3); - indices[j + 3] = (ushort)(i + 2); - indices[j + 4] = (ushort)(i + 3); - indices[j + 5] = (ushort)(i + 1); - } - - GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, QuadIndexData.EBO_ID); - GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(amountIndices * sizeof(ushort)), indices, BufferUsageHint.StaticDraw); - - QuadIndexData.MaxAmountIndices = amountIndices; - } - } - - public override void Bind(bool forRendering) - { - base.Bind(forRendering); - - if (forRendering) - GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, QuadIndexData.EBO_ID); - } - - protected override int ToElements(int vertices) => 3 * vertices / 2; - - protected override int ToElementIndex(int vertexIndex) => 3 * vertexIndex / 2; - - protected override PrimitiveType Type => PrimitiveType.Triangles; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Vertices; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + internal static class QuadIndexData + { + static QuadIndexData() + { + GL.GenBuffers(1, out EBO_ID); + } + + public static readonly int EBO_ID; + public static int MaxAmountIndices; + } + + public class QuadVertexBuffer : VertexBuffer + where T : struct, IEquatable, IVertex + { + public QuadVertexBuffer(int amountQuads, BufferUsageHint usage) + : base(amountQuads * 4, usage) + { + int amountIndices = amountQuads * 6; + if (amountIndices > QuadIndexData.MaxAmountIndices) + { + ushort[] indices = new ushort[amountIndices]; + + for (ushort i = 0, j = 0; j < amountIndices; i += 4, j += 6) + { + indices[j] = i; + indices[j + 1] = (ushort)(i + 1); + indices[j + 2] = (ushort)(i + 3); + indices[j + 3] = (ushort)(i + 2); + indices[j + 4] = (ushort)(i + 3); + indices[j + 5] = (ushort)(i + 1); + } + + GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, QuadIndexData.EBO_ID); + GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(amountIndices * sizeof(ushort)), indices, BufferUsageHint.StaticDraw); + + QuadIndexData.MaxAmountIndices = amountIndices; + } + } + + public override void Bind(bool forRendering) + { + base.Bind(forRendering); + + if (forRendering) + GLWrapper.BindBuffer(BufferTarget.ElementArrayBuffer, QuadIndexData.EBO_ID); + } + + protected override int ToElements(int vertices) => 3 * vertices / 2; + + protected override int ToElementIndex(int vertexIndex) => 3 * vertexIndex / 2; + + protected override PrimitiveType Type => PrimitiveType.Triangles; + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/RenderBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/RenderBuffer.cs index e984ae6dd..7fec88ccb 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/RenderBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/RenderBuffer.cs @@ -1,133 +1,133 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Buffers -{ - public class RenderBuffer : IDisposable - { - private static readonly Dictionary> render_buffer_cache = new Dictionary>(); - - public Vector2 Size = Vector2.One; - public RenderbufferInternalFormat Format { get; } - - private RenderBufferInfo info; - private bool isDisposed; - - public RenderBuffer(RenderbufferInternalFormat format) - { - Format = format; - } - - #region Disposal - - ~RenderBuffer() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (isDisposed) - return; - isDisposed = true; - - Unbind(); - } - - #endregion - - /// - /// Binds the renderbuffer to the specfied framebuffer. - /// - /// The framebuffer this renderbuffer should be bound to. - internal void Bind(int frameBuffer) - { - // Check if we're already bound - if (info != null) - return; - - if (!render_buffer_cache.ContainsKey(Format)) - render_buffer_cache[Format] = new Stack(); - - // Make sure we have renderbuffers available - if (render_buffer_cache[Format].Count == 0) - render_buffer_cache[Format].Push(new RenderBufferInfo - { - RenderBufferID = GL.GenRenderbuffer(), - FrameBufferID = -1 - }); - - // Get a renderbuffer from the cache - info = render_buffer_cache[Format].Pop(); - - // Check if we need to update the size - if (info.Size != Size) - { - GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, info.RenderBufferID); - GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, Format, (int)Math.Ceiling(Size.X), (int)Math.Ceiling(Size.Y)); - - info.Size = Size; - } - - // For performance reasons, we only need to re-bind the renderbuffer to - // the framebuffer if it is not already attached to it - if (info.FrameBufferID != frameBuffer) - { - // Make sure the framebuffer we want to attach to is bound - int lastFrameBuffer = GLWrapper.BindFrameBuffer(frameBuffer); - - switch (Format) - { - case RenderbufferInternalFormat.DepthComponent16: - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, info.RenderBufferID); - break; - case RenderbufferInternalFormat.Rgb565: - case RenderbufferInternalFormat.Rgb5A1: - case RenderbufferInternalFormat.Rgba4: - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, RenderbufferTarget.Renderbuffer, info.RenderBufferID); - break; - case RenderbufferInternalFormat.StencilIndex8: - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, info.RenderBufferID); - break; - } - - GLWrapper.BindFrameBuffer(lastFrameBuffer); - } - - info.FrameBufferID = frameBuffer; - } - - /// - /// Unbinds the renderbuffer. - /// The renderbuffer will remain internally attached to the framebuffer. - /// - internal void Unbind() - { - if (info == null) - return; - - // Return the renderbuffer to the cache - render_buffer_cache[Format].Push(info); - - info = null; - } - - private class RenderBufferInfo - { - public int RenderBufferID; - public int FrameBufferID; - public Vector2 Size; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using OpenTK; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + public class RenderBuffer : IDisposable + { + private static readonly Dictionary> render_buffer_cache = new Dictionary>(); + + public Vector2 Size = Vector2.One; + public RenderbufferInternalFormat Format { get; } + + private RenderBufferInfo info; + private bool isDisposed; + + public RenderBuffer(RenderbufferInternalFormat format) + { + Format = format; + } + + #region Disposal + + ~RenderBuffer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + + Unbind(); + } + + #endregion + + /// + /// Binds the renderbuffer to the specfied framebuffer. + /// + /// The framebuffer this renderbuffer should be bound to. + internal void Bind(int frameBuffer) + { + // Check if we're already bound + if (info != null) + return; + + if (!render_buffer_cache.ContainsKey(Format)) + render_buffer_cache[Format] = new Stack(); + + // Make sure we have renderbuffers available + if (render_buffer_cache[Format].Count == 0) + render_buffer_cache[Format].Push(new RenderBufferInfo + { + RenderBufferID = GL.GenRenderbuffer(), + FrameBufferID = -1 + }); + + // Get a renderbuffer from the cache + info = render_buffer_cache[Format].Pop(); + + // Check if we need to update the size + if (info.Size != Size) + { + GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, info.RenderBufferID); + GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, Format, (int)Math.Ceiling(Size.X), (int)Math.Ceiling(Size.Y)); + + info.Size = Size; + } + + // For performance reasons, we only need to re-bind the renderbuffer to + // the framebuffer if it is not already attached to it + if (info.FrameBufferID != frameBuffer) + { + // Make sure the framebuffer we want to attach to is bound + int lastFrameBuffer = GLWrapper.BindFrameBuffer(frameBuffer); + + switch (Format) + { + case RenderbufferInternalFormat.DepthComponent16: + GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, info.RenderBufferID); + break; + case RenderbufferInternalFormat.Rgb565: + case RenderbufferInternalFormat.Rgb5A1: + case RenderbufferInternalFormat.Rgba4: + GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, RenderbufferTarget.Renderbuffer, info.RenderBufferID); + break; + case RenderbufferInternalFormat.StencilIndex8: + GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, info.RenderBufferID); + break; + } + + GLWrapper.BindFrameBuffer(lastFrameBuffer); + } + + info.FrameBufferID = frameBuffer; + } + + /// + /// Unbinds the renderbuffer. + /// The renderbuffer will remain internally attached to the framebuffer. + /// + internal void Unbind() + { + if (info == null) + return; + + // Return the renderbuffer to the cache + render_buffer_cache[Format].Push(info); + + info = null; + } + + private class RenderBufferInfo + { + public int RenderBufferID; + public int FrameBufferID; + public Vector2 Size; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Buffers/VertexBuffer.cs b/osu.Framework/Graphics/OpenGL/Buffers/VertexBuffer.cs index 788348f5a..256d4630c 100644 --- a/osu.Framework/Graphics/OpenGL/Buffers/VertexBuffer.cs +++ b/osu.Framework/Graphics/OpenGL/Buffers/VertexBuffer.cs @@ -1,129 +1,129 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Vertices; -using OpenTK.Graphics.ES30; -using osu.Framework.Statistics; - -namespace osu.Framework.Graphics.OpenGL.Buffers -{ - public abstract class VertexBuffer : IDisposable - where T : struct, IEquatable, IVertex - { - public T[] Vertices; - - protected static readonly int STRIDE = VertexUtils.STRIDE; - - private readonly int vboId; - private readonly BufferUsageHint usage; - - protected VertexBuffer(int amountVertices, BufferUsageHint usage) - { - this.usage = usage; - GL.GenBuffers(1, out vboId); - - resize(amountVertices); - } - - ~VertexBuffer() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected bool IsDisposed; - - protected virtual void Dispose(bool disposing) - { - if (IsDisposed) - return; - - Unbind(); - - GLWrapper.DeleteBuffer(vboId); - - IsDisposed = true; - } - - private void resize(int amountVertices) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not resize disposed vertex buffers."); - - T[] oldVertices = Vertices; - Vertices = new T[amountVertices]; - - if (oldVertices != null) - for (int i = 0; i < oldVertices.Length && i < Vertices.Length; ++i) - Vertices[i] = oldVertices[i]; - - if (GLWrapper.BindBuffer(BufferTarget.ArrayBuffer, vboId)) - VertexUtils.Bind(); - - GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vertices.Length * STRIDE), IntPtr.Zero, usage); - } - - public virtual void Bind(bool forRendering) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not bind disposed vertex buffers."); - - if (GLWrapper.BindBuffer(BufferTarget.ArrayBuffer, vboId)) - VertexUtils.Bind(); - } - - public virtual void Unbind() - { - } - - protected virtual int ToElements(int vertices) - { - return vertices; - } - - protected virtual int ToElementIndex(int vertexIndex) - { - return vertexIndex; - } - - protected abstract PrimitiveType Type { get; } - - public void Draw() - { - DrawRange(0, Vertices.Length); - } - - public void DrawRange(int startIndex, int endIndex) - { - Bind(true); - - int amountVertices = endIndex - startIndex; - GL.DrawElements(Type, ToElements(amountVertices), DrawElementsType.UnsignedShort, (IntPtr)(ToElementIndex(startIndex) * sizeof(ushort))); - - Unbind(); - } - - public void Update() - { - UpdateRange(0, Vertices.Length); - } - - public void UpdateRange(int startIndex, int endIndex) - { - Bind(false); - - int amountVertices = endIndex - startIndex; - GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)(startIndex * STRIDE), (IntPtr)(amountVertices * STRIDE), ref Vertices[startIndex]); - - Unbind(); - - FrameStatistics.Add(StatisticsCounterType.VerticesUpl, amountVertices); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Vertices; +using OpenTK.Graphics.ES30; +using osu.Framework.Statistics; + +namespace osu.Framework.Graphics.OpenGL.Buffers +{ + public abstract class VertexBuffer : IDisposable + where T : struct, IEquatable, IVertex + { + public T[] Vertices; + + protected static readonly int STRIDE = VertexUtils.STRIDE; + + private readonly int vboId; + private readonly BufferUsageHint usage; + + protected VertexBuffer(int amountVertices, BufferUsageHint usage) + { + this.usage = usage; + GL.GenBuffers(1, out vboId); + + resize(amountVertices); + } + + ~VertexBuffer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected bool IsDisposed; + + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) + return; + + Unbind(); + + GLWrapper.DeleteBuffer(vboId); + + IsDisposed = true; + } + + private void resize(int amountVertices) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not resize disposed vertex buffers."); + + T[] oldVertices = Vertices; + Vertices = new T[amountVertices]; + + if (oldVertices != null) + for (int i = 0; i < oldVertices.Length && i < Vertices.Length; ++i) + Vertices[i] = oldVertices[i]; + + if (GLWrapper.BindBuffer(BufferTarget.ArrayBuffer, vboId)) + VertexUtils.Bind(); + + GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vertices.Length * STRIDE), IntPtr.Zero, usage); + } + + public virtual void Bind(bool forRendering) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not bind disposed vertex buffers."); + + if (GLWrapper.BindBuffer(BufferTarget.ArrayBuffer, vboId)) + VertexUtils.Bind(); + } + + public virtual void Unbind() + { + } + + protected virtual int ToElements(int vertices) + { + return vertices; + } + + protected virtual int ToElementIndex(int vertexIndex) + { + return vertexIndex; + } + + protected abstract PrimitiveType Type { get; } + + public void Draw() + { + DrawRange(0, Vertices.Length); + } + + public void DrawRange(int startIndex, int endIndex) + { + Bind(true); + + int amountVertices = endIndex - startIndex; + GL.DrawElements(Type, ToElements(amountVertices), DrawElementsType.UnsignedShort, (IntPtr)(ToElementIndex(startIndex) * sizeof(ushort))); + + Unbind(); + } + + public void Update() + { + UpdateRange(0, Vertices.Length); + } + + public void UpdateRange(int startIndex, int endIndex) + { + Bind(false); + + int amountVertices = endIndex - startIndex; + GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)(startIndex * STRIDE), (IntPtr)(amountVertices * STRIDE), ref Vertices[startIndex]); + + Unbind(); + + FrameStatistics.Add(StatisticsCounterType.VerticesUpl, amountVertices); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/GLWrapper.cs b/osu.Framework/Graphics/OpenGL/GLWrapper.cs index 6d4d287f0..62f5d7b80 100644 --- a/osu.Framework/Graphics/OpenGL/GLWrapper.cs +++ b/osu.Framework/Graphics/OpenGL/GLWrapper.cs @@ -1,689 +1,689 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using osu.Framework.Development; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; -using osu.Framework.Statistics; -using osu.Framework.MathUtils; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Colour; -using osu.Framework.Platform; - -namespace osu.Framework.Graphics.OpenGL -{ - internal static class GLWrapper - { - public static MaskingInfo CurrentMaskingInfo { get; private set; } - public static RectangleI Viewport { get; private set; } - public static RectangleF Ortho { get; private set; } - public static Matrix4 ProjectionMatrix { get; private set; } - - public static bool UsingBackbuffer => lastFrameBuffer == 0; - - /// - /// Check whether we have an initialised and non-disposed GL context. - /// - public static bool HasContext => GraphicsContext.CurrentContext != null; - - public static int MaxTextureSize { get; private set; } - - private static readonly Scheduler reset_scheduler = new Scheduler(null); //force no thread set until we are actually on the draw thread. - - /// - /// A queue from which a maximum of one operation is invoked per draw frame. - /// - private static readonly ConcurrentQueue expensive_operations_queue = new ConcurrentQueue(); - - public static bool IsInitialized { get; private set; } - - private static WeakReference host; - - internal static void Initialize(GameHost host) - { - if (IsInitialized) return; - - GLWrapper.host = new WeakReference(host); - reset_scheduler.SetCurrentThread(); - - MaxTextureSize = Math.Min(4096, GL.GetInteger(GetPName.MaxTextureSize)); - - GL.Disable(EnableCap.DepthTest); - GL.Disable(EnableCap.StencilTest); - GL.Enable(EnableCap.Blend); - GL.Enable(EnableCap.ScissorTest); - - IsInitialized = true; - } - - internal static void ScheduleDisposal(Action disposalAction) - { - if (host != null && host.TryGetTarget(out GameHost h)) - h.UpdateThread.Scheduler.Add(() => reset_scheduler.Add(disposalAction.Invoke)); - } - - internal static void Reset(Vector2 size) - { - Trace.Assert(shader_stack.Count == 0); - - reset_scheduler.Update(); - - if (expensive_operations_queue.TryDequeue(out Action action)) - action.Invoke(); - - lastBoundTexture = null; - - lastDepthTest = null; - - lastBlendingInfo = new BlendingInfo(); - lastBlendingEnabledState = null; - - foreach (IVertexBatch b in this_frame_batches) - b.ResetCounters(); - - this_frame_batches.Clear(); - if (lastActiveBatch != null) - this_frame_batches.Add(lastActiveBatch); - - lastFrameBuffer = 0; - - viewport_stack.Clear(); - ortho_stack.Clear(); - masking_stack.Clear(); - scissor_rect_stack.Clear(); - - scissor_rect_stack.Push(new RectangleI(0, 0, (int)size.X, (int)size.Y)); - - Viewport = RectangleI.Empty; - Ortho = RectangleF.Empty; - - PushViewport(new RectangleI(0, 0, (int)size.X, (int)size.Y)); - PushMaskingInfo(new MaskingInfo - { - ScreenSpaceAABB = new RectangleI(0, 0, (int)size.X, (int)size.Y), - MaskingRect = new RectangleF(0, 0, size.X, size.Y), - ToMaskingSpace = Matrix3.Identity, - BlendRange = 1, - AlphaExponent = 1, - }, true); - } - - // We initialize to an invalid value such that we are not missing an initial GL.ClearColor call. - private static Color4 clearColour = new Color4(-1, -1, -1, -1); - - public static void ClearColour(Color4 c) - { - if (clearColour != c) - { - clearColour = c; - GL.ClearColor(clearColour); - } - - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); - } - - /// - /// Enqueues a texture to be uploaded in the next frame. - /// - /// The texture to be uploaded. - public static void EnqueueTextureUpload(TextureGL texture) => expensive_operations_queue.Enqueue(() => texture.Upload()); - - /// - /// Enqueues the compile of a shader. - /// - /// The shader to compile. - public static void EnqueueShaderCompile(Shader shader) => expensive_operations_queue.Enqueue(shader.EnsureLoaded); - - private static readonly int[] last_bound_buffers = new int[2]; - - /// - /// Bind an OpenGL buffer object. - /// - /// The buffer type to bind. - /// The buffer ID to bind. - /// Whether an actual bind call was necessary. This value is false when repeatedly binding the same buffer. - public static bool BindBuffer(BufferTarget target, int buffer) - { - int bufferIndex = target - BufferTarget.ArrayBuffer; - if (last_bound_buffers[bufferIndex] == buffer) - return false; - - last_bound_buffers[bufferIndex] = buffer; - GL.BindBuffer(target, buffer); - - FrameStatistics.Increment(StatisticsCounterType.VBufBinds); - - return true; - } - - private static IVertexBatch lastActiveBatch; - - private static readonly List this_frame_batches = new List(); - - /// - /// Sets the last vertex batch used for drawing. - /// - /// This is done so that various methods that change GL state can force-draw the batch - /// before continuing with the state change. - /// - /// - /// The batch. - internal static void SetActiveBatch(IVertexBatch batch) - { - if (lastActiveBatch == batch) return; - - FlushCurrentBatch(); - - if (batch != null && !this_frame_batches.Contains(batch)) - this_frame_batches.Add(batch); - - lastActiveBatch = batch; - } - - private static TextureGL lastBoundTexture; - - internal static bool AtlasTextureIsBound => lastBoundTexture is TextureGLAtlas; - - /// - /// Binds a texture to darw with. - /// - /// - public static void BindTexture(TextureGL texture) - { - if (lastBoundTexture != texture) - { - FlushCurrentBatch(); - - GL.BindTexture(TextureTarget.Texture2D, texture?.TextureId ?? 0); - lastBoundTexture = texture; - - FrameStatistics.Increment(StatisticsCounterType.TextureBinds); - } - } - - private static bool? lastDepthTest; - - public static void SetDepthTest(bool enabled) - { - if (lastDepthTest == enabled) - return; - - lastDepthTest = enabled; - - FlushCurrentBatch(); - - if (enabled) - GL.Enable(EnableCap.DepthTest); - else - GL.Disable(EnableCap.DepthTest); - } - - private static BlendingInfo lastBlendingInfo; - private static bool? lastBlendingEnabledState; - - /// - /// Sets the blending function to draw with. - /// - /// The infor we should use to update the active state. - public static void SetBlend(BlendingInfo blendingInfo) - { - if (lastBlendingInfo.Equals(blendingInfo)) - return; - - FlushCurrentBatch(); - - if (blendingInfo.IsDisabled) - { - if (!lastBlendingEnabledState.HasValue || lastBlendingEnabledState.Value) - GL.Disable(EnableCap.Blend); - - lastBlendingEnabledState = false; - } - else - { - if (!lastBlendingEnabledState.HasValue || !lastBlendingEnabledState.Value) - GL.Enable(EnableCap.Blend); - - lastBlendingEnabledState = true; - - GL.BlendEquationSeparate(blendingInfo.RGBEquation, blendingInfo.AlphaEquation); - GL.BlendFuncSeparate(blendingInfo.Source, blendingInfo.Destination, blendingInfo.SourceAlpha, blendingInfo.DestinationAlpha); - } - - lastBlendingInfo = blendingInfo; - } - - private static int lastFrameBuffer; - - /// - /// Binds a framebuffer. - /// - /// The framebuffer to bind. - /// The last bound framebuffer. - public static int BindFrameBuffer(int frameBuffer) - { - if (lastFrameBuffer == frameBuffer) - return lastFrameBuffer; - - FlushCurrentBatch(); - - int last = lastFrameBuffer; - - GL.BindFramebuffer(FramebufferTarget.Framebuffer, frameBuffer); - lastFrameBuffer = frameBuffer; - - return last; - } - - private static readonly Stack viewport_stack = new Stack(); - - /// - /// Applies a new viewport rectangle. - /// - /// The viewport rectangle. - public static void PushViewport(RectangleI viewport) - { - var actualRect = viewport; - - if (actualRect.Width < 0) - { - actualRect.X += viewport.Width; - actualRect.Width = -viewport.Width; - } - - if (actualRect.Height < 0) - { - actualRect.Y += viewport.Height; - actualRect.Height = -viewport.Height; - } - - PushOrtho(viewport); - - viewport_stack.Push(actualRect); - - if (Viewport == actualRect) - return; - Viewport = actualRect; - - GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); - - UpdateScissorToCurrentViewportAndOrtho(); - } - - /// - /// Applies the last viewport rectangle. - /// - public static void PopViewport() - { - Trace.Assert(viewport_stack.Count > 1); - - PopOrtho(); - - viewport_stack.Pop(); - RectangleI actualRect = viewport_stack.Peek(); - - if (Viewport == actualRect) - return; - Viewport = actualRect; - - GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); - - UpdateScissorToCurrentViewportAndOrtho(); - } - - private static readonly Stack ortho_stack = new Stack(); - - /// - /// Applies a new orthographic projection rectangle. - /// - /// The orthographic projection rectangle. - public static void PushOrtho(RectangleF ortho) - { - FlushCurrentBatch(); - - ortho_stack.Push(ortho); - if (Ortho == ortho) - return; - Ortho = ortho; - - ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); - Shader.SetGlobalProperty(@"g_ProjMatrix", ProjectionMatrix); - - UpdateScissorToCurrentViewportAndOrtho(); - } - - /// - /// Applies the last orthographic projection rectangle. - /// - public static void PopOrtho() - { - Trace.Assert(ortho_stack.Count > 1); - - FlushCurrentBatch(); - - ortho_stack.Pop(); - RectangleF actualRect = ortho_stack.Peek(); - - if (Ortho == actualRect) - return; - Ortho = actualRect; - - ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); - Shader.SetGlobalProperty(@"g_ProjMatrix", ProjectionMatrix); - - UpdateScissorToCurrentViewportAndOrtho(); - } - - private static readonly Stack masking_stack = new Stack(); - private static readonly Stack scissor_rect_stack = new Stack(); - - public static void UpdateScissorToCurrentViewportAndOrtho() - { - RectangleF viewportRect = Viewport; - Vector2 offset = viewportRect.TopLeft - Ortho.TopLeft; - - RectangleI currentScissorRect = scissor_rect_stack.Peek(); - - RectangleI scissorRect = new RectangleI( - currentScissorRect.X + (int)Math.Floor(offset.X), - Viewport.Height - currentScissorRect.Bottom - (int)Math.Ceiling(offset.Y), - currentScissorRect.Width, - currentScissorRect.Height); - - if (!Precision.AlmostEquals(offset, Vector2.Zero)) - { - ++scissorRect.Width; - ++scissorRect.Height; - } - - GL.Scissor(scissorRect.X, scissorRect.Y, scissorRect.Width, scissorRect.Height); - } - - private static void setMaskingInfo(MaskingInfo maskingInfo, bool isPushing, bool overwritePreviousScissor) - { - FlushCurrentBatch(); - - Shader.SetGlobalProperty(@"g_MaskingRect", new Vector4( - maskingInfo.MaskingRect.Left, - maskingInfo.MaskingRect.Top, - maskingInfo.MaskingRect.Right, - maskingInfo.MaskingRect.Bottom)); - - Shader.SetGlobalProperty(@"g_ToMaskingSpace", maskingInfo.ToMaskingSpace); - Shader.SetGlobalProperty(@"g_CornerRadius", maskingInfo.CornerRadius); - - Shader.SetGlobalProperty(@"g_BorderThickness", maskingInfo.BorderThickness / maskingInfo.BlendRange); - Shader.SetGlobalProperty(@"g_BorderColour", new Vector4( - maskingInfo.BorderColour.Linear.R, - maskingInfo.BorderColour.Linear.G, - maskingInfo.BorderColour.Linear.B, - maskingInfo.BorderColour.Linear.A)); - - Shader.SetGlobalProperty(@"g_MaskingBlendRange", maskingInfo.BlendRange); - Shader.SetGlobalProperty(@"g_AlphaExponent", maskingInfo.AlphaExponent); - - Shader.SetGlobalProperty(@"g_DiscardInner", maskingInfo.Hollow); - - RectangleI actualRect = maskingInfo.ScreenSpaceAABB; - actualRect.X += Viewport.X; - actualRect.Y += Viewport.Y; - - // Ensure the rectangle only has positive width and height. (Required by OGL) - if (actualRect.Width < 0) - { - actualRect.X += actualRect.Width; - actualRect.Width = -actualRect.Width; - } - - if (actualRect.Height < 0) - { - actualRect.Y += actualRect.Height; - actualRect.Height = -actualRect.Height; - } - - if (isPushing) - { - RectangleI currentScissorRect; - if (overwritePreviousScissor) - currentScissorRect = actualRect; - else - { - currentScissorRect = scissor_rect_stack.Peek(); - currentScissorRect.Intersect(actualRect); - } - - scissor_rect_stack.Push(currentScissorRect); - } - else - { - Trace.Assert(scissor_rect_stack.Count > 1); - scissor_rect_stack.Pop(); - } - - UpdateScissorToCurrentViewportAndOrtho(); - } - - internal static void FlushCurrentBatch() - { - lastActiveBatch?.Draw(); - } - - public static bool IsMaskingActive => masking_stack.Count > 1; - - /// - /// Applies a new scissor rectangle. - /// - /// The masking info. - /// Whether or not to shrink an existing scissor rectangle. - public static void PushMaskingInfo(MaskingInfo maskingInfo, bool overwritePreviousScissor = false) - { - masking_stack.Push(maskingInfo); - if (CurrentMaskingInfo.Equals(maskingInfo)) - return; - - CurrentMaskingInfo = maskingInfo; - setMaskingInfo(CurrentMaskingInfo, true, overwritePreviousScissor); - } - - /// - /// Applies the last scissor rectangle. - /// - public static void PopMaskingInfo() - { - Trace.Assert(masking_stack.Count > 1); - - masking_stack.Pop(); - MaskingInfo maskingInfo = masking_stack.Peek(); - - if (CurrentMaskingInfo.Equals(maskingInfo)) - return; - - CurrentMaskingInfo = maskingInfo; - setMaskingInfo(CurrentMaskingInfo, false, true); - } - - /// - /// Deletes a framebuffer. - /// - /// The framebuffer to delete. - internal static void DeleteFramebuffer(int frameBuffer) - { - if (frameBuffer == -1) return; - - //todo: don't use scheduler - ScheduleDisposal(() => { GL.DeleteFramebuffer(frameBuffer); }); - } - - /// - /// Deletes a buffer object. - /// - /// The buffer object to delete. - internal static void DeleteBuffer(int vboId) - { - //todo: don't use scheduler - ScheduleDisposal(() => { GL.DeleteBuffer(vboId); }); - } - - /// - /// Deletes textures. - /// - /// An array of textures to delete. - internal static void DeleteTextures(params int[] ids) - { - //todo: don't use scheduler - ScheduleDisposal(() => { GL.DeleteTextures(ids.Length, ids); }); - } - - /// - /// Deletes a shader program. - /// - /// The shader program to delete. - internal static void DeleteProgram(Shader shader) - { - //todo: don't use scheduler - ScheduleDisposal(() => { GL.DeleteProgram(shader); }); - } - - /// - /// Deletes a shader part. - /// - /// The shader part to delete. - internal static void DeleteShader(ShaderPart shaderPart) - { - //todo: don't use scheduler - ScheduleDisposal(() => { GL.DeleteShader(shaderPart); }); - } - - private static int currentShader; - - private static readonly Stack shader_stack = new Stack(); - - public static void UseProgram(int? shader) - { - ThreadSafety.EnsureDrawThread(); - - if (shader != null) - { - shader_stack.Push(shader.Value); - } - else - { - shader_stack.Pop(); - - //check if the stack is empty, and if so don't restore the previous shader. - if (shader_stack.Count == 0) - return; - } - - int s = shader ?? shader_stack.Peek(); - - if (currentShader == s) return; - - FlushCurrentBatch(); - - GL.UseProgram(s); - currentShader = s; - } - - public static void SetUniform(int shader, ActiveUniformType type, int location, object value) - { - if (shader == currentShader) - FlushCurrentBatch(); - - switch (type) - { - case ActiveUniformType.Bool: - GL.Uniform1(location, (bool)value ? 1 : 0); - break; - case ActiveUniformType.Int: - GL.Uniform1(location, (int)value); - break; - case ActiveUniformType.Float: - GL.Uniform1(location, (float)value); - break; - case ActiveUniformType.BoolVec2: - case ActiveUniformType.IntVec2: - case ActiveUniformType.FloatVec2: - GL.Uniform2(location, (Vector2)value); - break; - case ActiveUniformType.FloatMat2: - { - Matrix2 mat = (Matrix2)value; - GL.UniformMatrix2(location, false, ref mat); - break; - } - case ActiveUniformType.BoolVec3: - case ActiveUniformType.IntVec3: - case ActiveUniformType.FloatVec3: - GL.Uniform3(location, (Vector3)value); - break; - case ActiveUniformType.FloatMat3: - { - Matrix3 mat = (Matrix3)value; - GL.UniformMatrix3(location, false, ref mat); - break; - } - case ActiveUniformType.BoolVec4: - case ActiveUniformType.IntVec4: - case ActiveUniformType.FloatVec4: - GL.Uniform4(location, (Vector4)value); - break; - case ActiveUniformType.FloatMat4: - { - Matrix4 mat = (Matrix4)value; - GL.UniformMatrix4(location, false, ref mat); - break; - } - case ActiveUniformType.Sampler2D: - GL.Uniform1(location, (int)value); - break; - } - } - } - - public struct MaskingInfo : IEquatable - { - public RectangleI ScreenSpaceAABB; - public RectangleF MaskingRect; - - /// - /// This matrix transforms screen space coordinates to masking space (likely the parent - /// space of the container doing the masking). - /// It is used by a shader to determine which pixels to discard. - /// - public Matrix3 ToMaskingSpace; - - public float CornerRadius; - - public float BorderThickness; - public SRGBColour BorderColour; - - public float BlendRange; - public float AlphaExponent; - - public bool Hollow; - - public bool Equals(MaskingInfo other) - { - return - ScreenSpaceAABB == other.ScreenSpaceAABB && - MaskingRect == other.MaskingRect && - ToMaskingSpace == other.ToMaskingSpace && - CornerRadius == other.CornerRadius && - BorderThickness == other.BorderThickness && - BorderColour.Equals(other.BorderColour) && - BlendRange == other.BlendRange && - AlphaExponent == other.AlphaExponent && - Hollow == other.Hollow; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Development; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; +using osu.Framework.Statistics; +using osu.Framework.MathUtils; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Colour; +using osu.Framework.Platform; + +namespace osu.Framework.Graphics.OpenGL +{ + internal static class GLWrapper + { + public static MaskingInfo CurrentMaskingInfo { get; private set; } + public static RectangleI Viewport { get; private set; } + public static RectangleF Ortho { get; private set; } + public static Matrix4 ProjectionMatrix { get; private set; } + + public static bool UsingBackbuffer => lastFrameBuffer == 0; + + /// + /// Check whether we have an initialised and non-disposed GL context. + /// + public static bool HasContext => GraphicsContext.CurrentContext != null; + + public static int MaxTextureSize { get; private set; } + + private static readonly Scheduler reset_scheduler = new Scheduler(null); //force no thread set until we are actually on the draw thread. + + /// + /// A queue from which a maximum of one operation is invoked per draw frame. + /// + private static readonly ConcurrentQueue expensive_operations_queue = new ConcurrentQueue(); + + public static bool IsInitialized { get; private set; } + + private static WeakReference host; + + internal static void Initialize(GameHost host) + { + if (IsInitialized) return; + + GLWrapper.host = new WeakReference(host); + reset_scheduler.SetCurrentThread(); + + MaxTextureSize = Math.Min(4096, GL.GetInteger(GetPName.MaxTextureSize)); + + GL.Disable(EnableCap.DepthTest); + GL.Disable(EnableCap.StencilTest); + GL.Enable(EnableCap.Blend); + GL.Enable(EnableCap.ScissorTest); + + IsInitialized = true; + } + + internal static void ScheduleDisposal(Action disposalAction) + { + if (host != null && host.TryGetTarget(out GameHost h)) + h.UpdateThread.Scheduler.Add(() => reset_scheduler.Add(disposalAction.Invoke)); + } + + internal static void Reset(Vector2 size) + { + Trace.Assert(shader_stack.Count == 0); + + reset_scheduler.Update(); + + if (expensive_operations_queue.TryDequeue(out Action action)) + action.Invoke(); + + lastBoundTexture = null; + + lastDepthTest = null; + + lastBlendingInfo = new BlendingInfo(); + lastBlendingEnabledState = null; + + foreach (IVertexBatch b in this_frame_batches) + b.ResetCounters(); + + this_frame_batches.Clear(); + if (lastActiveBatch != null) + this_frame_batches.Add(lastActiveBatch); + + lastFrameBuffer = 0; + + viewport_stack.Clear(); + ortho_stack.Clear(); + masking_stack.Clear(); + scissor_rect_stack.Clear(); + + scissor_rect_stack.Push(new RectangleI(0, 0, (int)size.X, (int)size.Y)); + + Viewport = RectangleI.Empty; + Ortho = RectangleF.Empty; + + PushViewport(new RectangleI(0, 0, (int)size.X, (int)size.Y)); + PushMaskingInfo(new MaskingInfo + { + ScreenSpaceAABB = new RectangleI(0, 0, (int)size.X, (int)size.Y), + MaskingRect = new RectangleF(0, 0, size.X, size.Y), + ToMaskingSpace = Matrix3.Identity, + BlendRange = 1, + AlphaExponent = 1, + }, true); + } + + // We initialize to an invalid value such that we are not missing an initial GL.ClearColor call. + private static Color4 clearColour = new Color4(-1, -1, -1, -1); + + public static void ClearColour(Color4 c) + { + if (clearColour != c) + { + clearColour = c; + GL.ClearColor(clearColour); + } + + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); + } + + /// + /// Enqueues a texture to be uploaded in the next frame. + /// + /// The texture to be uploaded. + public static void EnqueueTextureUpload(TextureGL texture) => expensive_operations_queue.Enqueue(() => texture.Upload()); + + /// + /// Enqueues the compile of a shader. + /// + /// The shader to compile. + public static void EnqueueShaderCompile(Shader shader) => expensive_operations_queue.Enqueue(shader.EnsureLoaded); + + private static readonly int[] last_bound_buffers = new int[2]; + + /// + /// Bind an OpenGL buffer object. + /// + /// The buffer type to bind. + /// The buffer ID to bind. + /// Whether an actual bind call was necessary. This value is false when repeatedly binding the same buffer. + public static bool BindBuffer(BufferTarget target, int buffer) + { + int bufferIndex = target - BufferTarget.ArrayBuffer; + if (last_bound_buffers[bufferIndex] == buffer) + return false; + + last_bound_buffers[bufferIndex] = buffer; + GL.BindBuffer(target, buffer); + + FrameStatistics.Increment(StatisticsCounterType.VBufBinds); + + return true; + } + + private static IVertexBatch lastActiveBatch; + + private static readonly List this_frame_batches = new List(); + + /// + /// Sets the last vertex batch used for drawing. + /// + /// This is done so that various methods that change GL state can force-draw the batch + /// before continuing with the state change. + /// + /// + /// The batch. + internal static void SetActiveBatch(IVertexBatch batch) + { + if (lastActiveBatch == batch) return; + + FlushCurrentBatch(); + + if (batch != null && !this_frame_batches.Contains(batch)) + this_frame_batches.Add(batch); + + lastActiveBatch = batch; + } + + private static TextureGL lastBoundTexture; + + internal static bool AtlasTextureIsBound => lastBoundTexture is TextureGLAtlas; + + /// + /// Binds a texture to darw with. + /// + /// + public static void BindTexture(TextureGL texture) + { + if (lastBoundTexture != texture) + { + FlushCurrentBatch(); + + GL.BindTexture(TextureTarget.Texture2D, texture?.TextureId ?? 0); + lastBoundTexture = texture; + + FrameStatistics.Increment(StatisticsCounterType.TextureBinds); + } + } + + private static bool? lastDepthTest; + + public static void SetDepthTest(bool enabled) + { + if (lastDepthTest == enabled) + return; + + lastDepthTest = enabled; + + FlushCurrentBatch(); + + if (enabled) + GL.Enable(EnableCap.DepthTest); + else + GL.Disable(EnableCap.DepthTest); + } + + private static BlendingInfo lastBlendingInfo; + private static bool? lastBlendingEnabledState; + + /// + /// Sets the blending function to draw with. + /// + /// The infor we should use to update the active state. + public static void SetBlend(BlendingInfo blendingInfo) + { + if (lastBlendingInfo.Equals(blendingInfo)) + return; + + FlushCurrentBatch(); + + if (blendingInfo.IsDisabled) + { + if (!lastBlendingEnabledState.HasValue || lastBlendingEnabledState.Value) + GL.Disable(EnableCap.Blend); + + lastBlendingEnabledState = false; + } + else + { + if (!lastBlendingEnabledState.HasValue || !lastBlendingEnabledState.Value) + GL.Enable(EnableCap.Blend); + + lastBlendingEnabledState = true; + + GL.BlendEquationSeparate(blendingInfo.RGBEquation, blendingInfo.AlphaEquation); + GL.BlendFuncSeparate(blendingInfo.Source, blendingInfo.Destination, blendingInfo.SourceAlpha, blendingInfo.DestinationAlpha); + } + + lastBlendingInfo = blendingInfo; + } + + private static int lastFrameBuffer; + + /// + /// Binds a framebuffer. + /// + /// The framebuffer to bind. + /// The last bound framebuffer. + public static int BindFrameBuffer(int frameBuffer) + { + if (lastFrameBuffer == frameBuffer) + return lastFrameBuffer; + + FlushCurrentBatch(); + + int last = lastFrameBuffer; + + GL.BindFramebuffer(FramebufferTarget.Framebuffer, frameBuffer); + lastFrameBuffer = frameBuffer; + + return last; + } + + private static readonly Stack viewport_stack = new Stack(); + + /// + /// Applies a new viewport rectangle. + /// + /// The viewport rectangle. + public static void PushViewport(RectangleI viewport) + { + var actualRect = viewport; + + if (actualRect.Width < 0) + { + actualRect.X += viewport.Width; + actualRect.Width = -viewport.Width; + } + + if (actualRect.Height < 0) + { + actualRect.Y += viewport.Height; + actualRect.Height = -viewport.Height; + } + + PushOrtho(viewport); + + viewport_stack.Push(actualRect); + + if (Viewport == actualRect) + return; + Viewport = actualRect; + + GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); + + UpdateScissorToCurrentViewportAndOrtho(); + } + + /// + /// Applies the last viewport rectangle. + /// + public static void PopViewport() + { + Trace.Assert(viewport_stack.Count > 1); + + PopOrtho(); + + viewport_stack.Pop(); + RectangleI actualRect = viewport_stack.Peek(); + + if (Viewport == actualRect) + return; + Viewport = actualRect; + + GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); + + UpdateScissorToCurrentViewportAndOrtho(); + } + + private static readonly Stack ortho_stack = new Stack(); + + /// + /// Applies a new orthographic projection rectangle. + /// + /// The orthographic projection rectangle. + public static void PushOrtho(RectangleF ortho) + { + FlushCurrentBatch(); + + ortho_stack.Push(ortho); + if (Ortho == ortho) + return; + Ortho = ortho; + + ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); + Shader.SetGlobalProperty(@"g_ProjMatrix", ProjectionMatrix); + + UpdateScissorToCurrentViewportAndOrtho(); + } + + /// + /// Applies the last orthographic projection rectangle. + /// + public static void PopOrtho() + { + Trace.Assert(ortho_stack.Count > 1); + + FlushCurrentBatch(); + + ortho_stack.Pop(); + RectangleF actualRect = ortho_stack.Peek(); + + if (Ortho == actualRect) + return; + Ortho = actualRect; + + ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); + Shader.SetGlobalProperty(@"g_ProjMatrix", ProjectionMatrix); + + UpdateScissorToCurrentViewportAndOrtho(); + } + + private static readonly Stack masking_stack = new Stack(); + private static readonly Stack scissor_rect_stack = new Stack(); + + public static void UpdateScissorToCurrentViewportAndOrtho() + { + RectangleF viewportRect = Viewport; + Vector2 offset = viewportRect.TopLeft - Ortho.TopLeft; + + RectangleI currentScissorRect = scissor_rect_stack.Peek(); + + RectangleI scissorRect = new RectangleI( + currentScissorRect.X + (int)Math.Floor(offset.X), + Viewport.Height - currentScissorRect.Bottom - (int)Math.Ceiling(offset.Y), + currentScissorRect.Width, + currentScissorRect.Height); + + if (!Precision.AlmostEquals(offset, Vector2.Zero)) + { + ++scissorRect.Width; + ++scissorRect.Height; + } + + GL.Scissor(scissorRect.X, scissorRect.Y, scissorRect.Width, scissorRect.Height); + } + + private static void setMaskingInfo(MaskingInfo maskingInfo, bool isPushing, bool overwritePreviousScissor) + { + FlushCurrentBatch(); + + Shader.SetGlobalProperty(@"g_MaskingRect", new Vector4( + maskingInfo.MaskingRect.Left, + maskingInfo.MaskingRect.Top, + maskingInfo.MaskingRect.Right, + maskingInfo.MaskingRect.Bottom)); + + Shader.SetGlobalProperty(@"g_ToMaskingSpace", maskingInfo.ToMaskingSpace); + Shader.SetGlobalProperty(@"g_CornerRadius", maskingInfo.CornerRadius); + + Shader.SetGlobalProperty(@"g_BorderThickness", maskingInfo.BorderThickness / maskingInfo.BlendRange); + Shader.SetGlobalProperty(@"g_BorderColour", new Vector4( + maskingInfo.BorderColour.Linear.R, + maskingInfo.BorderColour.Linear.G, + maskingInfo.BorderColour.Linear.B, + maskingInfo.BorderColour.Linear.A)); + + Shader.SetGlobalProperty(@"g_MaskingBlendRange", maskingInfo.BlendRange); + Shader.SetGlobalProperty(@"g_AlphaExponent", maskingInfo.AlphaExponent); + + Shader.SetGlobalProperty(@"g_DiscardInner", maskingInfo.Hollow); + + RectangleI actualRect = maskingInfo.ScreenSpaceAABB; + actualRect.X += Viewport.X; + actualRect.Y += Viewport.Y; + + // Ensure the rectangle only has positive width and height. (Required by OGL) + if (actualRect.Width < 0) + { + actualRect.X += actualRect.Width; + actualRect.Width = -actualRect.Width; + } + + if (actualRect.Height < 0) + { + actualRect.Y += actualRect.Height; + actualRect.Height = -actualRect.Height; + } + + if (isPushing) + { + RectangleI currentScissorRect; + if (overwritePreviousScissor) + currentScissorRect = actualRect; + else + { + currentScissorRect = scissor_rect_stack.Peek(); + currentScissorRect.Intersect(actualRect); + } + + scissor_rect_stack.Push(currentScissorRect); + } + else + { + Trace.Assert(scissor_rect_stack.Count > 1); + scissor_rect_stack.Pop(); + } + + UpdateScissorToCurrentViewportAndOrtho(); + } + + internal static void FlushCurrentBatch() + { + lastActiveBatch?.Draw(); + } + + public static bool IsMaskingActive => masking_stack.Count > 1; + + /// + /// Applies a new scissor rectangle. + /// + /// The masking info. + /// Whether or not to shrink an existing scissor rectangle. + public static void PushMaskingInfo(MaskingInfo maskingInfo, bool overwritePreviousScissor = false) + { + masking_stack.Push(maskingInfo); + if (CurrentMaskingInfo.Equals(maskingInfo)) + return; + + CurrentMaskingInfo = maskingInfo; + setMaskingInfo(CurrentMaskingInfo, true, overwritePreviousScissor); + } + + /// + /// Applies the last scissor rectangle. + /// + public static void PopMaskingInfo() + { + Trace.Assert(masking_stack.Count > 1); + + masking_stack.Pop(); + MaskingInfo maskingInfo = masking_stack.Peek(); + + if (CurrentMaskingInfo.Equals(maskingInfo)) + return; + + CurrentMaskingInfo = maskingInfo; + setMaskingInfo(CurrentMaskingInfo, false, true); + } + + /// + /// Deletes a framebuffer. + /// + /// The framebuffer to delete. + internal static void DeleteFramebuffer(int frameBuffer) + { + if (frameBuffer == -1) return; + + //todo: don't use scheduler + ScheduleDisposal(() => { GL.DeleteFramebuffer(frameBuffer); }); + } + + /// + /// Deletes a buffer object. + /// + /// The buffer object to delete. + internal static void DeleteBuffer(int vboId) + { + //todo: don't use scheduler + ScheduleDisposal(() => { GL.DeleteBuffer(vboId); }); + } + + /// + /// Deletes textures. + /// + /// An array of textures to delete. + internal static void DeleteTextures(params int[] ids) + { + //todo: don't use scheduler + ScheduleDisposal(() => { GL.DeleteTextures(ids.Length, ids); }); + } + + /// + /// Deletes a shader program. + /// + /// The shader program to delete. + internal static void DeleteProgram(Shader shader) + { + //todo: don't use scheduler + ScheduleDisposal(() => { GL.DeleteProgram(shader); }); + } + + /// + /// Deletes a shader part. + /// + /// The shader part to delete. + internal static void DeleteShader(ShaderPart shaderPart) + { + //todo: don't use scheduler + ScheduleDisposal(() => { GL.DeleteShader(shaderPart); }); + } + + private static int currentShader; + + private static readonly Stack shader_stack = new Stack(); + + public static void UseProgram(int? shader) + { + ThreadSafety.EnsureDrawThread(); + + if (shader != null) + { + shader_stack.Push(shader.Value); + } + else + { + shader_stack.Pop(); + + //check if the stack is empty, and if so don't restore the previous shader. + if (shader_stack.Count == 0) + return; + } + + int s = shader ?? shader_stack.Peek(); + + if (currentShader == s) return; + + FlushCurrentBatch(); + + GL.UseProgram(s); + currentShader = s; + } + + public static void SetUniform(int shader, ActiveUniformType type, int location, object value) + { + if (shader == currentShader) + FlushCurrentBatch(); + + switch (type) + { + case ActiveUniformType.Bool: + GL.Uniform1(location, (bool)value ? 1 : 0); + break; + case ActiveUniformType.Int: + GL.Uniform1(location, (int)value); + break; + case ActiveUniformType.Float: + GL.Uniform1(location, (float)value); + break; + case ActiveUniformType.BoolVec2: + case ActiveUniformType.IntVec2: + case ActiveUniformType.FloatVec2: + GL.Uniform2(location, (Vector2)value); + break; + case ActiveUniformType.FloatMat2: + { + Matrix2 mat = (Matrix2)value; + GL.UniformMatrix2(location, false, ref mat); + break; + } + case ActiveUniformType.BoolVec3: + case ActiveUniformType.IntVec3: + case ActiveUniformType.FloatVec3: + GL.Uniform3(location, (Vector3)value); + break; + case ActiveUniformType.FloatMat3: + { + Matrix3 mat = (Matrix3)value; + GL.UniformMatrix3(location, false, ref mat); + break; + } + case ActiveUniformType.BoolVec4: + case ActiveUniformType.IntVec4: + case ActiveUniformType.FloatVec4: + GL.Uniform4(location, (Vector4)value); + break; + case ActiveUniformType.FloatMat4: + { + Matrix4 mat = (Matrix4)value; + GL.UniformMatrix4(location, false, ref mat); + break; + } + case ActiveUniformType.Sampler2D: + GL.Uniform1(location, (int)value); + break; + } + } + } + + public struct MaskingInfo : IEquatable + { + public RectangleI ScreenSpaceAABB; + public RectangleF MaskingRect; + + /// + /// This matrix transforms screen space coordinates to masking space (likely the parent + /// space of the container doing the masking). + /// It is used by a shader to determine which pixels to discard. + /// + public Matrix3 ToMaskingSpace; + + public float CornerRadius; + + public float BorderThickness; + public SRGBColour BorderColour; + + public float BlendRange; + public float AlphaExponent; + + public bool Hollow; + + public bool Equals(MaskingInfo other) + { + return + ScreenSpaceAABB == other.ScreenSpaceAABB && + MaskingRect == other.MaskingRect && + ToMaskingSpace == other.ToMaskingSpace && + CornerRadius == other.CornerRadius && + BorderThickness == other.BorderThickness && + BorderColour.Equals(other.BorderColour) && + BlendRange == other.BlendRange && + AlphaExponent == other.AlphaExponent && + Hollow == other.Hollow; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureGL.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureGL.cs index 262239630..ed4e4609a 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureGL.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureGL.cs @@ -1,81 +1,81 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.Primitives; -using OpenTK.Graphics.ES30; -using OpenTK; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - public abstract class TextureGL : IDisposable - { - public bool IsTransparent; - public TextureWrapMode WrapMode = TextureWrapMode.ClampToEdge; - - #region Disposal - - ~TextureGL() - { - Dispose(false); - } - - protected bool IsDisposed; - - protected virtual void Dispose(bool isDisposing) - { - IsDisposed = true; - } - - public void Dispose() - { - if (IsDisposed) - return; - - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - public abstract TextureGL Native { get; } - - public abstract bool Loaded { get; } - - public abstract int TextureId { get; } - - public abstract int Height { get; set; } - - public abstract int Width { get; set; } - - public Vector2 Size => new Vector2(Width, Height); - - public abstract RectangleF GetTextureRect(RectangleF? textureRect); - - /// - /// Blit a triangle to OpenGL display with specified parameters. - /// - public abstract void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null); - - /// - /// Blit a quad to OpenGL display with specified parameters. - /// - public abstract void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null); - - /// - /// Bind as active texture. - /// - /// True if bind was successful. - public abstract bool Bind(); - - /// - /// Uploads pending texture data to the GPU if it exists. - /// - /// Whether pending data existed and an upload has been performed. - internal abstract bool Upload(); - - public abstract void SetData(TextureUpload upload); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.Primitives; +using OpenTK.Graphics.ES30; +using OpenTK; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + public abstract class TextureGL : IDisposable + { + public bool IsTransparent; + public TextureWrapMode WrapMode = TextureWrapMode.ClampToEdge; + + #region Disposal + + ~TextureGL() + { + Dispose(false); + } + + protected bool IsDisposed; + + protected virtual void Dispose(bool isDisposing) + { + IsDisposed = true; + } + + public void Dispose() + { + if (IsDisposed) + return; + + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + public abstract TextureGL Native { get; } + + public abstract bool Loaded { get; } + + public abstract int TextureId { get; } + + public abstract int Height { get; set; } + + public abstract int Width { get; set; } + + public Vector2 Size => new Vector2(Width, Height); + + public abstract RectangleF GetTextureRect(RectangleF? textureRect); + + /// + /// Blit a triangle to OpenGL display with specified parameters. + /// + public abstract void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null); + + /// + /// Blit a quad to OpenGL display with specified parameters. + /// + public abstract void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null); + + /// + /// Bind as active texture. + /// + /// True if bind was successful. + public abstract bool Bind(); + + /// + /// Uploads pending texture data to the GPU if it exists. + /// + /// Whether pending data existed and an upload has been performed. + internal abstract bool Upload(); + + public abstract void SetData(TextureUpload upload); + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlas.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlas.cs index 9b57917b3..7baf39a79 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlas.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlas.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - /// - /// A TextureGL which is acting as the backing for an atlas. - /// - internal class TextureGLAtlas : TextureGLSingle - { - public TextureGLAtlas(int width, int height, bool manualMipmaps, All filteringMode = All.Linear) - : base(width, height, manualMipmaps, filteringMode) - { - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + /// + /// A TextureGL which is acting as the backing for an atlas. + /// + internal class TextureGLAtlas : TextureGLSingle + { + public TextureGLAtlas(int width, int height, bool manualMipmaps, All filteringMode = All.Linear) + : base(width, height, manualMipmaps, filteringMode) + { + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlasWhite.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlasWhite.cs index 0e745423a..8b2eabd34 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlasWhite.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureGLAtlasWhite.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - /// - /// A special texture which refers to the area of a texture atlas which is white. - /// Allows use of such areas while being unaware of whether we need to bind a texture or not. - /// - internal class TextureGLAtlasWhite : TextureGLSub - { - public TextureGLAtlasWhite(TextureGLSingle parent) - : base(new RectangleI(0, 0, 1, 1), parent) - { - } - - public override bool Bind() - { - //we can use the special white space from any atlas texture. - if (GLWrapper.AtlasTextureIsBound) - return true; - - return base.Bind(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + /// + /// A special texture which refers to the area of a texture atlas which is white. + /// Allows use of such areas while being unaware of whether we need to bind a texture or not. + /// + internal class TextureGLAtlasWhite : TextureGLSub + { + public TextureGLAtlasWhite(TextureGLSingle parent) + : base(new RectangleI(0, 0, 1, 1), parent) + { + } + + public override bool Bind() + { + //we can use the special white space from any atlas texture. + if (GLWrapper.AtlasTextureIsBound) + return true; + + return base.Bind(); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureGLSingle.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureGLSingle.cs index 81433b343..161fb4d9c 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureGLSingle.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureGLSingle.cs @@ -1,429 +1,429 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Runtime.InteropServices; -using osu.Framework.Development; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Primitives; -using OpenTK; -using OpenTK.Graphics.ES30; -using osu.Framework.Statistics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - internal class TextureGLSingle : TextureGL - { - public const int MAX_MIPMAP_LEVELS = 3; - - private static readonly Action default_quad_action; - private static readonly Action default_triangle_action; - - static TextureGLSingle() - { - QuadBatch quadBatch = new QuadBatch(512, 128); - default_quad_action = quadBatch.AddAction; - - // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices - // per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong - // grouping of vertices into primitives. - LinearBatch triangleBatch = new LinearBatch(512 * 3, 128, PrimitiveType.Triangles); - default_triangle_action = triangleBatch.AddAction; - } - - private readonly ConcurrentQueue uploadQueue = new ConcurrentQueue(); - - private int internalWidth; - private int internalHeight; - - private readonly All filteringMode; - private TextureWrapMode internalWrapMode; - - public override bool Loaded => textureId > 0 || uploadQueue.Count > 0; - - public TextureGLSingle(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) - { - Width = width; - Height = height; - this.manualMipmaps = manualMipmaps; - this.filteringMode = filteringMode; - } - - #region Disposal - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - while (uploadQueue.TryDequeue(out TextureUpload u)) - u.Dispose(); - - GLWrapper.ScheduleDisposal(unload); - } - - /// - /// Removes texture from GL memory. - /// - private void unload() - { - int disposableId = textureId; - - if (disposableId <= 0) - return; - - GL.DeleteTextures(1, new[] { disposableId }); - - textureId = 0; - } - - #endregion - - private int height; - - public override TextureGL Native => this; - - public override int Height - { - get { return height; } - set { height = value; } - } - - private int width; - - public override int Width - { - get { return width; } - set { width = value; } - } - - private int textureId; - - public override int TextureId - { - get - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not obtain ID of a disposed texture."); - - if (textureId == 0) - throw new InvalidOperationException("Can not obtain ID of a texture before uploading it."); - - return textureId; - } - } - - private static void rotateVector(ref Vector2 toRotate, float sin, float cos) - { - float oldX = toRotate.X; - toRotate.X = toRotate.X * cos - toRotate.Y * sin; - toRotate.Y = oldX * sin + toRotate.Y * cos; - } - - public override RectangleF GetTextureRect(RectangleF? textureRect) - { - RectangleF texRect = textureRect != null - ? new RectangleF(textureRect.Value.X, textureRect.Value.Y, textureRect.Value.Width, textureRect.Value.Height) - : new RectangleF(0, 0, Width, Height); - - texRect.X /= width; - texRect.Y /= height; - texRect.Width /= width; - texRect.Height /= height; - - return texRect; - } - - public override void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not draw a triangle with a disposed texture."); - - RectangleF texRect = GetTextureRect(textureRect); - Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero; - RectangleF inflatedTexRect = texRect.Inflate(inflationAmount); - - if (vertexAction == null) - vertexAction = default_triangle_action; - - // We split the triangle into two, such that we can obtain smooth edges with our - // texture coordinate trick. We might want to revert this to drawing a single - // triangle in case we ever need proper texturing, or if the additional vertices - // end up becoming an overhead (unlikely). - SRGBColour topColour = (drawColour.TopLeft + drawColour.TopRight) / 2; - SRGBColour bottomColour = (drawColour.BottomLeft + drawColour.BottomRight) / 2; - - // Left triangle half - vertexAction(new TexturedVertex2D - { - Position = vertexTriangle.P0, - TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Top), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = topColour.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = vertexTriangle.P1, - TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = drawColour.BottomLeft.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = (vertexTriangle.P1 + vertexTriangle.P2) / 2, - TexturePosition = new Vector2((inflatedTexRect.Left + inflatedTexRect.Right) / 2, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = bottomColour.Linear, - }); - - // Right triangle half - vertexAction(new TexturedVertex2D - { - Position = vertexTriangle.P0, - TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Top), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = topColour.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = (vertexTriangle.P1 + vertexTriangle.P2) / 2, - TexturePosition = new Vector2((inflatedTexRect.Left + inflatedTexRect.Right) / 2, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = bottomColour.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = vertexTriangle.P2, - TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = inflationAmount, - Colour = drawColour.BottomRight.Linear, - }); - - FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexTriangle.ConservativeArea); - } - - public override void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not draw a quad with a disposed texture."); - - RectangleF texRect = GetTextureRect(textureRect); - Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero; - RectangleF inflatedTexRect = texRect.Inflate(inflationAmount); - Vector2 blendRange = blendRangeOverride ?? inflationAmount; - - if (vertexAction == null) - vertexAction = default_quad_action; - - vertexAction(new TexturedVertex2D - { - Position = vertexQuad.BottomLeft, - TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = blendRange, - Colour = drawColour.BottomLeft.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = vertexQuad.BottomRight, - TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Bottom), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = blendRange, - Colour = drawColour.BottomRight.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = vertexQuad.TopRight, - TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Top), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = blendRange, - Colour = drawColour.TopRight.Linear, - }); - vertexAction(new TexturedVertex2D - { - Position = vertexQuad.TopLeft, - TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Top), - TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), - BlendRange = blendRange, - Colour = drawColour.TopLeft.Linear, - }); - - FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexQuad.ConservativeArea); - } - - private void updateWrapMode() - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not update wrap mode of a disposed texture."); - - internalWrapMode = WrapMode; - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)internalWrapMode); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)internalWrapMode); - } - - public override void SetData(TextureUpload upload) - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not set data of a disposed texture."); - - if (upload.Bounds.IsEmpty) - upload.Bounds = new RectangleI(0, 0, width, height); - - IsTransparent = false; - - bool requireUpload = uploadQueue.Count == 0; - uploadQueue.Enqueue(upload); - if (requireUpload) - GLWrapper.EnqueueTextureUpload(this); - } - - public override bool Bind() - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not bind a disposed texture."); - - Upload(); - - if (textureId <= 0) - return false; - - if (IsTransparent) - return false; - - GLWrapper.BindTexture(this); - - if (internalWrapMode != WrapMode) - updateWrapMode(); - - return true; - } - - private bool manualMipmaps; - - internal override bool Upload() - { - // We should never run raw OGL calls on another thread than the main thread due to race conditions. - ThreadSafety.EnsureDrawThread(); - - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not upload data to a disposed texture."); - - bool didUpload = false; - - while (uploadQueue.TryDequeue(out TextureUpload upload)) - { - IntPtr dataPointer; - GCHandle? h0; - - if (upload.Data.Length == 0) - { - h0 = null; - dataPointer = IntPtr.Zero; - } - else - { - h0 = GCHandle.Alloc(upload.Data, GCHandleType.Pinned); - dataPointer = h0.Value.AddrOfPinnedObject(); - didUpload = true; - } - - try - { - // Do we need to generate a new texture? - if (textureId <= 0 || internalWidth != width || internalHeight != height) - { - internalWidth = width; - internalHeight = height; - - // We only need to generate a new texture if we don't have one already. Otherwise just re-use the current one. - if (textureId <= 0) - { - int[] textures = new int[1]; - GL.GenTextures(1, textures); - - textureId = textures[0]; - - GLWrapper.BindTexture(this); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, - (int)(manualMipmaps ? filteringMode : (filteringMode == All.Linear ? All.LinearMipmapLinear : All.Nearest))); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)filteringMode); - - // 33085 is GL_TEXTURE_MAX_LEVEL, which is not available within TextureParameterName. - // It controls the amount of mipmap levels generated by GL.GenerateMipmap later on. - GL.TexParameter(TextureTarget.Texture2D, (TextureParameterName)33085, MAX_MIPMAP_LEVELS); - - updateWrapMode(); - } - else - GLWrapper.BindTexture(this); - - if (width == upload.Bounds.Width && height == upload.Bounds.Height || dataPointer == IntPtr.Zero) - GL.TexImage2D(TextureTarget2d.Texture2D, upload.Level, TextureComponentCount.Srgb8Alpha8, width, height, 0, upload.Format, PixelType.UnsignedByte, dataPointer); - else - { - initializeLevel(upload.Level, width, height); - - GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X, upload.Bounds.Y, upload.Bounds.Width, upload.Bounds.Height, upload.Format, PixelType.UnsignedByte, - dataPointer); - } - } - // Just update content of the current texture - else if (dataPointer != IntPtr.Zero) - { - GLWrapper.BindTexture(this); - - if (!manualMipmaps && upload.Level > 0) - { - //allocate mipmap levels - int level = 1; - int d = 2; - - while (width / d > 0) - { - initializeLevel(level, width / d, height / d); - level++; - d *= 2; - } - - manualMipmaps = true; - } - - int div = (int)Math.Pow(2, upload.Level); - - GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X / div, upload.Bounds.Y / div, upload.Bounds.Width / div, upload.Bounds.Height / div, upload.Format, - PixelType.UnsignedByte, dataPointer); - } - } - finally - { - h0?.Free(); - upload.Dispose(); - } - } - - if (didUpload && !manualMipmaps) - { - GL.Hint(HintTarget.GenerateMipmapHint, HintMode.Nicest); - GL.GenerateMipmap(TextureTarget.Texture2D); - } - - return didUpload; - } - - private void initializeLevel(int level, int width, int height) - { - byte[] transparentWhite = new byte[width * height * 4]; - GCHandle h0 = GCHandle.Alloc(transparentWhite, GCHandleType.Pinned); - GL.TexImage2D(TextureTarget2d.Texture2D, level, TextureComponentCount.Srgb8Alpha8, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, h0.AddrOfPinnedObject()); - h0.Free(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using osu.Framework.Development; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.Primitives; +using OpenTK; +using OpenTK.Graphics.ES30; +using osu.Framework.Statistics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + internal class TextureGLSingle : TextureGL + { + public const int MAX_MIPMAP_LEVELS = 3; + + private static readonly Action default_quad_action; + private static readonly Action default_triangle_action; + + static TextureGLSingle() + { + QuadBatch quadBatch = new QuadBatch(512, 128); + default_quad_action = quadBatch.AddAction; + + // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices + // per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong + // grouping of vertices into primitives. + LinearBatch triangleBatch = new LinearBatch(512 * 3, 128, PrimitiveType.Triangles); + default_triangle_action = triangleBatch.AddAction; + } + + private readonly ConcurrentQueue uploadQueue = new ConcurrentQueue(); + + private int internalWidth; + private int internalHeight; + + private readonly All filteringMode; + private TextureWrapMode internalWrapMode; + + public override bool Loaded => textureId > 0 || uploadQueue.Count > 0; + + public TextureGLSingle(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) + { + Width = width; + Height = height; + this.manualMipmaps = manualMipmaps; + this.filteringMode = filteringMode; + } + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + while (uploadQueue.TryDequeue(out TextureUpload u)) + u.Dispose(); + + GLWrapper.ScheduleDisposal(unload); + } + + /// + /// Removes texture from GL memory. + /// + private void unload() + { + int disposableId = textureId; + + if (disposableId <= 0) + return; + + GL.DeleteTextures(1, new[] { disposableId }); + + textureId = 0; + } + + #endregion + + private int height; + + public override TextureGL Native => this; + + public override int Height + { + get { return height; } + set { height = value; } + } + + private int width; + + public override int Width + { + get { return width; } + set { width = value; } + } + + private int textureId; + + public override int TextureId + { + get + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not obtain ID of a disposed texture."); + + if (textureId == 0) + throw new InvalidOperationException("Can not obtain ID of a texture before uploading it."); + + return textureId; + } + } + + private static void rotateVector(ref Vector2 toRotate, float sin, float cos) + { + float oldX = toRotate.X; + toRotate.X = toRotate.X * cos - toRotate.Y * sin; + toRotate.Y = oldX * sin + toRotate.Y * cos; + } + + public override RectangleF GetTextureRect(RectangleF? textureRect) + { + RectangleF texRect = textureRect != null + ? new RectangleF(textureRect.Value.X, textureRect.Value.Y, textureRect.Value.Width, textureRect.Value.Height) + : new RectangleF(0, 0, Width, Height); + + texRect.X /= width; + texRect.Y /= height; + texRect.Width /= width; + texRect.Height /= height; + + return texRect; + } + + public override void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not draw a triangle with a disposed texture."); + + RectangleF texRect = GetTextureRect(textureRect); + Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero; + RectangleF inflatedTexRect = texRect.Inflate(inflationAmount); + + if (vertexAction == null) + vertexAction = default_triangle_action; + + // We split the triangle into two, such that we can obtain smooth edges with our + // texture coordinate trick. We might want to revert this to drawing a single + // triangle in case we ever need proper texturing, or if the additional vertices + // end up becoming an overhead (unlikely). + SRGBColour topColour = (drawColour.TopLeft + drawColour.TopRight) / 2; + SRGBColour bottomColour = (drawColour.BottomLeft + drawColour.BottomRight) / 2; + + // Left triangle half + vertexAction(new TexturedVertex2D + { + Position = vertexTriangle.P0, + TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Top), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = topColour.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = vertexTriangle.P1, + TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = drawColour.BottomLeft.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = (vertexTriangle.P1 + vertexTriangle.P2) / 2, + TexturePosition = new Vector2((inflatedTexRect.Left + inflatedTexRect.Right) / 2, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = bottomColour.Linear, + }); + + // Right triangle half + vertexAction(new TexturedVertex2D + { + Position = vertexTriangle.P0, + TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Top), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = topColour.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = (vertexTriangle.P1 + vertexTriangle.P2) / 2, + TexturePosition = new Vector2((inflatedTexRect.Left + inflatedTexRect.Right) / 2, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = bottomColour.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = vertexTriangle.P2, + TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = inflationAmount, + Colour = drawColour.BottomRight.Linear, + }); + + FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexTriangle.ConservativeArea); + } + + public override void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not draw a quad with a disposed texture."); + + RectangleF texRect = GetTextureRect(textureRect); + Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero; + RectangleF inflatedTexRect = texRect.Inflate(inflationAmount); + Vector2 blendRange = blendRangeOverride ?? inflationAmount; + + if (vertexAction == null) + vertexAction = default_quad_action; + + vertexAction(new TexturedVertex2D + { + Position = vertexQuad.BottomLeft, + TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = blendRange, + Colour = drawColour.BottomLeft.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = vertexQuad.BottomRight, + TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Bottom), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = blendRange, + Colour = drawColour.BottomRight.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = vertexQuad.TopRight, + TexturePosition = new Vector2(inflatedTexRect.Right, inflatedTexRect.Top), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = blendRange, + Colour = drawColour.TopRight.Linear, + }); + vertexAction(new TexturedVertex2D + { + Position = vertexQuad.TopLeft, + TexturePosition = new Vector2(inflatedTexRect.Left, inflatedTexRect.Top), + TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), + BlendRange = blendRange, + Colour = drawColour.TopLeft.Linear, + }); + + FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexQuad.ConservativeArea); + } + + private void updateWrapMode() + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not update wrap mode of a disposed texture."); + + internalWrapMode = WrapMode; + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)internalWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)internalWrapMode); + } + + public override void SetData(TextureUpload upload) + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not set data of a disposed texture."); + + if (upload.Bounds.IsEmpty) + upload.Bounds = new RectangleI(0, 0, width, height); + + IsTransparent = false; + + bool requireUpload = uploadQueue.Count == 0; + uploadQueue.Enqueue(upload); + if (requireUpload) + GLWrapper.EnqueueTextureUpload(this); + } + + public override bool Bind() + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not bind a disposed texture."); + + Upload(); + + if (textureId <= 0) + return false; + + if (IsTransparent) + return false; + + GLWrapper.BindTexture(this); + + if (internalWrapMode != WrapMode) + updateWrapMode(); + + return true; + } + + private bool manualMipmaps; + + internal override bool Upload() + { + // We should never run raw OGL calls on another thread than the main thread due to race conditions. + ThreadSafety.EnsureDrawThread(); + + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not upload data to a disposed texture."); + + bool didUpload = false; + + while (uploadQueue.TryDequeue(out TextureUpload upload)) + { + IntPtr dataPointer; + GCHandle? h0; + + if (upload.Data.Length == 0) + { + h0 = null; + dataPointer = IntPtr.Zero; + } + else + { + h0 = GCHandle.Alloc(upload.Data, GCHandleType.Pinned); + dataPointer = h0.Value.AddrOfPinnedObject(); + didUpload = true; + } + + try + { + // Do we need to generate a new texture? + if (textureId <= 0 || internalWidth != width || internalHeight != height) + { + internalWidth = width; + internalHeight = height; + + // We only need to generate a new texture if we don't have one already. Otherwise just re-use the current one. + if (textureId <= 0) + { + int[] textures = new int[1]; + GL.GenTextures(1, textures); + + textureId = textures[0]; + + GLWrapper.BindTexture(this); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, + (int)(manualMipmaps ? filteringMode : (filteringMode == All.Linear ? All.LinearMipmapLinear : All.Nearest))); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)filteringMode); + + // 33085 is GL_TEXTURE_MAX_LEVEL, which is not available within TextureParameterName. + // It controls the amount of mipmap levels generated by GL.GenerateMipmap later on. + GL.TexParameter(TextureTarget.Texture2D, (TextureParameterName)33085, MAX_MIPMAP_LEVELS); + + updateWrapMode(); + } + else + GLWrapper.BindTexture(this); + + if (width == upload.Bounds.Width && height == upload.Bounds.Height || dataPointer == IntPtr.Zero) + GL.TexImage2D(TextureTarget2d.Texture2D, upload.Level, TextureComponentCount.Srgb8Alpha8, width, height, 0, upload.Format, PixelType.UnsignedByte, dataPointer); + else + { + initializeLevel(upload.Level, width, height); + + GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X, upload.Bounds.Y, upload.Bounds.Width, upload.Bounds.Height, upload.Format, PixelType.UnsignedByte, + dataPointer); + } + } + // Just update content of the current texture + else if (dataPointer != IntPtr.Zero) + { + GLWrapper.BindTexture(this); + + if (!manualMipmaps && upload.Level > 0) + { + //allocate mipmap levels + int level = 1; + int d = 2; + + while (width / d > 0) + { + initializeLevel(level, width / d, height / d); + level++; + d *= 2; + } + + manualMipmaps = true; + } + + int div = (int)Math.Pow(2, upload.Level); + + GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X / div, upload.Bounds.Y / div, upload.Bounds.Width / div, upload.Bounds.Height / div, upload.Format, + PixelType.UnsignedByte, dataPointer); + } + } + finally + { + h0?.Free(); + upload.Dispose(); + } + } + + if (didUpload && !manualMipmaps) + { + GL.Hint(HintTarget.GenerateMipmapHint, HintMode.Nicest); + GL.GenerateMipmap(TextureTarget.Texture2D); + } + + return didUpload; + } + + private void initializeLevel(int level, int width, int height) + { + byte[] transparentWhite = new byte[width * height * 4]; + GCHandle h0 = GCHandle.Alloc(transparentWhite, GCHandleType.Pinned); + GL.TexImage2D(TextureTarget2d.Texture2D, level, TextureComponentCount.Srgb8Alpha8, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, h0.AddrOfPinnedObject()); + h0.Free(); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureGLSub.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureGLSub.cs index e7eb3449b..d8232785c 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureGLSub.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureGLSub.cs @@ -1,110 +1,110 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.Primitives; -using OpenTK; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - internal class TextureGLSub : TextureGL - { - private readonly TextureGLSingle parent; - private RectangleI bounds; - - public override TextureGL Native => parent.Native; - - public override int TextureId => parent.TextureId; - public override bool Loaded => parent.Loaded; - - public TextureGLSub(RectangleI bounds, TextureGLSingle parent) - { - // If GLWrapper is not initialized at this point, it means we do not have OpenGL available - // and thus will never draw anything. In this case it is fine if the parent texture is null. - if (GLWrapper.IsInitialized && parent == null) - throw new InvalidOperationException("May not construct a subtexture without a parent texture to refer to."); - - this.bounds = bounds; - this.parent = parent; - } - - public override int Height - { - get { return bounds.Height; } - set { bounds.Height = value; } - } - - public override int Width - { - get { return bounds.Width; } - set { bounds.Width = value; } - } - - private RectangleF boundsInParent(RectangleF? textureRect) - { - RectangleF actualBounds = bounds; - - if (textureRect.HasValue) - { - RectangleF localBounds = textureRect.Value; - actualBounds.X += localBounds.X; - actualBounds.Y += localBounds.Y; - actualBounds.Width = Math.Min(localBounds.Width, bounds.Width); - actualBounds.Height = Math.Min(localBounds.Height, bounds.Height); - } - - return actualBounds; - } - - public override RectangleF GetTextureRect(RectangleF? textureRect) - { - return parent.GetTextureRect(boundsInParent(textureRect)); - } - - public override void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null) - { - parent.DrawTriangle(vertexTriangle, boundsInParent(textureRect), drawColour, vertexAction, inflationPercentage); - } - - public override void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) - { - parent.DrawQuad(vertexQuad, boundsInParent(textureRect), drawColour, vertexAction, inflationPercentage, blendRangeOverride); - } - - internal override bool Upload() - { - //no upload required; our parent does this. - return false; - } - - public override bool Bind() - { - if (IsDisposed) - throw new ObjectDisposedException(ToString(), "Can not bind disposed sub textures."); - - Upload(); - - return parent.Bind(); - } - - public override void SetData(TextureUpload upload) - { - if (upload.Bounds.Width > bounds.Width || upload.Bounds.Height > bounds.Height) - throw new ArgumentOutOfRangeException( - $"Texture is too small to fit the requested upload. Texture size is {bounds.Width} x {bounds.Height}, upload size is {upload.Bounds.Width} x {upload.Bounds.Height}.", - nameof(upload)); - - if (upload.Bounds.IsEmpty) - upload.Bounds = bounds; - else - { - upload.Bounds.X += bounds.X; - upload.Bounds.Y += bounds.Y; - } - - parent?.SetData(upload); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.Primitives; +using OpenTK; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + internal class TextureGLSub : TextureGL + { + private readonly TextureGLSingle parent; + private RectangleI bounds; + + public override TextureGL Native => parent.Native; + + public override int TextureId => parent.TextureId; + public override bool Loaded => parent.Loaded; + + public TextureGLSub(RectangleI bounds, TextureGLSingle parent) + { + // If GLWrapper is not initialized at this point, it means we do not have OpenGL available + // and thus will never draw anything. In this case it is fine if the parent texture is null. + if (GLWrapper.IsInitialized && parent == null) + throw new InvalidOperationException("May not construct a subtexture without a parent texture to refer to."); + + this.bounds = bounds; + this.parent = parent; + } + + public override int Height + { + get { return bounds.Height; } + set { bounds.Height = value; } + } + + public override int Width + { + get { return bounds.Width; } + set { bounds.Width = value; } + } + + private RectangleF boundsInParent(RectangleF? textureRect) + { + RectangleF actualBounds = bounds; + + if (textureRect.HasValue) + { + RectangleF localBounds = textureRect.Value; + actualBounds.X += localBounds.X; + actualBounds.Y += localBounds.Y; + actualBounds.Width = Math.Min(localBounds.Width, bounds.Width); + actualBounds.Height = Math.Min(localBounds.Height, bounds.Height); + } + + return actualBounds; + } + + public override RectangleF GetTextureRect(RectangleF? textureRect) + { + return parent.GetTextureRect(boundsInParent(textureRect)); + } + + public override void DrawTriangle(Triangle vertexTriangle, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null) + { + parent.DrawTriangle(vertexTriangle, boundsInParent(textureRect), drawColour, vertexAction, inflationPercentage); + } + + public override void DrawQuad(Quad vertexQuad, RectangleF? textureRect, ColourInfo drawColour, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) + { + parent.DrawQuad(vertexQuad, boundsInParent(textureRect), drawColour, vertexAction, inflationPercentage, blendRangeOverride); + } + + internal override bool Upload() + { + //no upload required; our parent does this. + return false; + } + + public override bool Bind() + { + if (IsDisposed) + throw new ObjectDisposedException(ToString(), "Can not bind disposed sub textures."); + + Upload(); + + return parent.Bind(); + } + + public override void SetData(TextureUpload upload) + { + if (upload.Bounds.Width > bounds.Width || upload.Bounds.Height > bounds.Height) + throw new ArgumentOutOfRangeException( + $"Texture is too small to fit the requested upload. Texture size is {bounds.Width} x {bounds.Height}, upload size is {upload.Bounds.Width} x {upload.Bounds.Height}.", + nameof(upload)); + + if (upload.Bounds.IsEmpty) + upload.Bounds = bounds; + else + { + upload.Bounds.X += bounds.X; + upload.Bounds.Y += bounds.Y; + } + + parent?.SetData(upload); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Textures/TextureUpload.cs b/osu.Framework/Graphics/OpenGL/Textures/TextureUpload.cs index af0418d3d..c403ad31f 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/TextureUpload.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/TextureUpload.cs @@ -1,59 +1,59 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Allocation; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Graphics.OpenGL.Textures -{ - public class TextureUpload : IDisposable - { - private static readonly BufferStack global_buffer_stack = new BufferStack(10); - - public int Level; - public PixelFormat Format = PixelFormat.Rgba; - public RectangleI Bounds; - public readonly byte[] Data; - - private readonly BufferStack bufferStack; - - public TextureUpload(int size, BufferStack bufferStack = null) - { - this.bufferStack = bufferStack ?? global_buffer_stack; - Data = this.bufferStack.ReserveBuffer(size); - } - - public TextureUpload(byte[] data) - { - Data = data; - } - - #region IDisposable Support - - private bool disposedValue; - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - disposedValue = true; - bufferStack?.FreeBuffer(Data); - } - } - - ~TextureUpload() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Allocation; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Graphics.OpenGL.Textures +{ + public class TextureUpload : IDisposable + { + private static readonly BufferStack global_buffer_stack = new BufferStack(10); + + public int Level; + public PixelFormat Format = PixelFormat.Rgba; + public RectangleI Bounds; + public readonly byte[] Data; + + private readonly BufferStack bufferStack; + + public TextureUpload(int size, BufferStack bufferStack = null) + { + this.bufferStack = bufferStack ?? global_buffer_stack; + Data = this.bufferStack.ReserveBuffer(size); + } + + public TextureUpload(byte[] data) + { + Data = data; + } + + #region IDisposable Support + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + disposedValue = true; + bufferStack?.FreeBuffer(Data); + } + } + + ~TextureUpload() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/IVertex.cs b/osu.Framework/Graphics/OpenGL/Vertices/IVertex.cs index 941a9b367..ea957e584 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/IVertex.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/IVertex.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - public interface IVertex - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + public interface IVertex + { + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/ParticleVertex2D.cs b/osu.Framework/Graphics/OpenGL/Vertices/ParticleVertex2D.cs index 83abd6298..e85c9595e 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/ParticleVertex2D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/ParticleVertex2D.cs @@ -1,31 +1,31 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct ParticleVertex2D : IEquatable, IVertex - { - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Position; - [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 TexturePosition; - [VertexMember(1, VertexAttribPointerType.Float)] - public float Time; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Direction; - - public bool Equals(ParticleVertex2D other) - { - return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) && Time.Equals(other.Time) && Direction.Equals(other.Direction); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct ParticleVertex2D : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 TexturePosition; + [VertexMember(1, VertexAttribPointerType.Float)] + public float Time; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Direction; + + public bool Equals(ParticleVertex2D other) + { + return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) && Time.Equals(other.Time) && Direction.Equals(other.Direction); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex2D.cs b/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex2D.cs index 398a591d6..7d6f5bc46 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex2D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex2D.cs @@ -1,35 +1,35 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct TexturedVertex2D : IEquatable, IVertex - { - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Position; - [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 TexturePosition; - [VertexMember(4, VertexAttribPointerType.Float)] - public Vector4 TextureRect; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 BlendRange; - - public bool Equals(TexturedVertex2D other) - { - return Position.Equals(other.Position) - && TexturePosition.Equals(other.TexturePosition) - && Colour.Equals(other.Colour) - && TextureRect.Equals(other.TextureRect) - && BlendRange.Equals(other.BlendRange); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct TexturedVertex2D : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 TexturePosition; + [VertexMember(4, VertexAttribPointerType.Float)] + public Vector4 TextureRect; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 BlendRange; + + public bool Equals(TexturedVertex2D other) + { + return Position.Equals(other.Position) + && TexturePosition.Equals(other.TexturePosition) + && Colour.Equals(other.Colour) + && TextureRect.Equals(other.TextureRect) + && BlendRange.Equals(other.BlendRange); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex3D.cs b/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex3D.cs index 579f663d5..ece5d690b 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex3D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/TexturedVertex3D.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct TexturedVertex3D : IEquatable, IVertex - { - [VertexMember(3, VertexAttribPointerType.Float)] - public Vector3 Position; - [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 TexturePosition; - - public bool Equals(TexturedVertex3D other) - { - return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct TexturedVertex3D : IEquatable, IVertex + { + [VertexMember(3, VertexAttribPointerType.Float)] + public Vector3 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 TexturePosition; + + public bool Equals(TexturedVertex3D other) + { + return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/TimedTexturedVertex2D.cs b/osu.Framework/Graphics/OpenGL/Vertices/TimedTexturedVertex2D.cs index 891004de1..a7c943fdc 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/TimedTexturedVertex2D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/TimedTexturedVertex2D.cs @@ -1,29 +1,29 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct TimedTexturedVertex2D : IEquatable, IVertex - { - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Position; - [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 TexturePosition; - [VertexMember(1, VertexAttribPointerType.Float)] - public float Time; - - public bool Equals(TimedTexturedVertex2D other) - { - return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) && Time.Equals(other.Time); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct TimedTexturedVertex2D : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 TexturePosition; + [VertexMember(1, VertexAttribPointerType.Float)] + public float Time; + + public bool Equals(TimedTexturedVertex2D other) + { + return Position.Equals(other.Position) && TexturePosition.Equals(other.TexturePosition) && Colour.Equals(other.Colour) && Time.Equals(other.Time); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/UncolouredVertex2D.cs b/osu.Framework/Graphics/OpenGL/Vertices/UncolouredVertex2D.cs index 7b8690c34..ea0864b10 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/UncolouredVertex2D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/UncolouredVertex2D.cs @@ -1,22 +1,22 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct UncolouredVertex2D : IEquatable, IVertex - { - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Position; - - public bool Equals(UncolouredVertex2D other) - { - return Position.Equals(other.Position); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct UncolouredVertex2D : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + + public bool Equals(UncolouredVertex2D other) + { + return Position.Equals(other.Position); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/Vertex2D.cs b/osu.Framework/Graphics/OpenGL/Vertices/Vertex2D.cs index 62c7e72d7..3a0607d08 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/Vertex2D.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/Vertex2D.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [StructLayout(LayoutKind.Sequential)] - public struct Vertex2D : IEquatable, IVertex - { - [VertexMember(2, VertexAttribPointerType.Float)] - public Vector2 Position; - [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; - - public bool Equals(Vertex2D other) - { - return Position.Equals(other.Position) && Colour.Equals(other.Colour); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct Vertex2D : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + + public bool Equals(Vertex2D other) + { + return Position.Equals(other.Position) && Colour.Equals(other.Colour); + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/VertexMemberAttribute.cs b/osu.Framework/Graphics/OpenGL/Vertices/VertexMemberAttribute.cs index d9033d736..c68e0a05d 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/VertexMemberAttribute.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/VertexMemberAttribute.cs @@ -1,49 +1,49 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - [AttributeUsage(AttributeTargets.Field)] - public class VertexMemberAttribute : Attribute - { - /// - /// The number of components of represented by this vertex attribute member. - /// E.g. a is represented by **2** components. - /// - public int Count { get; private set; } - - /// - /// The type of each component of this vertex attribute member. - /// E.g. a is represented by 2 **** components. - /// - public VertexAttribPointerType Type { get; private set; } - - /// - /// Whether this vertex attribute member is normalized. If this is set to true, the member will be mapped to - /// a range of [-1, 1] (signed) or [0, 1] (unsigned) when it is passed to the shader. - /// - public bool Normalized { get; private set; } - - /// - /// The offset of this attribute member in the struct. This is computed internally by the framework. - /// - internal IntPtr Offset; - - public VertexMemberAttribute(int count, VertexAttribPointerType type) - { - Count = count; - Type = type; - Normalized = false; - } - - public VertexMemberAttribute(int count, VertexAttribPointerType type, bool normalized) - { - Count = count; - Type = type; - Normalized = normalized; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + [AttributeUsage(AttributeTargets.Field)] + public class VertexMemberAttribute : Attribute + { + /// + /// The number of components of represented by this vertex attribute member. + /// E.g. a is represented by **2** components. + /// + public int Count { get; private set; } + + /// + /// The type of each component of this vertex attribute member. + /// E.g. a is represented by 2 **** components. + /// + public VertexAttribPointerType Type { get; private set; } + + /// + /// Whether this vertex attribute member is normalized. If this is set to true, the member will be mapped to + /// a range of [-1, 1] (signed) or [0, 1] (unsigned) when it is passed to the shader. + /// + public bool Normalized { get; private set; } + + /// + /// The offset of this attribute member in the struct. This is computed internally by the framework. + /// + internal IntPtr Offset; + + public VertexMemberAttribute(int count, VertexAttribPointerType type) + { + Count = count; + Type = type; + Normalized = false; + } + + public VertexMemberAttribute(int count, VertexAttribPointerType type, bool normalized) + { + Count = count; + Type = type; + Normalized = normalized; + } + } +} diff --git a/osu.Framework/Graphics/OpenGL/Vertices/VertexUtils.cs b/osu.Framework/Graphics/OpenGL/Vertices/VertexUtils.cs index e4b389a2a..38745d2da 100644 --- a/osu.Framework/Graphics/OpenGL/Vertices/VertexUtils.cs +++ b/osu.Framework/Graphics/OpenGL/Vertices/VertexUtils.cs @@ -1,71 +1,71 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -// ReSharper disable StaticMemberInGenericType - -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.OpenGL.Vertices -{ - /// - /// Helper method that provides functionality to enable and bind vertex attributes. - /// - internal static class VertexUtils - where T : IVertex - { - /// - /// The stride of the vertex of type . - /// - public static readonly int STRIDE = BlittableValueType.StrideOf(default(T)); - - private static readonly List attributes = new List(); - private static int amountEnabledAttributes; - - static VertexUtils() - { - // Use reflection to retrieve the members attached with a VertexMemberAttribute - foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(t => t.IsDefined(typeof(VertexMemberAttribute), true))) - { - var attrib = (VertexMemberAttribute)field.GetCustomAttribute(typeof(VertexMemberAttribute)); - - // Because this is an un-seen vertex, the attribute locations are unknown, but they're needed for marshalling - attrib.Offset = Marshal.OffsetOf(typeof(T), field.Name); - - attributes.Add(attrib); - } - } - - /// - /// Enables and binds the vertex attributes/pointers for the vertex of type . - /// - public static void Bind() - { - enableAttributes(attributes.Count); - for (int i = 0; i < attributes.Count; i++) - GL.VertexAttribPointer(i, attributes[i].Count, attributes[i].Type, attributes[i].Normalized, STRIDE, attributes[i].Offset); - } - - private static void enableAttributes(int amount) - { - if (amount == amountEnabledAttributes) - return; - if (amount > amountEnabledAttributes) - { - for (int i = amountEnabledAttributes; i < amount; ++i) - GL.EnableVertexAttribArray(i); - } - else - { - for (int i = amountEnabledAttributes - 1; i >= amount; --i) - GL.DisableVertexAttribArray(i); - } - - amountEnabledAttributes = amount; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +// ReSharper disable StaticMemberInGenericType + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.OpenGL.Vertices +{ + /// + /// Helper method that provides functionality to enable and bind vertex attributes. + /// + internal static class VertexUtils + where T : IVertex + { + /// + /// The stride of the vertex of type . + /// + public static readonly int STRIDE = BlittableValueType.StrideOf(default(T)); + + private static readonly List attributes = new List(); + private static int amountEnabledAttributes; + + static VertexUtils() + { + // Use reflection to retrieve the members attached with a VertexMemberAttribute + foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(t => t.IsDefined(typeof(VertexMemberAttribute), true))) + { + var attrib = (VertexMemberAttribute)field.GetCustomAttribute(typeof(VertexMemberAttribute)); + + // Because this is an un-seen vertex, the attribute locations are unknown, but they're needed for marshalling + attrib.Offset = Marshal.OffsetOf(typeof(T), field.Name); + + attributes.Add(attrib); + } + } + + /// + /// Enables and binds the vertex attributes/pointers for the vertex of type . + /// + public static void Bind() + { + enableAttributes(attributes.Count); + for (int i = 0; i < attributes.Count; i++) + GL.VertexAttribPointer(i, attributes[i].Count, attributes[i].Type, attributes[i].Normalized, STRIDE, attributes[i].Offset); + } + + private static void enableAttributes(int amount) + { + if (amount == amountEnabledAttributes) + return; + if (amount > amountEnabledAttributes) + { + for (int i = amountEnabledAttributes; i < amount; ++i) + GL.EnableVertexAttribArray(i); + } + else + { + for (int i = amountEnabledAttributes - 1; i >= amount; --i) + GL.DisableVertexAttribArray(i); + } + + amountEnabledAttributes = amount; + } + } +} diff --git a/osu.Framework/Graphics/Performance/FpsDisplay.cs b/osu.Framework/Graphics/Performance/FpsDisplay.cs index 61440adb1..b197a3373 100644 --- a/osu.Framework/Graphics/Performance/FpsDisplay.cs +++ b/osu.Framework/Graphics/Performance/FpsDisplay.cs @@ -1,75 +1,75 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.MathUtils; -using osu.Framework.Timing; -using System; - -namespace osu.Framework.Graphics.Performance -{ - internal class FpsDisplay : Container - { - private readonly SpriteText counter; - - private readonly IFrameBasedClock clock; - private double displayFps; - - public bool Counting = true; - - public FpsDisplay(IFrameBasedClock clock) - { - this.clock = clock; - - Masking = true; - CornerRadius = 5; - - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.75f - }, - counter = new SpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Text = @"...", - FixedWidth = true, - } - }); - } - - private float aimWidth; - - protected override void Update() - { - base.Update(); - - if (!Counting) return; - - displayFps = Interpolation.Damp(displayFps, clock.FramesPerSecond, 0.01, Math.Max(clock.ElapsedFrameTime, 0) / 1000); - - if (counter.DrawWidth != aimWidth) - { - ClearTransforms(); - - if (aimWidth == 0) - Size = counter.DrawSize; - else if (Precision.AlmostBigger(counter.DrawWidth, aimWidth)) - this.ResizeTo(counter.DrawSize, 200, Easing.InOutSine); - else - this.Delay(1500).ResizeTo(counter.DrawSize, 500, Easing.InOutSine); - - aimWidth = counter.DrawWidth; - } - - counter.Text = displayFps.ToString(@"0"); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using System; + +namespace osu.Framework.Graphics.Performance +{ + internal class FpsDisplay : Container + { + private readonly SpriteText counter; + + private readonly IFrameBasedClock clock; + private double displayFps; + + public bool Counting = true; + + public FpsDisplay(IFrameBasedClock clock) + { + this.clock = clock; + + Masking = true; + CornerRadius = 5; + + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.75f + }, + counter = new SpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = @"...", + FixedWidth = true, + } + }); + } + + private float aimWidth; + + protected override void Update() + { + base.Update(); + + if (!Counting) return; + + displayFps = Interpolation.Damp(displayFps, clock.FramesPerSecond, 0.01, Math.Max(clock.ElapsedFrameTime, 0) / 1000); + + if (counter.DrawWidth != aimWidth) + { + ClearTransforms(); + + if (aimWidth == 0) + Size = counter.DrawSize; + else if (Precision.AlmostBigger(counter.DrawWidth, aimWidth)) + this.ResizeTo(counter.DrawSize, 200, Easing.InOutSine); + else + this.Delay(1500).ResizeTo(counter.DrawSize, 500, Easing.InOutSine); + + aimWidth = counter.DrawWidth; + } + + counter.Text = displayFps.ToString(@"0"); + } + } +} diff --git a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs index 21668da9e..21caa8cbe 100644 --- a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs +++ b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs @@ -1,579 +1,579 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input; -using osu.Framework.MathUtils; -using osu.Framework.Statistics; -using osu.Framework.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace osu.Framework.Graphics.Performance -{ - internal class FrameStatisticsDisplay : Container, IStateful - { - protected const int WIDTH = 800; - protected const int HEIGHT = 100; - - private const int amount_count_steps = 5; - - private const int amount_ms_steps = 5; - private const float visible_ms_range = 20; - private const float scale = HEIGHT / visible_ms_range; - - private const float alpha_when_active = 0.75f; - - private readonly TimeBar[] timeBars; - private readonly BufferStack textureBufferStack; - - private static readonly Color4[] garbage_collect_colors = { Color4.Green, Color4.Yellow, Color4.Red }; - private readonly PerformanceMonitor monitor; - - private int currentX; - - private int timeBarIndex => currentX / WIDTH; - private int timeBarX => currentX % WIDTH; - - private bool processFrames = true; - - private readonly Container overlayContainer; - private readonly Drawable labelText; - private readonly Sprite counterBarBackground; - - private readonly Container mainContainer; - private readonly Container timeBarsContainer; - - private readonly Drawable[] legendMapping = new Drawable[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES]; - private readonly Dictionary counterBars = new Dictionary(); - - private readonly FpsDisplay fpsDisplay; - - private FrameStatisticsMode state; - - public event Action StateChanged; - - public FrameStatisticsMode State - { - get { return state; } - - set - { - if (state == value) return; - - state = value; - - switch (state) - { - case FrameStatisticsMode.Minimal: - mainContainer.AutoSizeAxes = Axes.Both; - - timeBarsContainer.Hide(); - - labelText.Origin = Anchor.CentreRight; - labelText.Rotation = 0; - break; - case FrameStatisticsMode.Full: - mainContainer.AutoSizeAxes = Axes.None; - mainContainer.Size = new Vector2(WIDTH, HEIGHT); - - timeBarsContainer.Show(); - - labelText.Origin = Anchor.BottomCentre; - labelText.Rotation = -90; - break; - } - - Active = true; - - StateChanged?.Invoke(State); - } - } - - public FrameStatisticsDisplay(GameThread thread, TextureAtlas atlas) - { - Name = thread.Name; - monitor = thread.Monitor; - - Origin = Anchor.TopRight; - AutoSizeAxes = Axes.Both; - Alpha = alpha_when_active; - - bool hasCounters = monitor.ActiveCounters.Any(b => b); - Child = new Container - { - AutoSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Children = new[] - { - labelText = new SpriteText - { - Text = Name, - Origin = Anchor.BottomCentre, - Anchor = Anchor.CentreLeft, - Rotation = -90, - }, - !hasCounters - ? new Container { Width = 2 } - : new Container - { - Masking = true, - CornerRadius = 5, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Right = 2, Left = 2 }, - Children = new Drawable[] - { - counterBarBackground = new Sprite - { - Texture = atlas.Add(1, HEIGHT), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1, 1), - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - ChildrenEnumerable = - from StatisticsCounterType t in Enum.GetValues(typeof(StatisticsCounterType)) - where monitor.ActiveCounters[(int)t] - select counterBars[t] = new CounterBar - { - Colour = getColour(t), - Label = t.ToString(), - }, - }, - } - } - } - }, - mainContainer = new Container - { - Size = new Vector2(WIDTH, HEIGHT), - Children = new[] - { - timeBarsContainer = new Container - { - Masking = true, - CornerRadius = 5, - RelativeSizeAxes = Axes.Both, - Children = timeBars = new[] - { - new TimeBar(atlas), - new TimeBar(atlas), - }, - }, - fpsDisplay = new FpsDisplay(monitor.Clock) - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, - overlayContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Children = new[] - { - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5, 1), - Padding = new MarginPadding { Right = 5 }, - ChildrenEnumerable = - from PerformanceCollectionType t in Enum.GetValues(typeof(PerformanceCollectionType)) - select legendMapping[(int)t] = new SpriteText - { - Colour = getColour(t), - Text = t.ToString(), - Alpha = 0 - }, - }, - new SpriteText - { - Padding = new MarginPadding { Left = 4 }, - Text = $@"{visible_ms_range}ms" - }, - new SpriteText - { - Padding = new MarginPadding { Left = 4 }, - Text = @"0ms", - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } - } - } - } - } - } - }; - - textureBufferStack = new BufferStack(timeBars.Length * WIDTH); - } - - [BackgroundDependencyLoader] - private void load() - { - //initialise background - byte[] column = new byte[HEIGHT * 4]; - byte[] fullBackground = new byte[WIDTH * HEIGHT * 4]; - - addArea(null, null, HEIGHT, column, amount_ms_steps); - - for (int i = 0; i < HEIGHT; i++) - for (int k = 0; k < WIDTH; k++) - Buffer.BlockCopy(column, i * 4, fullBackground, i * WIDTH * 4 + k * 4, 4); - - addArea(null, null, HEIGHT, column, amount_count_steps); - - counterBarBackground?.Texture.SetData(new TextureUpload(column)); - Schedule(() => - { - foreach (var t in timeBars) - t.Sprite.Texture.SetData(new TextureUpload(fullBackground)); - }); - } - - private void addEvent(int type) - { - Box b = new Box - { - Origin = Anchor.TopCentre, - Position = new Vector2(timeBarX, type * 3), - Colour = garbage_collect_colors[type], - Size = new Vector2(3, 3) - }; - - timeBars[timeBarIndex].Add(b); - } - - private bool active = true; - - public bool Active - { - get { return active; } - - set - { - if (active == value) return; - - active = value || state != FrameStatisticsMode.Full; - - overlayContainer.FadeTo(active ? 0 : 1, 100); - this.FadeTo(active ? alpha_when_active : 1, 100); - fpsDisplay.Counting = active; - processFrames = active; - foreach (CounterBar bar in counterBars.Values) - bar.Active = active; - } - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Key == Key.ControlLeft) - Active = false; - return base.OnKeyDown(state, args); - } - - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) - { - if (args.Key == Key.ControlLeft) - Active = true; - return base.OnKeyUp(state, args); - } - - private void applyFrameGC(FrameStatistics frame) - { - foreach (int gcLevel in frame.GarbageCollections) - addEvent(gcLevel); - } - - private void applyFrameTime(FrameStatistics frame) - { - TimeBar timeBar = timeBars[timeBarIndex]; - TextureUpload upload = new TextureUpload(HEIGHT * 4, textureBufferStack) - { - Bounds = new RectangleI(timeBarX, 0, 1, HEIGHT) - }; - - int currentHeight = HEIGHT; - - for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++) - currentHeight = addArea(frame, (PerformanceCollectionType)i, currentHeight, upload.Data, amount_ms_steps); - addArea(frame, null, currentHeight, upload.Data, amount_ms_steps); - - timeBar.Sprite.Texture.SetData(upload); - - timeBars[timeBarIndex].MoveToX(WIDTH - timeBarX); - timeBars[(timeBarIndex + 1) % timeBars.Length].MoveToX(-timeBarX); - currentX = (currentX + 1) % (timeBars.Length * WIDTH); - - foreach (Drawable e in timeBars[(timeBarIndex + 1) % timeBars.Length].Children) - if (e is Box && e.DrawPosition.X <= timeBarX) - e.Expire(); - } - - private void applyFrameCounts(FrameStatistics frame) - { - foreach (var pair in frame.Counts) - counterBars[pair.Key].Value = pair.Value; - } - - private void applyFrame(FrameStatistics frame) - { - if (state == FrameStatisticsMode.Full) - { - applyFrameGC(frame); - applyFrameTime(frame); - } - - applyFrameCounts(frame); - } - - protected override void Update() - { - base.Update(); - - while (monitor.PendingFrames.TryDequeue(out FrameStatistics frame)) - { - if (processFrames) - applyFrame(frame); - - monitor.FramesHeap.FreeObject(frame); - } - } - - private Color4 getColour(PerformanceCollectionType type) - { - switch (type) - { - default: - return Color4.YellowGreen; - case PerformanceCollectionType.SwapBuffer: - return Color4.Red; -#if DEBUG - case PerformanceCollectionType.Debug: - return Color4.Yellow; -#endif - case PerformanceCollectionType.Sleep: - return Color4.DarkBlue; - case PerformanceCollectionType.Scheduler: - return Color4.HotPink; - case PerformanceCollectionType.WndProc: - return Color4.GhostWhite; - case PerformanceCollectionType.GLReset: - return Color4.Cyan; - } - } - - private Color4 getColour(StatisticsCounterType type) - { - switch (type) - { - default: - return Color4.Yellow; - - case StatisticsCounterType.VBufBinds: - return Color4.SkyBlue; - - case StatisticsCounterType.Invalidations: - case StatisticsCounterType.TextureBinds: - case StatisticsCounterType.TasksRun: - case StatisticsCounterType.MouseEvents: - return Color4.BlueViolet; - - case StatisticsCounterType.DrawCalls: - case StatisticsCounterType.Refreshes: - case StatisticsCounterType.Tracks: - case StatisticsCounterType.KeyEvents: - return Color4.YellowGreen; - - case StatisticsCounterType.DrawNodeCtor: - case StatisticsCounterType.VerticesDraw: - case StatisticsCounterType.Samples: - return Color4.HotPink; - - case StatisticsCounterType.DrawNodeAppl: - case StatisticsCounterType.VerticesUpl: - case StatisticsCounterType.SChannels: - return Color4.Red; - - case StatisticsCounterType.ScheduleInvk: - case StatisticsCounterType.Pixels: - case StatisticsCounterType.Components: - return Color4.Cyan; - } - } - - private int addArea(FrameStatistics frame, PerformanceCollectionType? frameTimeType, int currentHeight, byte[] textureData, int amountSteps) - { - Trace.Assert(textureData.Length >= HEIGHT * 4, $"textureData is too small ({textureData.Length}) to hold area data."); - - int drawHeight; - - if (!frameTimeType.HasValue) - drawHeight = currentHeight; - else if (frame.CollectedTimes.TryGetValue(frameTimeType.Value, out double elapsedMilliseconds)) - { - legendMapping[(int)frameTimeType].Alpha = 1; - drawHeight = (int)(elapsedMilliseconds * scale); - } - else - return currentHeight; - - Color4 col = frameTimeType.HasValue ? getColour(frameTimeType.Value) : new Color4(0.1f, 0.1f, 0.1f, 1); - - for (int i = currentHeight - 1; i >= 0; --i) - { - if (drawHeight-- == 0) break; - - bool acceptableRange = (float)currentHeight / HEIGHT > 1 - monitor.FrameAimTime / visible_ms_range; - - float brightnessAdjust = 1; - if (!frameTimeType.HasValue) - { - int step = amountSteps / HEIGHT; - brightnessAdjust *= 1 - i * step / 8f; - } - else if (acceptableRange) - brightnessAdjust *= 0.8f; - - int index = i * 4; - textureData[index] = (byte)(255 * col.R * brightnessAdjust); - textureData[index + 1] = (byte)(255 * col.G * brightnessAdjust); - textureData[index + 2] = (byte)(255 * col.B * brightnessAdjust); - textureData[index + 3] = (byte)(255 * col.A); - - currentHeight--; - } - - return currentHeight; - } - - private class TimeBar : Container - { - public readonly Sprite Sprite; - - public TimeBar(TextureAtlas atlas) - { - Size = new Vector2(WIDTH, HEIGHT); - Child = Sprite = new Sprite(); - - Sprite.Texture = atlas.Add(WIDTH, HEIGHT); - } - - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - } - - private class CounterBar : Container - { - private readonly Box box; - private readonly SpriteText text; - - public string Label; - - private bool active; - - public bool Active - { - get { return active; } - set - { - if (active == value) - return; - - active = value; - - if (active) - { - this.ResizeTo(new Vector2(bar_width, 1), 100); - text.FadeOut(100); - } - else - { - this.ResizeTo(new Vector2(bar_width + text.TextSize + 2, 1), 100); - text.FadeIn(100); - text.Text = $@"{Label}: {NumberFormatter.PrintWithSiSuffix(this.value)}"; - } - } - } - - private double height; - private double velocity; - private const double acceleration = 0.000001; - private const float bar_width = 6; - - private long value; - public long Value - { - set - { - this.value = value; - height = Math.Log10(value + 1) / amount_count_steps; - } - } - - public CounterBar() - { - Size = new Vector2(bar_width, 1); - RelativeSizeAxes = Axes.Y; - - Children = new Drawable[] - { - text = new SpriteText - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomRight, - Rotation = -90, - Position = new Vector2(-bar_width - 1, 0), - TextSize = 16, - }, - box = new Box - { - RelativeSizeAxes = Axes.Y, - Size = new Vector2(bar_width, 0), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - } - }; - - Active = true; - } - - protected override void Update() - { - base.Update(); - - double elapsedTime = Time.Elapsed; - double movement = velocity * Time.Elapsed + 0.5 * acceleration * elapsedTime * elapsedTime; - double newHeight = Math.Max(height, box.Height - movement); - box.Height = (float)newHeight; - - if (newHeight <= height) - velocity = 0; - else - velocity += Time.Elapsed * acceleration; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace osu.Framework.Graphics.Performance +{ + internal class FrameStatisticsDisplay : Container, IStateful + { + protected const int WIDTH = 800; + protected const int HEIGHT = 100; + + private const int amount_count_steps = 5; + + private const int amount_ms_steps = 5; + private const float visible_ms_range = 20; + private const float scale = HEIGHT / visible_ms_range; + + private const float alpha_when_active = 0.75f; + + private readonly TimeBar[] timeBars; + private readonly BufferStack textureBufferStack; + + private static readonly Color4[] garbage_collect_colors = { Color4.Green, Color4.Yellow, Color4.Red }; + private readonly PerformanceMonitor monitor; + + private int currentX; + + private int timeBarIndex => currentX / WIDTH; + private int timeBarX => currentX % WIDTH; + + private bool processFrames = true; + + private readonly Container overlayContainer; + private readonly Drawable labelText; + private readonly Sprite counterBarBackground; + + private readonly Container mainContainer; + private readonly Container timeBarsContainer; + + private readonly Drawable[] legendMapping = new Drawable[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES]; + private readonly Dictionary counterBars = new Dictionary(); + + private readonly FpsDisplay fpsDisplay; + + private FrameStatisticsMode state; + + public event Action StateChanged; + + public FrameStatisticsMode State + { + get { return state; } + + set + { + if (state == value) return; + + state = value; + + switch (state) + { + case FrameStatisticsMode.Minimal: + mainContainer.AutoSizeAxes = Axes.Both; + + timeBarsContainer.Hide(); + + labelText.Origin = Anchor.CentreRight; + labelText.Rotation = 0; + break; + case FrameStatisticsMode.Full: + mainContainer.AutoSizeAxes = Axes.None; + mainContainer.Size = new Vector2(WIDTH, HEIGHT); + + timeBarsContainer.Show(); + + labelText.Origin = Anchor.BottomCentre; + labelText.Rotation = -90; + break; + } + + Active = true; + + StateChanged?.Invoke(State); + } + } + + public FrameStatisticsDisplay(GameThread thread, TextureAtlas atlas) + { + Name = thread.Name; + monitor = thread.Monitor; + + Origin = Anchor.TopRight; + AutoSizeAxes = Axes.Both; + Alpha = alpha_when_active; + + bool hasCounters = monitor.ActiveCounters.Any(b => b); + Child = new Container + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new[] + { + labelText = new SpriteText + { + Text = Name, + Origin = Anchor.BottomCentre, + Anchor = Anchor.CentreLeft, + Rotation = -90, + }, + !hasCounters + ? new Container { Width = 2 } + : new Container + { + Masking = true, + CornerRadius = 5, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding { Right = 2, Left = 2 }, + Children = new Drawable[] + { + counterBarBackground = new Sprite + { + Texture = atlas.Add(1, HEIGHT), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 1), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + ChildrenEnumerable = + from StatisticsCounterType t in Enum.GetValues(typeof(StatisticsCounterType)) + where monitor.ActiveCounters[(int)t] + select counterBars[t] = new CounterBar + { + Colour = getColour(t), + Label = t.ToString(), + }, + }, + } + } + } + }, + mainContainer = new Container + { + Size = new Vector2(WIDTH, HEIGHT), + Children = new[] + { + timeBarsContainer = new Container + { + Masking = true, + CornerRadius = 5, + RelativeSizeAxes = Axes.Both, + Children = timeBars = new[] + { + new TimeBar(atlas), + new TimeBar(atlas), + }, + }, + fpsDisplay = new FpsDisplay(monitor.Clock) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + overlayContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new[] + { + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5, 1), + Padding = new MarginPadding { Right = 5 }, + ChildrenEnumerable = + from PerformanceCollectionType t in Enum.GetValues(typeof(PerformanceCollectionType)) + select legendMapping[(int)t] = new SpriteText + { + Colour = getColour(t), + Text = t.ToString(), + Alpha = 0 + }, + }, + new SpriteText + { + Padding = new MarginPadding { Left = 4 }, + Text = $@"{visible_ms_range}ms" + }, + new SpriteText + { + Padding = new MarginPadding { Left = 4 }, + Text = @"0ms", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft + } + } + } + } + } + } + }; + + textureBufferStack = new BufferStack(timeBars.Length * WIDTH); + } + + [BackgroundDependencyLoader] + private void load() + { + //initialise background + byte[] column = new byte[HEIGHT * 4]; + byte[] fullBackground = new byte[WIDTH * HEIGHT * 4]; + + addArea(null, null, HEIGHT, column, amount_ms_steps); + + for (int i = 0; i < HEIGHT; i++) + for (int k = 0; k < WIDTH; k++) + Buffer.BlockCopy(column, i * 4, fullBackground, i * WIDTH * 4 + k * 4, 4); + + addArea(null, null, HEIGHT, column, amount_count_steps); + + counterBarBackground?.Texture.SetData(new TextureUpload(column)); + Schedule(() => + { + foreach (var t in timeBars) + t.Sprite.Texture.SetData(new TextureUpload(fullBackground)); + }); + } + + private void addEvent(int type) + { + Box b = new Box + { + Origin = Anchor.TopCentre, + Position = new Vector2(timeBarX, type * 3), + Colour = garbage_collect_colors[type], + Size = new Vector2(3, 3) + }; + + timeBars[timeBarIndex].Add(b); + } + + private bool active = true; + + public bool Active + { + get { return active; } + + set + { + if (active == value) return; + + active = value || state != FrameStatisticsMode.Full; + + overlayContainer.FadeTo(active ? 0 : 1, 100); + this.FadeTo(active ? alpha_when_active : 1, 100); + fpsDisplay.Counting = active; + processFrames = active; + foreach (CounterBar bar in counterBars.Values) + bar.Active = active; + } + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Key == Key.ControlLeft) + Active = false; + return base.OnKeyDown(state, args); + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + if (args.Key == Key.ControlLeft) + Active = true; + return base.OnKeyUp(state, args); + } + + private void applyFrameGC(FrameStatistics frame) + { + foreach (int gcLevel in frame.GarbageCollections) + addEvent(gcLevel); + } + + private void applyFrameTime(FrameStatistics frame) + { + TimeBar timeBar = timeBars[timeBarIndex]; + TextureUpload upload = new TextureUpload(HEIGHT * 4, textureBufferStack) + { + Bounds = new RectangleI(timeBarX, 0, 1, HEIGHT) + }; + + int currentHeight = HEIGHT; + + for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++) + currentHeight = addArea(frame, (PerformanceCollectionType)i, currentHeight, upload.Data, amount_ms_steps); + addArea(frame, null, currentHeight, upload.Data, amount_ms_steps); + + timeBar.Sprite.Texture.SetData(upload); + + timeBars[timeBarIndex].MoveToX(WIDTH - timeBarX); + timeBars[(timeBarIndex + 1) % timeBars.Length].MoveToX(-timeBarX); + currentX = (currentX + 1) % (timeBars.Length * WIDTH); + + foreach (Drawable e in timeBars[(timeBarIndex + 1) % timeBars.Length].Children) + if (e is Box && e.DrawPosition.X <= timeBarX) + e.Expire(); + } + + private void applyFrameCounts(FrameStatistics frame) + { + foreach (var pair in frame.Counts) + counterBars[pair.Key].Value = pair.Value; + } + + private void applyFrame(FrameStatistics frame) + { + if (state == FrameStatisticsMode.Full) + { + applyFrameGC(frame); + applyFrameTime(frame); + } + + applyFrameCounts(frame); + } + + protected override void Update() + { + base.Update(); + + while (monitor.PendingFrames.TryDequeue(out FrameStatistics frame)) + { + if (processFrames) + applyFrame(frame); + + monitor.FramesHeap.FreeObject(frame); + } + } + + private Color4 getColour(PerformanceCollectionType type) + { + switch (type) + { + default: + return Color4.YellowGreen; + case PerformanceCollectionType.SwapBuffer: + return Color4.Red; +#if DEBUG + case PerformanceCollectionType.Debug: + return Color4.Yellow; +#endif + case PerformanceCollectionType.Sleep: + return Color4.DarkBlue; + case PerformanceCollectionType.Scheduler: + return Color4.HotPink; + case PerformanceCollectionType.WndProc: + return Color4.GhostWhite; + case PerformanceCollectionType.GLReset: + return Color4.Cyan; + } + } + + private Color4 getColour(StatisticsCounterType type) + { + switch (type) + { + default: + return Color4.Yellow; + + case StatisticsCounterType.VBufBinds: + return Color4.SkyBlue; + + case StatisticsCounterType.Invalidations: + case StatisticsCounterType.TextureBinds: + case StatisticsCounterType.TasksRun: + case StatisticsCounterType.MouseEvents: + return Color4.BlueViolet; + + case StatisticsCounterType.DrawCalls: + case StatisticsCounterType.Refreshes: + case StatisticsCounterType.Tracks: + case StatisticsCounterType.KeyEvents: + return Color4.YellowGreen; + + case StatisticsCounterType.DrawNodeCtor: + case StatisticsCounterType.VerticesDraw: + case StatisticsCounterType.Samples: + return Color4.HotPink; + + case StatisticsCounterType.DrawNodeAppl: + case StatisticsCounterType.VerticesUpl: + case StatisticsCounterType.SChannels: + return Color4.Red; + + case StatisticsCounterType.ScheduleInvk: + case StatisticsCounterType.Pixels: + case StatisticsCounterType.Components: + return Color4.Cyan; + } + } + + private int addArea(FrameStatistics frame, PerformanceCollectionType? frameTimeType, int currentHeight, byte[] textureData, int amountSteps) + { + Trace.Assert(textureData.Length >= HEIGHT * 4, $"textureData is too small ({textureData.Length}) to hold area data."); + + int drawHeight; + + if (!frameTimeType.HasValue) + drawHeight = currentHeight; + else if (frame.CollectedTimes.TryGetValue(frameTimeType.Value, out double elapsedMilliseconds)) + { + legendMapping[(int)frameTimeType].Alpha = 1; + drawHeight = (int)(elapsedMilliseconds * scale); + } + else + return currentHeight; + + Color4 col = frameTimeType.HasValue ? getColour(frameTimeType.Value) : new Color4(0.1f, 0.1f, 0.1f, 1); + + for (int i = currentHeight - 1; i >= 0; --i) + { + if (drawHeight-- == 0) break; + + bool acceptableRange = (float)currentHeight / HEIGHT > 1 - monitor.FrameAimTime / visible_ms_range; + + float brightnessAdjust = 1; + if (!frameTimeType.HasValue) + { + int step = amountSteps / HEIGHT; + brightnessAdjust *= 1 - i * step / 8f; + } + else if (acceptableRange) + brightnessAdjust *= 0.8f; + + int index = i * 4; + textureData[index] = (byte)(255 * col.R * brightnessAdjust); + textureData[index + 1] = (byte)(255 * col.G * brightnessAdjust); + textureData[index + 2] = (byte)(255 * col.B * brightnessAdjust); + textureData[index + 3] = (byte)(255 * col.A); + + currentHeight--; + } + + return currentHeight; + } + + private class TimeBar : Container + { + public readonly Sprite Sprite; + + public TimeBar(TextureAtlas atlas) + { + Size = new Vector2(WIDTH, HEIGHT); + Child = Sprite = new Sprite(); + + Sprite.Texture = atlas.Add(WIDTH, HEIGHT); + } + + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + } + + private class CounterBar : Container + { + private readonly Box box; + private readonly SpriteText text; + + public string Label; + + private bool active; + + public bool Active + { + get { return active; } + set + { + if (active == value) + return; + + active = value; + + if (active) + { + this.ResizeTo(new Vector2(bar_width, 1), 100); + text.FadeOut(100); + } + else + { + this.ResizeTo(new Vector2(bar_width + text.TextSize + 2, 1), 100); + text.FadeIn(100); + text.Text = $@"{Label}: {NumberFormatter.PrintWithSiSuffix(this.value)}"; + } + } + } + + private double height; + private double velocity; + private const double acceleration = 0.000001; + private const float bar_width = 6; + + private long value; + public long Value + { + set + { + this.value = value; + height = Math.Log10(value + 1) / amount_count_steps; + } + } + + public CounterBar() + { + Size = new Vector2(bar_width, 1); + RelativeSizeAxes = Axes.Y; + + Children = new Drawable[] + { + text = new SpriteText + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomRight, + Rotation = -90, + Position = new Vector2(-bar_width - 1, 0), + TextSize = 16, + }, + box = new Box + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(bar_width, 0), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + } + }; + + Active = true; + } + + protected override void Update() + { + base.Update(); + + double elapsedTime = Time.Elapsed; + double movement = velocity * Time.Elapsed + 0.5 * acceleration * elapsedTime * elapsedTime; + double newHeight = Math.Max(height, box.Height - movement); + box.Height = (float)newHeight; + + if (newHeight <= height) + velocity = 0; + else + velocity += Time.Elapsed * acceleration; + } + } + } +} diff --git a/osu.Framework/Graphics/Performance/PerformanceOverlay.cs b/osu.Framework/Graphics/Performance/PerformanceOverlay.cs index cad69f9e8..5d69259c2 100644 --- a/osu.Framework/Graphics/Performance/PerformanceOverlay.cs +++ b/osu.Framework/Graphics/Performance/PerformanceOverlay.cs @@ -1,64 +1,64 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.Textures; -using OpenTK.Graphics.ES30; -using osu.Framework.Threading; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Performance -{ - internal class PerformanceOverlay : FillFlowContainer, IStateful - { - private FrameStatisticsMode state; - - public event Action StateChanged; - - public FrameStatisticsMode State - { - get { return state; } - - set - { - if (state == value) return; - - state = value; - - switch (state) - { - case FrameStatisticsMode.None: - this.FadeOut(100); - break; - case FrameStatisticsMode.Minimal: - case FrameStatisticsMode.Full: - this.FadeIn(100); - break; - } - - foreach (FrameStatisticsDisplay d in Children) - d.State = state; - - StateChanged?.Invoke(State); - } - } - - public PerformanceOverlay(IEnumerable threads) - { - Direction = FillDirection.Vertical; - TextureAtlas atlas = new TextureAtlas(GLWrapper.MaxTextureSize, GLWrapper.MaxTextureSize, true, All.Nearest); - - foreach (GameThread t in threads) - Add(new FrameStatisticsDisplay(t, atlas) { State = state }); - } - } - - public enum FrameStatisticsMode - { - None, - Minimal, - Full - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Threading; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Performance +{ + internal class PerformanceOverlay : FillFlowContainer, IStateful + { + private FrameStatisticsMode state; + + public event Action StateChanged; + + public FrameStatisticsMode State + { + get { return state; } + + set + { + if (state == value) return; + + state = value; + + switch (state) + { + case FrameStatisticsMode.None: + this.FadeOut(100); + break; + case FrameStatisticsMode.Minimal: + case FrameStatisticsMode.Full: + this.FadeIn(100); + break; + } + + foreach (FrameStatisticsDisplay d in Children) + d.State = state; + + StateChanged?.Invoke(State); + } + } + + public PerformanceOverlay(IEnumerable threads) + { + Direction = FillDirection.Vertical; + TextureAtlas atlas = new TextureAtlas(GLWrapper.MaxTextureSize, GLWrapper.MaxTextureSize, true, All.Nearest); + + foreach (GameThread t in threads) + Add(new FrameStatisticsDisplay(t, atlas) { State = state }); + } + } + + public enum FrameStatisticsMode + { + None, + Minimal, + Full + } +} diff --git a/osu.Framework/Graphics/Primitives/IConvexPolygon.cs b/osu.Framework/Graphics/Primitives/IConvexPolygon.cs index 47fbbe01c..863208394 100644 --- a/osu.Framework/Graphics/Primitives/IConvexPolygon.cs +++ b/osu.Framework/Graphics/Primitives/IConvexPolygon.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Primitives -{ - public interface IConvexPolygon : IPolygon - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Primitives +{ + public interface IConvexPolygon : IPolygon + { + } +} diff --git a/osu.Framework/Graphics/Primitives/IPolygon.cs b/osu.Framework/Graphics/Primitives/IPolygon.cs index 01c05670d..69f68c755 100644 --- a/osu.Framework/Graphics/Primitives/IPolygon.cs +++ b/osu.Framework/Graphics/Primitives/IPolygon.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Graphics.Primitives -{ - public interface IPolygon - { - /// - /// The vertices for this polygon. - /// - Vector2[] Vertices { get; } - - /// - /// The vertices for this polygon that are used to compute the axes of the polygon. - /// - /// Optimisation: Edges that would form duplicate normals as other edges - /// in the polygon do not need their vertices added to this array. - /// - /// - Vector2[] AxisVertices { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Graphics.Primitives +{ + public interface IPolygon + { + /// + /// The vertices for this polygon. + /// + Vector2[] Vertices { get; } + + /// + /// The vertices for this polygon that are used to compute the axes of the polygon. + /// + /// Optimisation: Edges that would form duplicate normals as other edges + /// in the polygon do not need their vertices added to this array. + /// + /// + Vector2[] AxisVertices { get; } + } +} diff --git a/osu.Framework/Graphics/Primitives/Line.cs b/osu.Framework/Graphics/Primitives/Line.cs index 02164ea81..0882854e1 100644 --- a/osu.Framework/Graphics/Primitives/Line.cs +++ b/osu.Framework/Graphics/Primitives/Line.cs @@ -1,114 +1,114 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; - -namespace osu.Framework.Graphics.Primitives -{ - /// - /// Represents a single line segment. Drawing is handled by the LineManager class. - /// - public class Line - { - /// - /// Begin point of the line. - /// - public Vector2 StartPoint; - - /// - /// End point of the line. - /// - public Vector2 EndPoint; - - /// - /// The length of the line. - /// - public float Rho => (EndPoint - StartPoint).Length; - - /// - /// The direction of the second point from the first. - /// - public float Theta => (float)Math.Atan2(EndPoint.Y - StartPoint.Y, EndPoint.X - StartPoint.X); - - public Vector2 Direction => (EndPoint - StartPoint).Normalized(); - - public Vector2 OrthogonalDirection - { - get - { - Vector2 dir = Direction; - return new Vector2(-dir.Y, dir.X); - } - } - - - public Line(Vector2 p1, Vector2 p2) - { - StartPoint = p1; - EndPoint = p2; - } - - /// - /// Distance squared from an arbitrary point p to this line. - /// - public float DistanceSquaredToPoint(Vector2 p) - { - return Vector2Extensions.DistanceSquared(p, ClosestPointTo(p)); - } - - /// - /// Distance from an arbitrary point to this line. - /// - public float DistanceToPoint(Vector2 p) - { - return Vector2Extensions.Distance(p, ClosestPointTo(p)); - } - - /// - /// Finds the point closest to the given point on this line. - /// - /// - /// See http://geometryalgorithms.com/Archive/algorithm_0102/algorithm_0102.htm, near the bottom. - /// - public Vector2 ClosestPointTo(Vector2 p) - { - Vector2 v = EndPoint - StartPoint; // Vector from line's p1 to p2 - Vector2 w = p - StartPoint; // Vector from line's p1 to p - - // See if p is closer to p1 than to the segment - float c1 = Vector2.Dot(w, v); - if (c1 <= 0) - return StartPoint; - - // See if p is closer to p2 than to the segment - float c2 = Vector2.Dot(v, v); - if (c2 <= c1) - return EndPoint; - - // p is closest to point pB, between p1 and p2 - float b = c1 / c2; - Vector2 pB = StartPoint + b * v; - - return pB; - } - - public Matrix4 WorldMatrix() - { - return Matrix4.CreateRotationZ(Theta) * Matrix4.CreateTranslation(StartPoint.X, StartPoint.Y, 0); - } - - /// - /// It's the end of the world as we know it - /// - public Matrix4 EndWorldMatrix() - { - return Matrix4.CreateRotationZ(Theta) * Matrix4.CreateTranslation(EndPoint.X, EndPoint.Y, 0); - } - - public object Clone() - { - return MemberwiseClone(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; + +namespace osu.Framework.Graphics.Primitives +{ + /// + /// Represents a single line segment. Drawing is handled by the LineManager class. + /// + public class Line + { + /// + /// Begin point of the line. + /// + public Vector2 StartPoint; + + /// + /// End point of the line. + /// + public Vector2 EndPoint; + + /// + /// The length of the line. + /// + public float Rho => (EndPoint - StartPoint).Length; + + /// + /// The direction of the second point from the first. + /// + public float Theta => (float)Math.Atan2(EndPoint.Y - StartPoint.Y, EndPoint.X - StartPoint.X); + + public Vector2 Direction => (EndPoint - StartPoint).Normalized(); + + public Vector2 OrthogonalDirection + { + get + { + Vector2 dir = Direction; + return new Vector2(-dir.Y, dir.X); + } + } + + + public Line(Vector2 p1, Vector2 p2) + { + StartPoint = p1; + EndPoint = p2; + } + + /// + /// Distance squared from an arbitrary point p to this line. + /// + public float DistanceSquaredToPoint(Vector2 p) + { + return Vector2Extensions.DistanceSquared(p, ClosestPointTo(p)); + } + + /// + /// Distance from an arbitrary point to this line. + /// + public float DistanceToPoint(Vector2 p) + { + return Vector2Extensions.Distance(p, ClosestPointTo(p)); + } + + /// + /// Finds the point closest to the given point on this line. + /// + /// + /// See http://geometryalgorithms.com/Archive/algorithm_0102/algorithm_0102.htm, near the bottom. + /// + public Vector2 ClosestPointTo(Vector2 p) + { + Vector2 v = EndPoint - StartPoint; // Vector from line's p1 to p2 + Vector2 w = p - StartPoint; // Vector from line's p1 to p + + // See if p is closer to p1 than to the segment + float c1 = Vector2.Dot(w, v); + if (c1 <= 0) + return StartPoint; + + // See if p is closer to p2 than to the segment + float c2 = Vector2.Dot(v, v); + if (c2 <= c1) + return EndPoint; + + // p is closest to point pB, between p1 and p2 + float b = c1 / c2; + Vector2 pB = StartPoint + b * v; + + return pB; + } + + public Matrix4 WorldMatrix() + { + return Matrix4.CreateRotationZ(Theta) * Matrix4.CreateTranslation(StartPoint.X, StartPoint.Y, 0); + } + + /// + /// It's the end of the world as we know it + /// + public Matrix4 EndWorldMatrix() + { + return Matrix4.CreateRotationZ(Theta) * Matrix4.CreateTranslation(EndPoint.X, EndPoint.Y, 0); + } + + public object Clone() + { + return MemberwiseClone(); + } + } +} diff --git a/osu.Framework/Graphics/Primitives/ProjectionRange.cs b/osu.Framework/Graphics/Primitives/ProjectionRange.cs index bb792af25..1ffa97d58 100644 --- a/osu.Framework/Graphics/Primitives/ProjectionRange.cs +++ b/osu.Framework/Graphics/Primitives/ProjectionRange.cs @@ -1,55 +1,55 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Graphics.Primitives -{ - /// - /// A structure that tells how "far" along an axis - /// the projection of vertices onto the axis would be. - /// - internal struct ProjectionRange - { - /// - /// The minimum projected value. - /// - public float Min { get; } - - /// - /// The maximum projected value. - /// - public float Max { get; } - - public ProjectionRange(Vector2 axis, Vector2[] vertices) - { - Min = 0; - Max = 0; - - if (vertices.Length == 0) - return; - - Min = Vector2.Dot(axis, vertices[0]); - Max = Min; - - for (int i = 1; i < vertices.Length; i++) - { - float val = Vector2.Dot(axis, vertices[i]); - if (val < Min) - Min = val; - if (val > Max) - Max = val; - } - } - - /// - /// Checks whether this range overlaps another range. - /// - /// The other range to test against. - /// Whether the two ranges overlap. - public bool Overlaps(ProjectionRange other) - { - return Min <= other.Max && Max >= other.Min; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Graphics.Primitives +{ + /// + /// A structure that tells how "far" along an axis + /// the projection of vertices onto the axis would be. + /// + internal struct ProjectionRange + { + /// + /// The minimum projected value. + /// + public float Min { get; } + + /// + /// The maximum projected value. + /// + public float Max { get; } + + public ProjectionRange(Vector2 axis, Vector2[] vertices) + { + Min = 0; + Max = 0; + + if (vertices.Length == 0) + return; + + Min = Vector2.Dot(axis, vertices[0]); + Max = Min; + + for (int i = 1; i < vertices.Length; i++) + { + float val = Vector2.Dot(axis, vertices[i]); + if (val < Min) + Min = val; + if (val > Max) + Max = val; + } + } + + /// + /// Checks whether this range overlaps another range. + /// + /// The other range to test against. + /// Whether the two ranges overlap. + public bool Overlaps(ProjectionRange other) + { + return Min <= other.Max && Max >= other.Min; + } + } +} diff --git a/osu.Framework/Graphics/Primitives/Quad.cs b/osu.Framework/Graphics/Primitives/Quad.cs index 6401a2fa6..4596eedaa 100644 --- a/osu.Framework/Graphics/Primitives/Quad.cs +++ b/osu.Framework/Graphics/Primitives/Quad.cs @@ -1,167 +1,167 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Extensions.PolygonExtensions; -using OpenTK; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics.Primitives -{ - public struct Quad : IConvexPolygon, IEquatable - { - public Vector2 TopLeft; - public Vector2 TopRight; - public Vector2 BottomLeft; - public Vector2 BottomRight; - - public Quad(Vector2 topLeft, Vector2 topRight, Vector2 bottomLeft, Vector2 bottomRight) - { - TopLeft = topLeft; - TopRight = topRight; - BottomLeft = bottomLeft; - BottomRight = bottomRight; - } - - public Quad(float x, float y, float width, float height) - : this() - { - TopLeft = new Vector2(x, y); - TopRight = new Vector2(x + width, y); - BottomLeft = new Vector2(x, y + height); - BottomRight = new Vector2(x + width, y + height); - } - - public static implicit operator Quad(RectangleI r) => FromRectangle(r); - public static implicit operator Quad(RectangleF r) => FromRectangle(r); - - public static Quad FromRectangle(RectangleF rectangle) - { - return new Quad(new Vector2(rectangle.Left, rectangle.Top), - new Vector2(rectangle.Right, rectangle.Top), - new Vector2(rectangle.Left, rectangle.Bottom), - new Vector2(rectangle.Right, rectangle.Bottom)); - } - - public static Quad operator *(Quad r, Matrix3 m) - { - return new Quad( - Vector2Extensions.Transform(r.TopLeft, m), - Vector2Extensions.Transform(r.TopRight, m), - Vector2Extensions.Transform(r.BottomLeft, m), - Vector2Extensions.Transform(r.BottomRight, m)); - } - - public Matrix2 BasisTransform - { - get - { - Vector2 row0 = TopRight - TopLeft; - Vector2 row1 = BottomLeft - TopLeft; - - if (row0 != Vector2.Zero) - row0 /= row0.LengthSquared; - - if (row1 != Vector2.Zero) - row1 /= row1.LengthSquared; - - return new Matrix2( - row0.X, row0.Y, - row1.X, row1.Y); - } - } - - public Vector2 Centre => (TopLeft + TopRight + BottomLeft + BottomRight) / 4; - public Vector2 Size => new Vector2(Width, Height); - - public float Width => Vector2Extensions.Distance(TopLeft, TopRight); - public float Height => Vector2Extensions.Distance(TopLeft, BottomLeft); - - public RectangleI AABB - { - get - { - int xMin = (int)Math.Floor(Math.Min(TopLeft.X, Math.Min(TopRight.X, Math.Min(BottomLeft.X, BottomRight.X)))); - int yMin = (int)Math.Floor(Math.Min(TopLeft.Y, Math.Min(TopRight.Y, Math.Min(BottomLeft.Y, BottomRight.Y)))); - int xMax = (int)Math.Ceiling(Math.Max(TopLeft.X, Math.Max(TopRight.X, Math.Max(BottomLeft.X, BottomRight.X)))); - int yMax = (int)Math.Ceiling(Math.Max(TopLeft.Y, Math.Max(TopRight.Y, Math.Max(BottomLeft.Y, BottomRight.Y)))); - - return new RectangleI(xMin, yMin, xMax - xMin, yMax - yMin); - } - } - - public RectangleF AABBFloat - { - get - { - float xMin = Math.Min(TopLeft.X, Math.Min(TopRight.X, Math.Min(BottomLeft.X, BottomRight.X))); - float yMin = Math.Min(TopLeft.Y, Math.Min(TopRight.Y, Math.Min(BottomLeft.Y, BottomRight.Y))); - float xMax = Math.Max(TopLeft.X, Math.Max(TopRight.X, Math.Max(BottomLeft.X, BottomRight.X))); - float yMax = Math.Max(TopLeft.Y, Math.Max(TopRight.Y, Math.Max(BottomLeft.Y, BottomRight.Y))); - - return new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin); - } - } - - public Vector2[] Vertices => new[] { TopLeft, TopRight, BottomRight, BottomLeft }; - public Vector2[] AxisVertices => Vertices; - - public bool Contains(Vector2 pos) - { - return - new Triangle(BottomRight, BottomLeft, TopRight).Contains(pos) || - new Triangle(TopLeft, TopRight, BottomLeft).Contains(pos); - } - - public float Area => new Triangle(BottomRight, BottomLeft, TopRight).Area + new Triangle(TopLeft, TopRight, BottomLeft).Area; - - public float ConservativeArea - { - get - { - if (Precision.AlmostEquals(TopLeft.Y, TopRight.Y)) - return Math.Abs((TopLeft.Y - BottomLeft.Y) * (TopLeft.X - TopRight.X)); - - // Uncomment this to speed this computation up at the cost of losing accuracy when considering shearing. - //return Math.Sqrt(Vector2Extensions.DistanceSquared(TopLeft, TopRight) * Vector2Extensions.DistanceSquared(TopLeft, BottomLeft)); - - Vector2 d1 = TopLeft - TopRight; - float lsq1 = d1.LengthSquared; - - Vector2 d2 = TopLeft - BottomLeft; - float lsq2 = Vector2Extensions.DistanceSquared(d2, d1 * Vector2.Dot(d2, d1 * MathHelper.InverseSqrtFast(lsq1))); - - return (float)Math.Sqrt(lsq1 * lsq2); - } - } - - public bool Intersects(IConvexPolygon other) - { - return (this as IConvexPolygon).Intersects(other); - } - - public bool Equals(Quad other) - { - return - TopLeft == other.TopLeft && - TopRight == other.TopRight && - BottomLeft == other.BottomLeft && - BottomRight == other.BottomRight; - } - - public bool AlmostEquals(Quad other) - { - return - Precision.AlmostEquals(TopLeft.X, other.TopLeft.X) && - Precision.AlmostEquals(TopLeft.Y, other.TopLeft.Y) && - Precision.AlmostEquals(TopRight.X, other.TopRight.X) && - Precision.AlmostEquals(TopRight.Y, other.TopRight.Y) && - Precision.AlmostEquals(BottomLeft.X, other.BottomLeft.X) && - Precision.AlmostEquals(BottomLeft.Y, other.BottomLeft.Y) && - Precision.AlmostEquals(BottomRight.X, other.BottomRight.X) && - Precision.AlmostEquals(BottomRight.Y, other.BottomRight.Y); - } - - public override string ToString() => $"{TopLeft} {TopRight} {BottomLeft} {BottomRight}"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Extensions.PolygonExtensions; +using OpenTK; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics.Primitives +{ + public struct Quad : IConvexPolygon, IEquatable + { + public Vector2 TopLeft; + public Vector2 TopRight; + public Vector2 BottomLeft; + public Vector2 BottomRight; + + public Quad(Vector2 topLeft, Vector2 topRight, Vector2 bottomLeft, Vector2 bottomRight) + { + TopLeft = topLeft; + TopRight = topRight; + BottomLeft = bottomLeft; + BottomRight = bottomRight; + } + + public Quad(float x, float y, float width, float height) + : this() + { + TopLeft = new Vector2(x, y); + TopRight = new Vector2(x + width, y); + BottomLeft = new Vector2(x, y + height); + BottomRight = new Vector2(x + width, y + height); + } + + public static implicit operator Quad(RectangleI r) => FromRectangle(r); + public static implicit operator Quad(RectangleF r) => FromRectangle(r); + + public static Quad FromRectangle(RectangleF rectangle) + { + return new Quad(new Vector2(rectangle.Left, rectangle.Top), + new Vector2(rectangle.Right, rectangle.Top), + new Vector2(rectangle.Left, rectangle.Bottom), + new Vector2(rectangle.Right, rectangle.Bottom)); + } + + public static Quad operator *(Quad r, Matrix3 m) + { + return new Quad( + Vector2Extensions.Transform(r.TopLeft, m), + Vector2Extensions.Transform(r.TopRight, m), + Vector2Extensions.Transform(r.BottomLeft, m), + Vector2Extensions.Transform(r.BottomRight, m)); + } + + public Matrix2 BasisTransform + { + get + { + Vector2 row0 = TopRight - TopLeft; + Vector2 row1 = BottomLeft - TopLeft; + + if (row0 != Vector2.Zero) + row0 /= row0.LengthSquared; + + if (row1 != Vector2.Zero) + row1 /= row1.LengthSquared; + + return new Matrix2( + row0.X, row0.Y, + row1.X, row1.Y); + } + } + + public Vector2 Centre => (TopLeft + TopRight + BottomLeft + BottomRight) / 4; + public Vector2 Size => new Vector2(Width, Height); + + public float Width => Vector2Extensions.Distance(TopLeft, TopRight); + public float Height => Vector2Extensions.Distance(TopLeft, BottomLeft); + + public RectangleI AABB + { + get + { + int xMin = (int)Math.Floor(Math.Min(TopLeft.X, Math.Min(TopRight.X, Math.Min(BottomLeft.X, BottomRight.X)))); + int yMin = (int)Math.Floor(Math.Min(TopLeft.Y, Math.Min(TopRight.Y, Math.Min(BottomLeft.Y, BottomRight.Y)))); + int xMax = (int)Math.Ceiling(Math.Max(TopLeft.X, Math.Max(TopRight.X, Math.Max(BottomLeft.X, BottomRight.X)))); + int yMax = (int)Math.Ceiling(Math.Max(TopLeft.Y, Math.Max(TopRight.Y, Math.Max(BottomLeft.Y, BottomRight.Y)))); + + return new RectangleI(xMin, yMin, xMax - xMin, yMax - yMin); + } + } + + public RectangleF AABBFloat + { + get + { + float xMin = Math.Min(TopLeft.X, Math.Min(TopRight.X, Math.Min(BottomLeft.X, BottomRight.X))); + float yMin = Math.Min(TopLeft.Y, Math.Min(TopRight.Y, Math.Min(BottomLeft.Y, BottomRight.Y))); + float xMax = Math.Max(TopLeft.X, Math.Max(TopRight.X, Math.Max(BottomLeft.X, BottomRight.X))); + float yMax = Math.Max(TopLeft.Y, Math.Max(TopRight.Y, Math.Max(BottomLeft.Y, BottomRight.Y))); + + return new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin); + } + } + + public Vector2[] Vertices => new[] { TopLeft, TopRight, BottomRight, BottomLeft }; + public Vector2[] AxisVertices => Vertices; + + public bool Contains(Vector2 pos) + { + return + new Triangle(BottomRight, BottomLeft, TopRight).Contains(pos) || + new Triangle(TopLeft, TopRight, BottomLeft).Contains(pos); + } + + public float Area => new Triangle(BottomRight, BottomLeft, TopRight).Area + new Triangle(TopLeft, TopRight, BottomLeft).Area; + + public float ConservativeArea + { + get + { + if (Precision.AlmostEquals(TopLeft.Y, TopRight.Y)) + return Math.Abs((TopLeft.Y - BottomLeft.Y) * (TopLeft.X - TopRight.X)); + + // Uncomment this to speed this computation up at the cost of losing accuracy when considering shearing. + //return Math.Sqrt(Vector2Extensions.DistanceSquared(TopLeft, TopRight) * Vector2Extensions.DistanceSquared(TopLeft, BottomLeft)); + + Vector2 d1 = TopLeft - TopRight; + float lsq1 = d1.LengthSquared; + + Vector2 d2 = TopLeft - BottomLeft; + float lsq2 = Vector2Extensions.DistanceSquared(d2, d1 * Vector2.Dot(d2, d1 * MathHelper.InverseSqrtFast(lsq1))); + + return (float)Math.Sqrt(lsq1 * lsq2); + } + } + + public bool Intersects(IConvexPolygon other) + { + return (this as IConvexPolygon).Intersects(other); + } + + public bool Equals(Quad other) + { + return + TopLeft == other.TopLeft && + TopRight == other.TopRight && + BottomLeft == other.BottomLeft && + BottomRight == other.BottomRight; + } + + public bool AlmostEquals(Quad other) + { + return + Precision.AlmostEquals(TopLeft.X, other.TopLeft.X) && + Precision.AlmostEquals(TopLeft.Y, other.TopLeft.Y) && + Precision.AlmostEquals(TopRight.X, other.TopRight.X) && + Precision.AlmostEquals(TopRight.Y, other.TopRight.Y) && + Precision.AlmostEquals(BottomLeft.X, other.BottomLeft.X) && + Precision.AlmostEquals(BottomLeft.Y, other.BottomLeft.Y) && + Precision.AlmostEquals(BottomRight.X, other.BottomRight.X) && + Precision.AlmostEquals(BottomRight.Y, other.BottomRight.Y); + } + + public override string ToString() => $"{TopLeft} {TopRight} {BottomLeft} {BottomRight}"; + } +} diff --git a/osu.Framework/Graphics/Primitives/RectangleF.cs b/osu.Framework/Graphics/Primitives/RectangleF.cs index d434e807d..4ac5cf712 100644 --- a/osu.Framework/Graphics/Primitives/RectangleF.cs +++ b/osu.Framework/Graphics/Primitives/RectangleF.cs @@ -1,342 +1,342 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.ComponentModel; -using System.Globalization; -using System.Runtime.InteropServices; -using OpenTK; - -namespace osu.Framework.Graphics.Primitives -{ - /// Stores a set of four floating-point numbers that represent the location and size of a rectangle. For more advanced region functions, use a object. - /// 1 - [Serializable, StructLayout(LayoutKind.Sequential)] - public struct RectangleF : IEquatable - { - /// Represents an instance of the class with its members uninitialized. - /// 1 - public static readonly RectangleF Empty; - - public float X; - public float Y; - - public float Width; - public float Height; - - /// Initializes a new instance of the class with the specified location and size. - /// The y-coordinate of the upper-left corner of the rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// The x-coordinate of the upper-left corner of the rectangle. - public RectangleF(float x, float y, float width, float height) - { - X = x; - Y = y; - Width = width; - Height = height; - } - - /// Initializes a new instance of the class with the specified location and size. - /// A that represents the width and height of the rectangular region. - /// A that represents the upper-left corner of the rectangular region. - public RectangleF(Vector2 location, Vector2 size) - { - X = location.X; - Y = location.Y; - Width = size.X; - Height = size.Y; - } - - /// Gets or sets the coordinates of the upper-left corner of this structure. - /// A that represents the upper-left corner of this structure. - /// 1 - [Browsable(false)] - public Vector2 Location - { - get { return new Vector2(X, Y); } - set - { - X = value.X; - Y = value.Y; - } - } - - /// Gets or sets the size of this . - /// A that represents the width and height of this structure. - /// 1 - [Browsable(false)] - public Vector2 Size - { - get { return new Vector2(Width, Height); } - set - { - Width = value.X; - Height = value.Y; - } - } - - /// Gets the y-coordinate of the top edge of this structure. - /// The y-coordinate of the top edge of this structure. - /// 1 - [Browsable(false)] - public float Left => X; - - /// Gets the y-coordinate of the top edge of this structure. - /// The y-coordinate of the top edge of this structure. - /// 1 - [Browsable(false)] - public float Top => Y; - - /// Gets the x-coordinate that is the sum of and of this structure. - /// The x-coordinate that is the sum of and of this structure. - /// 1 - [Browsable(false)] - public float Right => X + Width; - - /// Gets the y-coordinate that is the sum of and of this structure. - /// The y-coordinate that is the sum of and of this structure. - /// 1 - [Browsable(false)] - public float Bottom => Y + Height; - - [Browsable(false)] - public Vector2 TopLeft => new Vector2(Left, Top); - - [Browsable(false)] - public Vector2 TopRight => new Vector2(Right, Top); - - [Browsable(false)] - public Vector2 BottomLeft => new Vector2(Left, Bottom); - - [Browsable(false)] - public Vector2 BottomRight => new Vector2(Right, Bottom); - - [Browsable(false)] - public Vector2 Centre => new Vector2(X + Width / 2, Y + Height / 2); - - /// Tests whether the or property of this has a value of zero. - /// This property returns true if the or property of this has a value of zero; otherwise, false. - /// 1 - [Browsable(false)] - public bool IsEmpty => Width <= 0 || Height <= 0; - - /// Tests whether obj is a with the same location and size of this . - /// This method returns true if obj is a and its X, Y, Width, and Height properties are equal to the corresponding properties of this ; otherwise, false. - /// The to test. - /// 1 - public override bool Equals(object obj) - { - if (!(obj is RectangleF)) - return false; - RectangleF ef = (RectangleF)obj; - return ef.X == X && ef.Y == Y && ef.Width == Width && ef.Height == Height; - } - - /// Tests whether two structures have equal location and size. - /// This operator returns true if the two specified structures have equal , , , and properties. - /// The structure that is to the right of the equality operator. - /// The structure that is to the left of the equality operator. - /// 3 - public static bool operator ==(RectangleF left, RectangleF right) => left.Equals(right); - - /// Tests whether two structures differ in location or size. - /// This operator returns true if any of the , , , or properties of the two structures are unequal; otherwise false. - /// The structure that is to the right of the inequality operator. - /// The structure that is to the left of the inequality operator. - /// 3 - public static bool operator !=(RectangleF left, RectangleF right) => !(left == right); - - public static RectangleF operator *(RectangleF left, float right) => new RectangleF(left.X * right, left.Y * right, left.Width * right, left.Height * right); - - public static RectangleF operator /(RectangleF left, float right) => new RectangleF(left.X / right, left.Y / right, left.Width / right, left.Height / right); - - /// Determines if the specified point is contained within this structure. - /// This method returns true if the point defined by x and y is contained within this structure; otherwise false. - /// The y-coordinate of the point to test. - /// The x-coordinate of the point to test. - /// 1 - public bool Contains(float x, float y) => X <= x && x < X + Width && Y <= y && y < Y + Height; - - public bool Contains(Vector2 pt) => Contains(pt.X, pt.Y); - - /// Determines if the specified point is contained within this structure. - /// This method returns true if the point represented by the pt parameter is contained within this structure; otherwise false. - /// The to test. - /// 1 - public bool Contains(Vector2I pt) => Contains(pt.X, pt.Y); - - /// Determines if the rectangular region represented by rect is entirely contained within this structure. - /// This method returns true if the rectangular region represented by rect is entirely contained within the rectangular region represented by this ; otherwise false. - /// The to test. - /// 1 - public bool Contains(RectangleF rect) => - X <= rect.X && rect.X + rect.Width <= X + Width && Y <= rect.Y && - rect.Y + rect.Height <= Y + Height; - - /// Gets the hash code for this structure. For information about the use of hash codes, see Object.GetHashCode. - /// The hash code for this . - /// 1 - public override int GetHashCode() - { - // ReSharper disable NonReadonlyMemberInGetHashCode - return - (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13 ^ - (uint)Width << 0x1a | (uint)Width >> 6 ^ - (uint)Height << 7 | (uint)Height >> 0x19); - // ReSharper restore NonReadonlyMemberInGetHashCode - } - - public float Area => Width * Height; - - public RectangleF WithPositiveExtent - { - get - { - RectangleF result = this; - - if (result.Width < 0) - { - result.Width = -result.Width; - result.X -= result.Width; - } - - if (Height < 0) - { - result.Height = -result.Height; - result.Y -= result.Height; - } - - return result; - } - } - - public RectangleF Inflate(float amount) => Inflate(new Vector2(amount, amount)); - - public RectangleF Inflate(Vector2 amount) => Inflate(new MarginPadding { Left = amount.X, Right = amount.X, Top = amount.Y, Bottom = amount.Y }); - - public RectangleF Inflate(MarginPadding amount) => new RectangleF( - X - amount.Left, - Y - amount.Top, - Width + amount.TotalHorizontal, - Height + amount.TotalVertical); - - public RectangleF Shrink(float amount) => Shrink(new Vector2(amount, amount)); - - public RectangleF Shrink(Vector2 amount) => Shrink(new MarginPadding { Left = amount.X, Right = amount.X, Top = amount.Y, Bottom = amount.Y }); - - public RectangleF Shrink(MarginPadding amount) => Inflate(-amount); - - /// Replaces this structure with the intersection of itself and the specified structure. - /// This method does not return a value. - /// The rectangle to intersect. - /// 1 - public void Intersect(RectangleF rect) - { - RectangleF ef = Intersect(rect, this); - X = ef.X; - Y = ef.Y; - Width = ef.Width; - Height = ef.Height; - } - - /// Returns a structure that represents the intersection of two rectangles. If there is no intersection, and empty is returned. - /// A third structure the size of which represents the overlapped area of the two specified rectangles. - /// A rectangle to intersect. - /// A rectangle to intersect. - /// 1 - public static RectangleF Intersect(RectangleF a, RectangleF b) - { - float x = Math.Max(a.X, b.X); - float num2 = Math.Min(a.X + a.Width, b.X + b.Width); - float y = Math.Max(a.Y, b.Y); - float num4 = Math.Min(a.Y + a.Height, b.Y + b.Height); - if (num2 >= x && num4 >= y) - return new RectangleF(x, y, num2 - x, num4 - y); - return Empty; - } - - /// Determines if this rectangle intersects with rect. - /// This method returns true if there is any intersection. - /// The rectangle to test. - /// 1 - public bool IntersectsWith(RectangleF rect) => - rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; - - /// Determines if this rectangle intersects with rect. - /// This method returns true if there is any intersection. - /// The rectangle to test. - /// 1 - public bool IntersectsWith(RectangleI rect) => - rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; - - /// Creates the smallest possible third rectangle that can contain both of two rectangles that form a union. - /// A third structure that contains both of the two rectangles that form the union. - /// A rectangle to union. - /// A rectangle to union. - /// 1 - public static RectangleF Union(RectangleF a, RectangleF b) - { - float x = Math.Min(a.X, b.X); - float num2 = Math.Max(a.X + a.Width, b.X + b.Width); - float y = Math.Min(a.Y, b.Y); - float num4 = Math.Max(a.Y + a.Height, b.Y + b.Height); - return new RectangleF(x, y, num2 - x, num4 - y); - } - - /// Adjusts the location of this rectangle by the specified amount. - /// This method does not return a value. - /// The amount to offset the location. - /// 1 - public RectangleF Offset(Vector2 pos) => Offset(pos.X, pos.Y); - - /// Adjusts the location of this rectangle by the specified amount. - /// This method does not return a value. - /// The amount to offset the location vertically. - /// The amount to offset the location horizontally. - /// 1 - public RectangleF Offset(float x, float y) => new RectangleF(X + x, Y + y, Width, Height); - - internal float DistanceSquared(Vector2 localSpacePos) - { - Vector2 dist = new Vector2( - Math.Max(0.0f, Math.Max(localSpacePos.X - Right, Left - localSpacePos.X)), - Math.Max(0.0f, Math.Max(localSpacePos.Y - Bottom, Top - localSpacePos.Y)) - ); - - return dist.LengthSquared; - } - - // This could be optimized further in the future, but made for a simple implementation right now. - public RectangleI AABB => ((Quad)this).AABB; - - /// - /// Constructs a from left, top, right, and bottom coordinates. - /// - /// The left coordinate. - /// The top coordinate. - /// The right coordinate. - /// The bottom coordinate. - /// The . - public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new RectangleF(left, top, right - left, bottom - top); - - /// Converts the specified structure to a structure. - /// The structure that is converted from the specified structure. - /// The structure to convert. - /// 3 - public static implicit operator RectangleF(RectangleI r) => new RectangleF(r.X, r.Y, r.Width, r.Height); - - public static implicit operator System.Drawing.RectangleF(RectangleF r) => new System.Drawing.RectangleF(r.X, r.Y, r.Width, r.Height); - - /// Converts the Location and of this to a human-readable string. - /// A string that contains the position, width, and height of this structure¾for example, "{X=20, Y=20, Width=100, Height=50}". - /// 1 - /// - public override string ToString() => $"X={Math.Round(X, 3).ToString(CultureInfo.CurrentCulture)}, " - + $"Y={Math.Round(Y, 3).ToString(CultureInfo.CurrentCulture)}, " - + $"Width={Math.Round(Width, 3).ToString(CultureInfo.CurrentCulture)}, " - + $"Height={Math.Round(Height, 3).ToString(CultureInfo.CurrentCulture)}"; - - public bool Equals(RectangleF other) => X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; +using OpenTK; + +namespace osu.Framework.Graphics.Primitives +{ + /// Stores a set of four floating-point numbers that represent the location and size of a rectangle. For more advanced region functions, use a object. + /// 1 + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct RectangleF : IEquatable + { + /// Represents an instance of the class with its members uninitialized. + /// 1 + public static readonly RectangleF Empty; + + public float X; + public float Y; + + public float Width; + public float Height; + + /// Initializes a new instance of the class with the specified location and size. + /// The y-coordinate of the upper-left corner of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + /// The x-coordinate of the upper-left corner of the rectangle. + public RectangleF(float x, float y, float width, float height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + /// Initializes a new instance of the class with the specified location and size. + /// A that represents the width and height of the rectangular region. + /// A that represents the upper-left corner of the rectangular region. + public RectangleF(Vector2 location, Vector2 size) + { + X = location.X; + Y = location.Y; + Width = size.X; + Height = size.Y; + } + + /// Gets or sets the coordinates of the upper-left corner of this structure. + /// A that represents the upper-left corner of this structure. + /// 1 + [Browsable(false)] + public Vector2 Location + { + get { return new Vector2(X, Y); } + set + { + X = value.X; + Y = value.Y; + } + } + + /// Gets or sets the size of this . + /// A that represents the width and height of this structure. + /// 1 + [Browsable(false)] + public Vector2 Size + { + get { return new Vector2(Width, Height); } + set + { + Width = value.X; + Height = value.Y; + } + } + + /// Gets the y-coordinate of the top edge of this structure. + /// The y-coordinate of the top edge of this structure. + /// 1 + [Browsable(false)] + public float Left => X; + + /// Gets the y-coordinate of the top edge of this structure. + /// The y-coordinate of the top edge of this structure. + /// 1 + [Browsable(false)] + public float Top => Y; + + /// Gets the x-coordinate that is the sum of and of this structure. + /// The x-coordinate that is the sum of and of this structure. + /// 1 + [Browsable(false)] + public float Right => X + Width; + + /// Gets the y-coordinate that is the sum of and of this structure. + /// The y-coordinate that is the sum of and of this structure. + /// 1 + [Browsable(false)] + public float Bottom => Y + Height; + + [Browsable(false)] + public Vector2 TopLeft => new Vector2(Left, Top); + + [Browsable(false)] + public Vector2 TopRight => new Vector2(Right, Top); + + [Browsable(false)] + public Vector2 BottomLeft => new Vector2(Left, Bottom); + + [Browsable(false)] + public Vector2 BottomRight => new Vector2(Right, Bottom); + + [Browsable(false)] + public Vector2 Centre => new Vector2(X + Width / 2, Y + Height / 2); + + /// Tests whether the or property of this has a value of zero. + /// This property returns true if the or property of this has a value of zero; otherwise, false. + /// 1 + [Browsable(false)] + public bool IsEmpty => Width <= 0 || Height <= 0; + + /// Tests whether obj is a with the same location and size of this . + /// This method returns true if obj is a and its X, Y, Width, and Height properties are equal to the corresponding properties of this ; otherwise, false. + /// The to test. + /// 1 + public override bool Equals(object obj) + { + if (!(obj is RectangleF)) + return false; + RectangleF ef = (RectangleF)obj; + return ef.X == X && ef.Y == Y && ef.Width == Width && ef.Height == Height; + } + + /// Tests whether two structures have equal location and size. + /// This operator returns true if the two specified structures have equal , , , and properties. + /// The structure that is to the right of the equality operator. + /// The structure that is to the left of the equality operator. + /// 3 + public static bool operator ==(RectangleF left, RectangleF right) => left.Equals(right); + + /// Tests whether two structures differ in location or size. + /// This operator returns true if any of the , , , or properties of the two structures are unequal; otherwise false. + /// The structure that is to the right of the inequality operator. + /// The structure that is to the left of the inequality operator. + /// 3 + public static bool operator !=(RectangleF left, RectangleF right) => !(left == right); + + public static RectangleF operator *(RectangleF left, float right) => new RectangleF(left.X * right, left.Y * right, left.Width * right, left.Height * right); + + public static RectangleF operator /(RectangleF left, float right) => new RectangleF(left.X / right, left.Y / right, left.Width / right, left.Height / right); + + /// Determines if the specified point is contained within this structure. + /// This method returns true if the point defined by x and y is contained within this structure; otherwise false. + /// The y-coordinate of the point to test. + /// The x-coordinate of the point to test. + /// 1 + public bool Contains(float x, float y) => X <= x && x < X + Width && Y <= y && y < Y + Height; + + public bool Contains(Vector2 pt) => Contains(pt.X, pt.Y); + + /// Determines if the specified point is contained within this structure. + /// This method returns true if the point represented by the pt parameter is contained within this structure; otherwise false. + /// The to test. + /// 1 + public bool Contains(Vector2I pt) => Contains(pt.X, pt.Y); + + /// Determines if the rectangular region represented by rect is entirely contained within this structure. + /// This method returns true if the rectangular region represented by rect is entirely contained within the rectangular region represented by this ; otherwise false. + /// The to test. + /// 1 + public bool Contains(RectangleF rect) => + X <= rect.X && rect.X + rect.Width <= X + Width && Y <= rect.Y && + rect.Y + rect.Height <= Y + Height; + + /// Gets the hash code for this structure. For information about the use of hash codes, see Object.GetHashCode. + /// The hash code for this . + /// 1 + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return + (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13 ^ + (uint)Width << 0x1a | (uint)Width >> 6 ^ + (uint)Height << 7 | (uint)Height >> 0x19); + // ReSharper restore NonReadonlyMemberInGetHashCode + } + + public float Area => Width * Height; + + public RectangleF WithPositiveExtent + { + get + { + RectangleF result = this; + + if (result.Width < 0) + { + result.Width = -result.Width; + result.X -= result.Width; + } + + if (Height < 0) + { + result.Height = -result.Height; + result.Y -= result.Height; + } + + return result; + } + } + + public RectangleF Inflate(float amount) => Inflate(new Vector2(amount, amount)); + + public RectangleF Inflate(Vector2 amount) => Inflate(new MarginPadding { Left = amount.X, Right = amount.X, Top = amount.Y, Bottom = amount.Y }); + + public RectangleF Inflate(MarginPadding amount) => new RectangleF( + X - amount.Left, + Y - amount.Top, + Width + amount.TotalHorizontal, + Height + amount.TotalVertical); + + public RectangleF Shrink(float amount) => Shrink(new Vector2(amount, amount)); + + public RectangleF Shrink(Vector2 amount) => Shrink(new MarginPadding { Left = amount.X, Right = amount.X, Top = amount.Y, Bottom = amount.Y }); + + public RectangleF Shrink(MarginPadding amount) => Inflate(-amount); + + /// Replaces this structure with the intersection of itself and the specified structure. + /// This method does not return a value. + /// The rectangle to intersect. + /// 1 + public void Intersect(RectangleF rect) + { + RectangleF ef = Intersect(rect, this); + X = ef.X; + Y = ef.Y; + Width = ef.Width; + Height = ef.Height; + } + + /// Returns a structure that represents the intersection of two rectangles. If there is no intersection, and empty is returned. + /// A third structure the size of which represents the overlapped area of the two specified rectangles. + /// A rectangle to intersect. + /// A rectangle to intersect. + /// 1 + public static RectangleF Intersect(RectangleF a, RectangleF b) + { + float x = Math.Max(a.X, b.X); + float num2 = Math.Min(a.X + a.Width, b.X + b.Width); + float y = Math.Max(a.Y, b.Y); + float num4 = Math.Min(a.Y + a.Height, b.Y + b.Height); + if (num2 >= x && num4 >= y) + return new RectangleF(x, y, num2 - x, num4 - y); + return Empty; + } + + /// Determines if this rectangle intersects with rect. + /// This method returns true if there is any intersection. + /// The rectangle to test. + /// 1 + public bool IntersectsWith(RectangleF rect) => + rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; + + /// Determines if this rectangle intersects with rect. + /// This method returns true if there is any intersection. + /// The rectangle to test. + /// 1 + public bool IntersectsWith(RectangleI rect) => + rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; + + /// Creates the smallest possible third rectangle that can contain both of two rectangles that form a union. + /// A third structure that contains both of the two rectangles that form the union. + /// A rectangle to union. + /// A rectangle to union. + /// 1 + public static RectangleF Union(RectangleF a, RectangleF b) + { + float x = Math.Min(a.X, b.X); + float num2 = Math.Max(a.X + a.Width, b.X + b.Width); + float y = Math.Min(a.Y, b.Y); + float num4 = Math.Max(a.Y + a.Height, b.Y + b.Height); + return new RectangleF(x, y, num2 - x, num4 - y); + } + + /// Adjusts the location of this rectangle by the specified amount. + /// This method does not return a value. + /// The amount to offset the location. + /// 1 + public RectangleF Offset(Vector2 pos) => Offset(pos.X, pos.Y); + + /// Adjusts the location of this rectangle by the specified amount. + /// This method does not return a value. + /// The amount to offset the location vertically. + /// The amount to offset the location horizontally. + /// 1 + public RectangleF Offset(float x, float y) => new RectangleF(X + x, Y + y, Width, Height); + + internal float DistanceSquared(Vector2 localSpacePos) + { + Vector2 dist = new Vector2( + Math.Max(0.0f, Math.Max(localSpacePos.X - Right, Left - localSpacePos.X)), + Math.Max(0.0f, Math.Max(localSpacePos.Y - Bottom, Top - localSpacePos.Y)) + ); + + return dist.LengthSquared; + } + + // This could be optimized further in the future, but made for a simple implementation right now. + public RectangleI AABB => ((Quad)this).AABB; + + /// + /// Constructs a from left, top, right, and bottom coordinates. + /// + /// The left coordinate. + /// The top coordinate. + /// The right coordinate. + /// The bottom coordinate. + /// The . + public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new RectangleF(left, top, right - left, bottom - top); + + /// Converts the specified structure to a structure. + /// The structure that is converted from the specified structure. + /// The structure to convert. + /// 3 + public static implicit operator RectangleF(RectangleI r) => new RectangleF(r.X, r.Y, r.Width, r.Height); + + public static implicit operator System.Drawing.RectangleF(RectangleF r) => new System.Drawing.RectangleF(r.X, r.Y, r.Width, r.Height); + + /// Converts the Location and of this to a human-readable string. + /// A string that contains the position, width, and height of this structure¾for example, "{X=20, Y=20, Width=100, Height=50}". + /// 1 + /// + public override string ToString() => $"X={Math.Round(X, 3).ToString(CultureInfo.CurrentCulture)}, " + + $"Y={Math.Round(Y, 3).ToString(CultureInfo.CurrentCulture)}, " + + $"Width={Math.Round(Width, 3).ToString(CultureInfo.CurrentCulture)}, " + + $"Height={Math.Round(Height, 3).ToString(CultureInfo.CurrentCulture)}"; + + public bool Equals(RectangleF other) => X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; + } +} diff --git a/osu.Framework/Graphics/Primitives/RectangleI.cs b/osu.Framework/Graphics/Primitives/RectangleI.cs index bcb5994d3..6ac319355 100644 --- a/osu.Framework/Graphics/Primitives/RectangleI.cs +++ b/osu.Framework/Graphics/Primitives/RectangleI.cs @@ -1,270 +1,270 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.ComponentModel; -using System.Globalization; -using System.Runtime.InteropServices; -using OpenTK; - -namespace osu.Framework.Graphics.Primitives -{ - /// Stores a set of four integer numbers that represent the location and size of a rectangle. For more advanced region functions, use a object. - /// 1 - [Serializable, StructLayout(LayoutKind.Sequential)] - public struct RectangleI : IEquatable - { - /// Represents an instance of the class with its members uninitialized. - /// 1 - public static readonly RectangleI Empty; - - public int X; - public int Y; - - public int Width; - public int Height; - - /// Initializes a new instance of the class with the specified location and size. - /// The y-coordinate of the upper-left corner of the rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// The x-coordinate of the upper-left corner of the rectangle. - public RectangleI(int x, int y, int width, int height) - { - X = x; - Y = y; - Width = width; - Height = height; - } - - /// Gets or sets the coordinates of the upper-left corner of this structure. - /// A that represents the upper-left corner of this structure. - /// 1 - [Browsable(false)] - public Vector2I Location - { - get { return new Vector2I(X, Y); } - } - - /// Gets or sets the size of this . - /// A that represents the width and height of this structure. - /// 1 - [Browsable(false)] - public Vector2I Size - { - get { return new Vector2I(Width, Height); } - } - - /// Gets the y-coordinate of the top edge of this structure. - /// The y-coordinate of the top edge of this structure. - /// 1 - [Browsable(false)] - public int Left => X; - - /// Gets the y-coordinate of the top edge of this structure. - /// The y-coordinate of the top edge of this structure. - /// 1 - [Browsable(false)] - public int Top => Y; - - /// Gets the x-coordinate that is the sum of and of this structure. - /// The x-coordinate that is the sum of and of this structure. - /// 1 - [Browsable(false)] - public int Right => X + Width; - - /// Gets the y-coordinate that is the sum of and of this structure. - /// The y-coordinate that is the sum of and of this structure. - /// 1 - [Browsable(false)] - public int Bottom => Y + Height; - - [Browsable(false)] - public Vector2I TopLeft => new Vector2I(Left, Top); - - [Browsable(false)] - public Vector2I TopRight => new Vector2I(Right, Top); - - [Browsable(false)] - public Vector2I BottomLeft => new Vector2I(Left, Bottom); - - [Browsable(false)] - public Vector2I BottomRight => new Vector2I(Right, Bottom); - - /// Tests whether the or property of this has a value of zero. - /// This property returns true if the or property of this has a value of zero; otherwise, false. - /// 1 - [Browsable(false)] - public bool IsEmpty => Width <= 0 || Height <= 0; - - /// Tests whether obj is a with the same location and size of this . - /// This method returns true if obj is a and its X, Y, Width, and Height properties are equal to the corresponding properties of this ; otherwise, false. - /// The to test. - /// 1 - public override bool Equals(object obj) - { - if (!(obj is RectangleI)) - return false; - RectangleI ef = (RectangleI)obj; - return ef.X == X && ef.Y == Y && ef.Width == Width && ef.Height == Height; - } - - /// Tests whether two structures have equal location and size. - /// This operator returns true if the two specified structures have equal , , , and properties. - /// The structure that is to the right of the equality operator. - /// The structure that is to the left of the equality operator. - /// 3 - public static bool operator ==(RectangleI left, RectangleI right) => left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height; - - /// Tests whether two structures differ in location or size. - /// This operator returns true if any of the , , , or properties of the two structures are unequal; otherwise false. - /// The structure that is to the right of the inequality operator. - /// The structure that is to the left of the inequality operator. - /// 3 - public static bool operator !=(RectangleI left, RectangleI right) => !(left == right); - - /// Determines if the specified point is contained within this structure. - /// This method returns true if the point defined by x and y is contained within this structure; otherwise false. - /// The y-coordinate of the point to test. - /// The x-coordinate of the point to test. - /// 1 - public bool Contains(float x, float y) => X <= x && x < X + Width && Y <= y && y < Y + Height; - - public bool Contains(Vector2 pt) => Contains(pt.X, pt.Y); - - public bool Contains(int x, int y) => X <= x && x < X + Width && Y <= y && y < Y + Height; - - public bool Contains(Vector2I pt) => Contains(pt.X, pt.Y); - - /// Determines if the rectangular region represented by rect is entirely contained within this structure. - /// This method returns true if the rectangular region represented by rect is entirely contained within the rectangular region represented by this ; otherwise false. - /// The to test. - /// 1 - public bool Contains(RectangleI rect) => - X <= rect.X && rect.X + rect.Width <= X + Width && Y <= rect.Y && - rect.Y + rect.Height <= Y + Height; - - /// Gets the hash code for this structure. For information about the use of hash codes, see Object.GetHashCode. - /// The hash code for this . - /// 1 - public override int GetHashCode() - { - // ReSharper disable NonReadonlyMemberInGetHashCode - return - (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13 ^ - (uint)Width << 0x1a | (uint)Width >> 6 ^ - (uint)Height << 7 | (uint)Height >> 0x19); - // ReSharper restore NonReadonlyMemberInGetHashCode - } - - public int Area => Width * Height; - - public RectangleI WithPositiveExtent - { - get - { - RectangleI result = this; - - if (result.Width < 0) - { - result.Width = -result.Width; - result.X -= result.Width; - } - - if (Height < 0) - { - result.Height = -result.Height; - result.Y -= result.Height; - } - - return result; - } - } - - public RectangleI Inflate(int amount) => Inflate(new Vector2I(amount)); - - public RectangleI Inflate(Vector2I amount) => Inflate(amount.X, amount.X, amount.Y, amount.Y); - - public RectangleI Inflate(int left, int right, int top, int bottom) => new RectangleI( - X - left, - Y - top, - Width + left + right, - Height + top + bottom); - - public RectangleI Shrink(int amount) => Shrink(new Vector2I(amount)); - - public RectangleI Shrink(Vector2I amount) => Shrink(amount.X, amount.X, amount.Y, amount.Y); - - public RectangleI Shrink(int left, int right, int top, int bottom) => Inflate(-left, -right, -top, -bottom); - - /// Replaces this structure with the intersection of itself and the specified structure. - /// This method does not return a value. - /// The rectangle to intersect. - /// 1 - public void Intersect(RectangleI rect) - { - RectangleI ef = Intersect(rect, this); - X = ef.X; - Y = ef.Y; - Width = ef.Width; - Height = ef.Height; - } - - /// Returns a structure that represents the intersection of two rectangles. If there is no intersection, and empty is returned. - /// A third structure the size of which represents the overlapped area of the two specified rectangles. - /// A rectangle to intersect. - /// A rectangle to intersect. - /// 1 - public static RectangleI Intersect(RectangleI a, RectangleI b) - { - int x = Math.Max(a.X, b.X); - int num2 = Math.Min(a.X + a.Width, b.X + b.Width); - int y = Math.Max(a.Y, b.Y); - int num4 = Math.Min(a.Y + a.Height, b.Y + b.Height); - if (num2 >= x && num4 >= y) - return new RectangleI(x, y, num2 - x, num4 - y); - return Empty; - } - - /// Determines if this rectangle intersects with rect. - /// This method returns true if there is any intersection. - /// The rectangle to test. - /// 1 - public bool IntersectsWith(RectangleI rect) => - rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; - - /// Creates the smallest possible third rectangle that can contain both of two rectangles that form a union. - /// A third structure that contains both of the two rectangles that form the union. - /// A rectangle to union. - /// A rectangle to union. - /// 1 - public static RectangleI Union(RectangleI a, RectangleI b) - { - int x = Math.Min(a.X, b.X); - int num2 = Math.Max(a.X + a.Width, b.X + b.Width); - int y = Math.Min(a.Y, b.Y); - int num4 = Math.Max(a.Y + a.Height, b.Y + b.Height); - return new RectangleI(x, y, num2 - x, num4 - y); - } - - /// Adjusts the location of this rectangle by the specified amount. - /// This method does not return a value. - /// The amount to offset the location vertically. - /// The amount to offset the location horizontally. - /// 1 - public RectangleI Offset(int x, int y) => new RectangleI(X + x, Y + y, Width, Height); - - public static implicit operator RectangleI(RectangleF r) => r.AABB; - - /// Converts the Location and of this to a human-readable string. - /// A string that contains the position, width, and height of this structure¾for example, "{X=20, Y=20, Width=100, Height=50}". - /// 1 - /// - public override string ToString() => $"X={X.ToString(CultureInfo.CurrentCulture)}, " - + $"Y={Y.ToString(CultureInfo.CurrentCulture)}, " - + $"Width={Width.ToString(CultureInfo.CurrentCulture)}, " - + $"Height={Height.ToString(CultureInfo.CurrentCulture)}"; - - public bool Equals(RectangleI other) => X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; +using OpenTK; + +namespace osu.Framework.Graphics.Primitives +{ + /// Stores a set of four integer numbers that represent the location and size of a rectangle. For more advanced region functions, use a object. + /// 1 + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct RectangleI : IEquatable + { + /// Represents an instance of the class with its members uninitialized. + /// 1 + public static readonly RectangleI Empty; + + public int X; + public int Y; + + public int Width; + public int Height; + + /// Initializes a new instance of the class with the specified location and size. + /// The y-coordinate of the upper-left corner of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + /// The x-coordinate of the upper-left corner of the rectangle. + public RectangleI(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + /// Gets or sets the coordinates of the upper-left corner of this structure. + /// A that represents the upper-left corner of this structure. + /// 1 + [Browsable(false)] + public Vector2I Location + { + get { return new Vector2I(X, Y); } + } + + /// Gets or sets the size of this . + /// A that represents the width and height of this structure. + /// 1 + [Browsable(false)] + public Vector2I Size + { + get { return new Vector2I(Width, Height); } + } + + /// Gets the y-coordinate of the top edge of this structure. + /// The y-coordinate of the top edge of this structure. + /// 1 + [Browsable(false)] + public int Left => X; + + /// Gets the y-coordinate of the top edge of this structure. + /// The y-coordinate of the top edge of this structure. + /// 1 + [Browsable(false)] + public int Top => Y; + + /// Gets the x-coordinate that is the sum of and of this structure. + /// The x-coordinate that is the sum of and of this structure. + /// 1 + [Browsable(false)] + public int Right => X + Width; + + /// Gets the y-coordinate that is the sum of and of this structure. + /// The y-coordinate that is the sum of and of this structure. + /// 1 + [Browsable(false)] + public int Bottom => Y + Height; + + [Browsable(false)] + public Vector2I TopLeft => new Vector2I(Left, Top); + + [Browsable(false)] + public Vector2I TopRight => new Vector2I(Right, Top); + + [Browsable(false)] + public Vector2I BottomLeft => new Vector2I(Left, Bottom); + + [Browsable(false)] + public Vector2I BottomRight => new Vector2I(Right, Bottom); + + /// Tests whether the or property of this has a value of zero. + /// This property returns true if the or property of this has a value of zero; otherwise, false. + /// 1 + [Browsable(false)] + public bool IsEmpty => Width <= 0 || Height <= 0; + + /// Tests whether obj is a with the same location and size of this . + /// This method returns true if obj is a and its X, Y, Width, and Height properties are equal to the corresponding properties of this ; otherwise, false. + /// The to test. + /// 1 + public override bool Equals(object obj) + { + if (!(obj is RectangleI)) + return false; + RectangleI ef = (RectangleI)obj; + return ef.X == X && ef.Y == Y && ef.Width == Width && ef.Height == Height; + } + + /// Tests whether two structures have equal location and size. + /// This operator returns true if the two specified structures have equal , , , and properties. + /// The structure that is to the right of the equality operator. + /// The structure that is to the left of the equality operator. + /// 3 + public static bool operator ==(RectangleI left, RectangleI right) => left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height; + + /// Tests whether two structures differ in location or size. + /// This operator returns true if any of the , , , or properties of the two structures are unequal; otherwise false. + /// The structure that is to the right of the inequality operator. + /// The structure that is to the left of the inequality operator. + /// 3 + public static bool operator !=(RectangleI left, RectangleI right) => !(left == right); + + /// Determines if the specified point is contained within this structure. + /// This method returns true if the point defined by x and y is contained within this structure; otherwise false. + /// The y-coordinate of the point to test. + /// The x-coordinate of the point to test. + /// 1 + public bool Contains(float x, float y) => X <= x && x < X + Width && Y <= y && y < Y + Height; + + public bool Contains(Vector2 pt) => Contains(pt.X, pt.Y); + + public bool Contains(int x, int y) => X <= x && x < X + Width && Y <= y && y < Y + Height; + + public bool Contains(Vector2I pt) => Contains(pt.X, pt.Y); + + /// Determines if the rectangular region represented by rect is entirely contained within this structure. + /// This method returns true if the rectangular region represented by rect is entirely contained within the rectangular region represented by this ; otherwise false. + /// The to test. + /// 1 + public bool Contains(RectangleI rect) => + X <= rect.X && rect.X + rect.Width <= X + Width && Y <= rect.Y && + rect.Y + rect.Height <= Y + Height; + + /// Gets the hash code for this structure. For information about the use of hash codes, see Object.GetHashCode. + /// The hash code for this . + /// 1 + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return + (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13 ^ + (uint)Width << 0x1a | (uint)Width >> 6 ^ + (uint)Height << 7 | (uint)Height >> 0x19); + // ReSharper restore NonReadonlyMemberInGetHashCode + } + + public int Area => Width * Height; + + public RectangleI WithPositiveExtent + { + get + { + RectangleI result = this; + + if (result.Width < 0) + { + result.Width = -result.Width; + result.X -= result.Width; + } + + if (Height < 0) + { + result.Height = -result.Height; + result.Y -= result.Height; + } + + return result; + } + } + + public RectangleI Inflate(int amount) => Inflate(new Vector2I(amount)); + + public RectangleI Inflate(Vector2I amount) => Inflate(amount.X, amount.X, amount.Y, amount.Y); + + public RectangleI Inflate(int left, int right, int top, int bottom) => new RectangleI( + X - left, + Y - top, + Width + left + right, + Height + top + bottom); + + public RectangleI Shrink(int amount) => Shrink(new Vector2I(amount)); + + public RectangleI Shrink(Vector2I amount) => Shrink(amount.X, amount.X, amount.Y, amount.Y); + + public RectangleI Shrink(int left, int right, int top, int bottom) => Inflate(-left, -right, -top, -bottom); + + /// Replaces this structure with the intersection of itself and the specified structure. + /// This method does not return a value. + /// The rectangle to intersect. + /// 1 + public void Intersect(RectangleI rect) + { + RectangleI ef = Intersect(rect, this); + X = ef.X; + Y = ef.Y; + Width = ef.Width; + Height = ef.Height; + } + + /// Returns a structure that represents the intersection of two rectangles. If there is no intersection, and empty is returned. + /// A third structure the size of which represents the overlapped area of the two specified rectangles. + /// A rectangle to intersect. + /// A rectangle to intersect. + /// 1 + public static RectangleI Intersect(RectangleI a, RectangleI b) + { + int x = Math.Max(a.X, b.X); + int num2 = Math.Min(a.X + a.Width, b.X + b.Width); + int y = Math.Max(a.Y, b.Y); + int num4 = Math.Min(a.Y + a.Height, b.Y + b.Height); + if (num2 >= x && num4 >= y) + return new RectangleI(x, y, num2 - x, num4 - y); + return Empty; + } + + /// Determines if this rectangle intersects with rect. + /// This method returns true if there is any intersection. + /// The rectangle to test. + /// 1 + public bool IntersectsWith(RectangleI rect) => + rect.X <= X + Width && X <= rect.X + rect.Width && rect.Y <= Y + Height && Y <= rect.Y + rect.Height; + + /// Creates the smallest possible third rectangle that can contain both of two rectangles that form a union. + /// A third structure that contains both of the two rectangles that form the union. + /// A rectangle to union. + /// A rectangle to union. + /// 1 + public static RectangleI Union(RectangleI a, RectangleI b) + { + int x = Math.Min(a.X, b.X); + int num2 = Math.Max(a.X + a.Width, b.X + b.Width); + int y = Math.Min(a.Y, b.Y); + int num4 = Math.Max(a.Y + a.Height, b.Y + b.Height); + return new RectangleI(x, y, num2 - x, num4 - y); + } + + /// Adjusts the location of this rectangle by the specified amount. + /// This method does not return a value. + /// The amount to offset the location vertically. + /// The amount to offset the location horizontally. + /// 1 + public RectangleI Offset(int x, int y) => new RectangleI(X + x, Y + y, Width, Height); + + public static implicit operator RectangleI(RectangleF r) => r.AABB; + + /// Converts the Location and of this to a human-readable string. + /// A string that contains the position, width, and height of this structure¾for example, "{X=20, Y=20, Width=100, Height=50}". + /// 1 + /// + public override string ToString() => $"X={X.ToString(CultureInfo.CurrentCulture)}, " + + $"Y={Y.ToString(CultureInfo.CurrentCulture)}, " + + $"Width={Width.ToString(CultureInfo.CurrentCulture)}, " + + $"Height={Height.ToString(CultureInfo.CurrentCulture)}"; + + public bool Equals(RectangleI other) => X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; + } +} diff --git a/osu.Framework/Graphics/Primitives/Triangle.cs b/osu.Framework/Graphics/Primitives/Triangle.cs index c711dba70..4ef979cf2 100644 --- a/osu.Framework/Graphics/Primitives/Triangle.cs +++ b/osu.Framework/Graphics/Primitives/Triangle.cs @@ -1,73 +1,73 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; - -namespace osu.Framework.Graphics.Primitives -{ - public struct Triangle - { - public Vector2 P0; - public Vector2 P1; - public Vector2 P2; - - public Triangle(Vector2 p0, Vector2 p1, Vector2 p2) - { - P0 = p0; - P1 = p1; - P2 = p2; - } - - /// - /// Checks whether a point lies within the triangle. - /// - /// The point to check. - /// Outcome of the check. - public bool Contains(Vector2 pos) - { - // This code parametrizes pos as a linear combination of 2 edges s*(p1-p0) + t*(p2->p0). - // pos is contained if s>0, t>0, s+t<1 - float area2 = P0.Y * (P2.X - P1.X) + P0.X * (P1.Y - P2.Y) + P1.X * P2.Y - P1.Y * P2.X; - if (area2 == 0) - return false; - - float s = (P0.Y * P2.X - P0.X * P2.Y + (P2.Y - P0.Y) * pos.X + (P0.X - P2.X) * pos.Y) / area2; - if (s < 0) - return false; - - float t = (P0.X * P1.Y - P0.Y * P1.X + (P0.Y - P1.Y) * pos.X + (P1.X - P0.X) * pos.Y) / area2; - if (t < 0 || s + t > 1) - return false; - - return true; - } - - public RectangleF AABBFloat - { - get - { - float xMin = Math.Min(P0.X, Math.Min(P1.X, P2.X)); - float yMin = Math.Min(P0.Y, Math.Min(P1.Y, P2.Y)); - float xMax = Math.Max(P0.X, Math.Max(P1.X, P2.X)); - float yMax = Math.Max(P0.Y, Math.Max(P1.Y, P2.Y)); - - return new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin); - } - } - - public float ConservativeArea => Math.Abs((P0.Y - P1.Y) * (P1.X - P2.X)) / 2; - - public float Area - { - get - { - float a = (P0 - P1).Length; - float b = (P0 - P2).Length; - float c = (P1 - P2).Length; - float s = (a + b + c) / 2.0f; - return (float)Math.Sqrt(s * (s - a) * (s - b) * (s - c)); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; + +namespace osu.Framework.Graphics.Primitives +{ + public struct Triangle + { + public Vector2 P0; + public Vector2 P1; + public Vector2 P2; + + public Triangle(Vector2 p0, Vector2 p1, Vector2 p2) + { + P0 = p0; + P1 = p1; + P2 = p2; + } + + /// + /// Checks whether a point lies within the triangle. + /// + /// The point to check. + /// Outcome of the check. + public bool Contains(Vector2 pos) + { + // This code parametrizes pos as a linear combination of 2 edges s*(p1-p0) + t*(p2->p0). + // pos is contained if s>0, t>0, s+t<1 + float area2 = P0.Y * (P2.X - P1.X) + P0.X * (P1.Y - P2.Y) + P1.X * P2.Y - P1.Y * P2.X; + if (area2 == 0) + return false; + + float s = (P0.Y * P2.X - P0.X * P2.Y + (P2.Y - P0.Y) * pos.X + (P0.X - P2.X) * pos.Y) / area2; + if (s < 0) + return false; + + float t = (P0.X * P1.Y - P0.Y * P1.X + (P0.Y - P1.Y) * pos.X + (P1.X - P0.X) * pos.Y) / area2; + if (t < 0 || s + t > 1) + return false; + + return true; + } + + public RectangleF AABBFloat + { + get + { + float xMin = Math.Min(P0.X, Math.Min(P1.X, P2.X)); + float yMin = Math.Min(P0.Y, Math.Min(P1.Y, P2.Y)); + float xMax = Math.Max(P0.X, Math.Max(P1.X, P2.X)); + float yMax = Math.Max(P0.Y, Math.Max(P1.Y, P2.Y)); + + return new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin); + } + } + + public float ConservativeArea => Math.Abs((P0.Y - P1.Y) * (P1.X - P2.X)) / 2; + + public float Area + { + get + { + float a = (P0 - P1).Length; + float b = (P0 - P2).Length; + float c = (P1 - P2).Length; + float s = (a + b + c) / 2.0f; + return (float)Math.Sqrt(s * (s - a) * (s - b) * (s - c)); + } + } + } +} diff --git a/osu.Framework/Graphics/Primitives/Vector2I.cs b/osu.Framework/Graphics/Primitives/Vector2I.cs index 17129a548..fabff3982 100644 --- a/osu.Framework/Graphics/Primitives/Vector2I.cs +++ b/osu.Framework/Graphics/Primitives/Vector2I.cs @@ -1,56 +1,56 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; -using System.Runtime.InteropServices; - -namespace osu.Framework.Graphics.Primitives -{ - [Serializable, StructLayout(LayoutKind.Sequential)] - public struct Vector2I : IEquatable - { - public int X; - public int Y; - - public Vector2I(int val) : this(val, val) - { - } - - public Vector2I(int x, int y) - { - X = x; - Y = y; - } - - public static readonly Vector2I Zero; - - public static readonly Vector2I One = new Vector2I(1); - - public static implicit operator Vector2(Vector2I r) => new Vector2(r.X, r.Y); - - public static bool operator ==(Vector2I left, Vector2I right) => left.Equals(right); - - public static bool operator !=(Vector2I left, Vector2I right) => !(left == right); - - public static Vector2I operator +(Vector2I left, Vector2I right) => new Vector2I(left.X + right.X, left.Y + right.Y); - - public static Vector2I operator -(Vector2I left, Vector2I right) => new Vector2I(left.X - right.X, left.Y - right.Y); - - public bool Equals(Vector2I other) => other.X == X && other.Y == Y; - - public override bool Equals(object obj) - { - if (!(obj is Vector2I)) - return false; - return Equals((Vector2I)obj); - } - - public override int GetHashCode() - { - // ReSharper disable NonReadonlyMemberInGetHashCode - return (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13); - // ReSharper restore NonReadonlyMemberInGetHashCode - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; +using System.Runtime.InteropServices; + +namespace osu.Framework.Graphics.Primitives +{ + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct Vector2I : IEquatable + { + public int X; + public int Y; + + public Vector2I(int val) : this(val, val) + { + } + + public Vector2I(int x, int y) + { + X = x; + Y = y; + } + + public static readonly Vector2I Zero; + + public static readonly Vector2I One = new Vector2I(1); + + public static implicit operator Vector2(Vector2I r) => new Vector2(r.X, r.Y); + + public static bool operator ==(Vector2I left, Vector2I right) => left.Equals(right); + + public static bool operator !=(Vector2I left, Vector2I right) => !(left == right); + + public static Vector2I operator +(Vector2I left, Vector2I right) => new Vector2I(left.X + right.X, left.Y + right.Y); + + public static Vector2I operator -(Vector2I left, Vector2I right) => new Vector2I(left.X - right.X, left.Y - right.Y); + + public bool Equals(Vector2I other) => other.X == X && other.Y == Y; + + public override bool Equals(object obj) + { + if (!(obj is Vector2I)) + return false; + return Equals((Vector2I)obj); + } + + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return (int)((uint)X ^ (uint)Y << 13 | (uint)Y >> 0x13); + // ReSharper restore NonReadonlyMemberInGetHashCode + } + } +} diff --git a/osu.Framework/Graphics/ProxyDrawable.cs b/osu.Framework/Graphics/ProxyDrawable.cs index 5fb4489de..e5796ed93 100644 --- a/osu.Framework/Graphics/ProxyDrawable.cs +++ b/osu.Framework/Graphics/ProxyDrawable.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Graphics -{ - public class ProxyDrawable : Drawable - { - public ProxyDrawable(Drawable original) - { - Original = original; - } - - internal sealed override Drawable Original { get; } - - public override bool RemoveWhenNotAlive => base.RemoveWhenNotAlive && Original.RemoveWhenNotAlive; - - protected internal override bool ShouldBeAlive => base.ShouldBeAlive && Original.ShouldBeAlive; - - // We do not want to receive updates. That is the business - // of the original drawable. - public override bool IsPresent => false; - - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => Original.UpdateSubTreeMasking(this, maskingBounds); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Graphics +{ + public class ProxyDrawable : Drawable + { + public ProxyDrawable(Drawable original) + { + Original = original; + } + + internal sealed override Drawable Original { get; } + + public override bool RemoveWhenNotAlive => base.RemoveWhenNotAlive && Original.RemoveWhenNotAlive; + + protected internal override bool ShouldBeAlive => base.ShouldBeAlive && Original.ShouldBeAlive; + + // We do not want to receive updates. That is the business + // of the original drawable. + public override bool IsPresent => false; + + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => Original.UpdateSubTreeMasking(this, maskingBounds); + } +} diff --git a/osu.Framework/Graphics/Shaders/AttributeInfo.cs b/osu.Framework/Graphics/Shaders/AttributeInfo.cs index 6aedcc8a9..a022e869d 100644 --- a/osu.Framework/Graphics/Shaders/AttributeInfo.cs +++ b/osu.Framework/Graphics/Shaders/AttributeInfo.cs @@ -1,22 +1,22 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Shaders -{ - /// - /// Stores a vertex shader attribute. - /// - internal struct AttributeInfo - { - /// - /// The 0-based location of this attribute. This is in order of appearance in the shader code. - /// Note that osu! uses 0-based attribute locations to bind vertex pointers to. - /// - public int Location; - - /// - /// The name of the attribute. - /// - public string Name; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Shaders +{ + /// + /// Stores a vertex shader attribute. + /// + internal struct AttributeInfo + { + /// + /// The 0-based location of this attribute. This is in order of appearance in the shader code. + /// Note that osu! uses 0-based attribute locations to bind vertex pointers to. + /// + public int Location; + + /// + /// The name of the attribute. + /// + public string Name; + } +} diff --git a/osu.Framework/Graphics/Shaders/Shader.cs b/osu.Framework/Graphics/Shaders/Shader.cs index 4faf45ff2..fbaf25a98 100644 --- a/osu.Framework/Graphics/Shaders/Shader.cs +++ b/osu.Framework/Graphics/Shaders/Shader.cs @@ -1,205 +1,205 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Text; -using osu.Framework.Graphics.OpenGL; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.Shaders -{ - public class Shader : IDisposable - { - internal StringBuilder Log = new StringBuilder(); - - /// - /// Whether this shader has been loaded and compiled. - /// - public bool Loaded { get; private set; } - - internal bool IsBound; - - private readonly string name; - private int programID = -1; - - private static readonly List all_shaders = new List(); - private static readonly Dictionary global_properties = new Dictionary(); - - private readonly Dictionary uniforms = new Dictionary(); - private UniformBase[] uniformsArray; - private readonly List parts; - - internal Shader(string name, List parts) - { - this.name = name; - this.parts = parts; - - GLWrapper.EnqueueShaderCompile(this); - } - - #region Disposal - - ~Shader() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (Loaded) - { - Unbind(); - - GLWrapper.DeleteProgram(this); - Loaded = false; - programID = -1; - all_shaders.Remove(this); - } - } - - #endregion - - internal void Compile() - { - parts.RemoveAll(p => p == null); - uniforms.Clear(); - uniformsArray = null; - Log.Clear(); - - if (programID != -1) - Dispose(true); - - if (parts.Count == 0) - return; - - programID = GL.CreateProgram(); - foreach (ShaderPart p in parts) - { - if (!p.Compiled) p.Compile(); - GL.AttachShader(this, p); - - foreach (AttributeInfo attribute in p.Attributes) - GL.BindAttribLocation(this, attribute.Location, attribute.Name); - } - - GL.LinkProgram(this); - - GL.GetProgram(this, GetProgramParameterName.LinkStatus, out int linkResult); - string linkLog = GL.GetProgramInfoLog(this); - - Log.AppendLine(string.Format(ShaderPart.BOUNDARY, name)); - Log.AppendLine($"Linked: {linkResult == 1}"); - if (linkResult == 0) - { - Log.AppendLine("Log:"); - Log.AppendLine(linkLog); - } - - foreach (var part in parts) - GL.DetachShader(this, part); - - Loaded = linkResult == 1; - - if (Loaded) - { - //Obtain all the shader uniforms - GL.GetProgram(this, GetProgramParameterName.ActiveUniforms, out int uniformCount); - uniformsArray = new UniformBase[uniformCount]; - - for (int i = 0; i < uniformCount; i++) - { - GL.GetActiveUniform(this, i, 100, out _, out _, out ActiveUniformType type, out string uniformName); - - uniformsArray[i] = new UniformBase(this, uniformName, GL.GetUniformLocation(this, uniformName), type); - uniforms.Add(uniformName, uniformsArray[i]); - } - - foreach (KeyValuePair kvp in global_properties) - { - if (!uniforms.ContainsKey(kvp.Key)) - continue; - uniforms[kvp.Key].Value = kvp.Value; - } - - all_shaders.Add(this); - } - } - - internal void EnsureLoaded() - { - if (!Loaded) - Compile(); - } - - public void Bind() - { - if (IsBound) - return; - - EnsureLoaded(); - - GLWrapper.UseProgram(this); - - foreach (var uniform in uniformsArray) - if (uniform.HasChanged) - uniform.Update(); - - IsBound = true; - } - - public void Unbind() - { - if (!IsBound) - return; - - GLWrapper.UseProgram(null); - - IsBound = false; - } - - public override string ToString() => $@"{name} Shader (Compiled: {programID != -1})"; - - /// - /// Returns a uniform from the shader. - /// - /// The name of the uniform. - /// Returns a base uniform. - public Uniform GetUniform(string name) - { - EnsureLoaded(); - if (!uniforms.ContainsKey(name)) - throw new ArgumentException($"Uniform {name} does not exist in shader {this.name}.", nameof(name)); - return new Uniform(uniforms[name]); - } - - /// - /// Sets a uniform for all shaders that contain this property. - /// Any future-initialized shaders will also have this uniform set. - /// - /// The uniform name. - /// The uniform value. - public static void SetGlobalProperty(string name, object value) - { - if (global_properties.TryGetValue(name, out object found) && found.Equals(value)) - return; - - global_properties[name] = value; - - foreach (Shader shader in all_shaders) - if (shader.Loaded && shader.uniforms.TryGetValue(name, out UniformBase b)) - b.Value = value; - } - - public static implicit operator int(Shader shader) - { - return shader.programID; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Text; +using osu.Framework.Graphics.OpenGL; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.Shaders +{ + public class Shader : IDisposable + { + internal StringBuilder Log = new StringBuilder(); + + /// + /// Whether this shader has been loaded and compiled. + /// + public bool Loaded { get; private set; } + + internal bool IsBound; + + private readonly string name; + private int programID = -1; + + private static readonly List all_shaders = new List(); + private static readonly Dictionary global_properties = new Dictionary(); + + private readonly Dictionary uniforms = new Dictionary(); + private UniformBase[] uniformsArray; + private readonly List parts; + + internal Shader(string name, List parts) + { + this.name = name; + this.parts = parts; + + GLWrapper.EnqueueShaderCompile(this); + } + + #region Disposal + + ~Shader() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Loaded) + { + Unbind(); + + GLWrapper.DeleteProgram(this); + Loaded = false; + programID = -1; + all_shaders.Remove(this); + } + } + + #endregion + + internal void Compile() + { + parts.RemoveAll(p => p == null); + uniforms.Clear(); + uniformsArray = null; + Log.Clear(); + + if (programID != -1) + Dispose(true); + + if (parts.Count == 0) + return; + + programID = GL.CreateProgram(); + foreach (ShaderPart p in parts) + { + if (!p.Compiled) p.Compile(); + GL.AttachShader(this, p); + + foreach (AttributeInfo attribute in p.Attributes) + GL.BindAttribLocation(this, attribute.Location, attribute.Name); + } + + GL.LinkProgram(this); + + GL.GetProgram(this, GetProgramParameterName.LinkStatus, out int linkResult); + string linkLog = GL.GetProgramInfoLog(this); + + Log.AppendLine(string.Format(ShaderPart.BOUNDARY, name)); + Log.AppendLine($"Linked: {linkResult == 1}"); + if (linkResult == 0) + { + Log.AppendLine("Log:"); + Log.AppendLine(linkLog); + } + + foreach (var part in parts) + GL.DetachShader(this, part); + + Loaded = linkResult == 1; + + if (Loaded) + { + //Obtain all the shader uniforms + GL.GetProgram(this, GetProgramParameterName.ActiveUniforms, out int uniformCount); + uniformsArray = new UniformBase[uniformCount]; + + for (int i = 0; i < uniformCount; i++) + { + GL.GetActiveUniform(this, i, 100, out _, out _, out ActiveUniformType type, out string uniformName); + + uniformsArray[i] = new UniformBase(this, uniformName, GL.GetUniformLocation(this, uniformName), type); + uniforms.Add(uniformName, uniformsArray[i]); + } + + foreach (KeyValuePair kvp in global_properties) + { + if (!uniforms.ContainsKey(kvp.Key)) + continue; + uniforms[kvp.Key].Value = kvp.Value; + } + + all_shaders.Add(this); + } + } + + internal void EnsureLoaded() + { + if (!Loaded) + Compile(); + } + + public void Bind() + { + if (IsBound) + return; + + EnsureLoaded(); + + GLWrapper.UseProgram(this); + + foreach (var uniform in uniformsArray) + if (uniform.HasChanged) + uniform.Update(); + + IsBound = true; + } + + public void Unbind() + { + if (!IsBound) + return; + + GLWrapper.UseProgram(null); + + IsBound = false; + } + + public override string ToString() => $@"{name} Shader (Compiled: {programID != -1})"; + + /// + /// Returns a uniform from the shader. + /// + /// The name of the uniform. + /// Returns a base uniform. + public Uniform GetUniform(string name) + { + EnsureLoaded(); + if (!uniforms.ContainsKey(name)) + throw new ArgumentException($"Uniform {name} does not exist in shader {this.name}.", nameof(name)); + return new Uniform(uniforms[name]); + } + + /// + /// Sets a uniform for all shaders that contain this property. + /// Any future-initialized shaders will also have this uniform set. + /// + /// The uniform name. + /// The uniform value. + public static void SetGlobalProperty(string name, object value) + { + if (global_properties.TryGetValue(name, out object found) && found.Equals(value)) + return; + + global_properties[name] = value; + + foreach (Shader shader in all_shaders) + if (shader.Loaded && shader.uniforms.TryGetValue(name, out UniformBase b)) + b.Value = value; + } + + public static implicit operator int(Shader shader) + { + return shader.programID; + } + } +} diff --git a/osu.Framework/Graphics/Shaders/ShaderManager.cs b/osu.Framework/Graphics/Shaders/ShaderManager.cs index 66c6851f0..204f4687c 100644 --- a/osu.Framework/Graphics/Shaders/ShaderManager.cs +++ b/osu.Framework/Graphics/Shaders/ShaderManager.cs @@ -1,118 +1,118 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; -using osu.Framework.IO.Stores; -using osu.Framework.Logging; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.Shaders -{ - public class ShaderManager - { - private const string shader_prefix = @"sh_"; - - private readonly ConcurrentDictionary partCache = new ConcurrentDictionary(); - private readonly ConcurrentDictionary shaderCache = new ConcurrentDictionary(); - - private readonly ResourceStore store; - - public ShaderManager(ResourceStore store) - { - this.store = store; - } - - private string getFileEnding(ShaderType type) - { - switch (type) - { - case ShaderType.FragmentShader: - return @".fs"; - case ShaderType.VertexShader: - return @".vs"; - } - - return string.Empty; - } - - private string ensureValidName(string name, ShaderType type) - { - string ending = getFileEnding(type); - if (!name.StartsWith(shader_prefix, StringComparison.Ordinal)) - name = shader_prefix + name; - if (name.EndsWith(ending, StringComparison.Ordinal)) - return name; - return name + ending; - } - - internal byte[] LoadRaw(string name) => store.Get(name); - - private ShaderPart createShaderPart(string name, ShaderType type, bool bypassCache = false) - { - name = ensureValidName(name, type); - - if (!bypassCache && partCache.TryGetValue(name, out ShaderPart part)) - return part; - - byte[] rawData = LoadRaw(name); - - part = new ShaderPart(name, rawData, type, this); - - //cache even on failure so we don't try and fail every time. - partCache[name] = part; - return part; - } - - public Shader Load(string vertex, string fragment, bool continuousCompilation = false) - { - string name = vertex + '/' + fragment; - - if (!shaderCache.TryGetValue(name, out Shader shader)) - { - List parts = new List - { - createShaderPart(vertex, ShaderType.VertexShader), - createShaderPart(fragment, ShaderType.FragmentShader) - }; - - shader = new Shader(name, parts); - - if (!shader.Loaded) - { - StringBuilder logContents = new StringBuilder(); - logContents.AppendLine($@"Loading shader {name}:"); - logContents.Append(shader.Log); - logContents.AppendLine(@"Parts:"); - foreach (ShaderPart p in parts) - logContents.Append(p.Log); - Logger.Log(logContents.ToString(), LoggingTarget.Runtime, LogLevel.Debug); - } - - shaderCache[name] = shader; - } - - return shader; - } - } - - public static class VertexShaderDescriptor - { - public const string TEXTURE_2 = "Texture2D"; - public const string TEXTURE_3 = "Texture3D"; - public const string POSITION = "Position"; - public const string COLOUR = "Colour"; - } - - public static class FragmentShaderDescriptor - { - public const string TEXTURE = "Texture"; - public const string TEXTURE_ROUNDED = "TextureRounded"; - public const string COLOUR = "Colour"; - public const string COLOUR_ROUNDED = "ColourRounded"; - public const string GLOW = "Glow"; - public const string BLUR = "Blur"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.Shaders +{ + public class ShaderManager + { + private const string shader_prefix = @"sh_"; + + private readonly ConcurrentDictionary partCache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary shaderCache = new ConcurrentDictionary(); + + private readonly ResourceStore store; + + public ShaderManager(ResourceStore store) + { + this.store = store; + } + + private string getFileEnding(ShaderType type) + { + switch (type) + { + case ShaderType.FragmentShader: + return @".fs"; + case ShaderType.VertexShader: + return @".vs"; + } + + return string.Empty; + } + + private string ensureValidName(string name, ShaderType type) + { + string ending = getFileEnding(type); + if (!name.StartsWith(shader_prefix, StringComparison.Ordinal)) + name = shader_prefix + name; + if (name.EndsWith(ending, StringComparison.Ordinal)) + return name; + return name + ending; + } + + internal byte[] LoadRaw(string name) => store.Get(name); + + private ShaderPart createShaderPart(string name, ShaderType type, bool bypassCache = false) + { + name = ensureValidName(name, type); + + if (!bypassCache && partCache.TryGetValue(name, out ShaderPart part)) + return part; + + byte[] rawData = LoadRaw(name); + + part = new ShaderPart(name, rawData, type, this); + + //cache even on failure so we don't try and fail every time. + partCache[name] = part; + return part; + } + + public Shader Load(string vertex, string fragment, bool continuousCompilation = false) + { + string name = vertex + '/' + fragment; + + if (!shaderCache.TryGetValue(name, out Shader shader)) + { + List parts = new List + { + createShaderPart(vertex, ShaderType.VertexShader), + createShaderPart(fragment, ShaderType.FragmentShader) + }; + + shader = new Shader(name, parts); + + if (!shader.Loaded) + { + StringBuilder logContents = new StringBuilder(); + logContents.AppendLine($@"Loading shader {name}:"); + logContents.Append(shader.Log); + logContents.AppendLine(@"Parts:"); + foreach (ShaderPart p in parts) + logContents.Append(p.Log); + Logger.Log(logContents.ToString(), LoggingTarget.Runtime, LogLevel.Debug); + } + + shaderCache[name] = shader; + } + + return shader; + } + } + + public static class VertexShaderDescriptor + { + public const string TEXTURE_2 = "Texture2D"; + public const string TEXTURE_3 = "Texture3D"; + public const string POSITION = "Position"; + public const string COLOUR = "Colour"; + } + + public static class FragmentShaderDescriptor + { + public const string TEXTURE = "Texture"; + public const string TEXTURE_ROUNDED = "TextureRounded"; + public const string COLOUR = "Colour"; + public const string COLOUR_ROUNDED = "ColourRounded"; + public const string GLOW = "Glow"; + public const string BLUR = "Blur"; + } +} diff --git a/osu.Framework/Graphics/Shaders/ShaderPart.cs b/osu.Framework/Graphics/Shaders/ShaderPart.cs index 553239ca5..7d558b7e0 100644 --- a/osu.Framework/Graphics/Shaders/ShaderPart.cs +++ b/osu.Framework/Graphics/Shaders/ShaderPart.cs @@ -1,157 +1,157 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using osu.Framework.Graphics.OpenGL; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.Shaders -{ - internal class ShaderPart : IDisposable - { - internal const string BOUNDARY = @"----------------------{0}"; - - internal StringBuilder Log = new StringBuilder(); - - internal List Attributes = new List(); - - internal string Name; - internal bool HasCode; - internal bool Compiled; - - internal ShaderType Type; - - private int partID = -1; - - private int lastAttributeIndex; - - private readonly List shaderCodes = new List(); - - private readonly Regex includeRegex = new Regex("^\\s*#\\s*include\\s+[\"<](.*)[\">]"); - private readonly Regex attributeRegex = new Regex("^\\s*attribute\\s+[^\\s]+\\s+([^;]+);"); - - private readonly ShaderManager manager; - - internal ShaderPart(string name, byte[] data, ShaderType type, ShaderManager manager) - { - Name = name; - Type = type; - - this.manager = manager; - - shaderCodes.Add(loadFile(data)); - shaderCodes.RemoveAll(string.IsNullOrEmpty); - - if (shaderCodes.Count == 0) - return; - - HasCode = true; - } - - private string loadFile(byte[] bytes) - { - if (bytes == null) - return null; - - using (MemoryStream ms = new MemoryStream(bytes)) - using (StreamReader sr = new StreamReader(ms)) - { - string code = string.Empty; - - while (sr.Peek() != -1) - { - string line = sr.ReadLine(); - - if (string.IsNullOrEmpty(line)) - continue; - - Match includeMatch = includeRegex.Match(line); - if (includeMatch.Success) - { - string includeName = includeMatch.Groups[1].Value.Trim(); - - //#if DEBUG - // byte[] rawData = null; - // if (File.Exists(includeName)) - // rawData = File.ReadAllBytes(includeName); - //#endif - shaderCodes.Add(loadFile(manager.LoadRaw(includeName))); - } - else - code += '\n' + line; - - Match attributeMatch = attributeRegex.Match(line); - if (attributeMatch.Success) - { - Attributes.Add(new AttributeInfo - { - Location = lastAttributeIndex++, - Name = attributeMatch.Groups[1].Value.Trim() - }); - } - } - - return code; - } - } - - internal bool Compile() - { - if (!HasCode) - return false; - - if (partID == -1) - partID = GL.CreateShader(Type); - - int[] codeLengths = new int[shaderCodes.Count]; - for (int i = 0; i < shaderCodes.Count; i++) - codeLengths[i] = shaderCodes[i].Length; - - GL.ShaderSource(this, shaderCodes.Count, shaderCodes.ToArray(), codeLengths); - GL.CompileShader(this); - - GL.GetShader(this, ShaderParameter.CompileStatus, out int compileResult); - Compiled = compileResult == 1; - -#if DEBUG - string compileLog = GL.GetShaderInfoLog(this); - Log.AppendLine(string.Format('\t' + BOUNDARY, Name)); - Log.AppendLine($"\tCompiled: {Compiled}"); - if (!Compiled) - { - Log.AppendLine("\tLog:"); - Log.AppendLine('\t' + compileLog); - } -#endif - - if (!Compiled) - Dispose(true); - - return Compiled; - } - - public static implicit operator int(ShaderPart program) - { - return program.partID; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - if (!disposing || partID == -1) return; - - GLWrapper.DeleteShader(this); - Compiled = false; - partID = -1; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using osu.Framework.Graphics.OpenGL; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.Shaders +{ + internal class ShaderPart : IDisposable + { + internal const string BOUNDARY = @"----------------------{0}"; + + internal StringBuilder Log = new StringBuilder(); + + internal List Attributes = new List(); + + internal string Name; + internal bool HasCode; + internal bool Compiled; + + internal ShaderType Type; + + private int partID = -1; + + private int lastAttributeIndex; + + private readonly List shaderCodes = new List(); + + private readonly Regex includeRegex = new Regex("^\\s*#\\s*include\\s+[\"<](.*)[\">]"); + private readonly Regex attributeRegex = new Regex("^\\s*attribute\\s+[^\\s]+\\s+([^;]+);"); + + private readonly ShaderManager manager; + + internal ShaderPart(string name, byte[] data, ShaderType type, ShaderManager manager) + { + Name = name; + Type = type; + + this.manager = manager; + + shaderCodes.Add(loadFile(data)); + shaderCodes.RemoveAll(string.IsNullOrEmpty); + + if (shaderCodes.Count == 0) + return; + + HasCode = true; + } + + private string loadFile(byte[] bytes) + { + if (bytes == null) + return null; + + using (MemoryStream ms = new MemoryStream(bytes)) + using (StreamReader sr = new StreamReader(ms)) + { + string code = string.Empty; + + while (sr.Peek() != -1) + { + string line = sr.ReadLine(); + + if (string.IsNullOrEmpty(line)) + continue; + + Match includeMatch = includeRegex.Match(line); + if (includeMatch.Success) + { + string includeName = includeMatch.Groups[1].Value.Trim(); + + //#if DEBUG + // byte[] rawData = null; + // if (File.Exists(includeName)) + // rawData = File.ReadAllBytes(includeName); + //#endif + shaderCodes.Add(loadFile(manager.LoadRaw(includeName))); + } + else + code += '\n' + line; + + Match attributeMatch = attributeRegex.Match(line); + if (attributeMatch.Success) + { + Attributes.Add(new AttributeInfo + { + Location = lastAttributeIndex++, + Name = attributeMatch.Groups[1].Value.Trim() + }); + } + } + + return code; + } + } + + internal bool Compile() + { + if (!HasCode) + return false; + + if (partID == -1) + partID = GL.CreateShader(Type); + + int[] codeLengths = new int[shaderCodes.Count]; + for (int i = 0; i < shaderCodes.Count; i++) + codeLengths[i] = shaderCodes[i].Length; + + GL.ShaderSource(this, shaderCodes.Count, shaderCodes.ToArray(), codeLengths); + GL.CompileShader(this); + + GL.GetShader(this, ShaderParameter.CompileStatus, out int compileResult); + Compiled = compileResult == 1; + +#if DEBUG + string compileLog = GL.GetShaderInfoLog(this); + Log.AppendLine(string.Format('\t' + BOUNDARY, Name)); + Log.AppendLine($"\tCompiled: {Compiled}"); + if (!Compiled) + { + Log.AppendLine("\tLog:"); + Log.AppendLine('\t' + compileLog); + } +#endif + + if (!Compiled) + Dispose(true); + + return Compiled; + } + + public static implicit operator int(ShaderPart program) + { + return program.partID; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (!disposing || partID == -1) return; + + GLWrapper.DeleteShader(this); + Compiled = false; + partID = -1; + } + } +} diff --git a/osu.Framework/Graphics/Shaders/Uniform.cs b/osu.Framework/Graphics/Shaders/Uniform.cs index ffbf9f819..e36ec5cb0 100644 --- a/osu.Framework/Graphics/Shaders/Uniform.cs +++ b/osu.Framework/Graphics/Shaders/Uniform.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Shaders -{ - public class Uniform - { - private readonly UniformBase uniformBase; - - internal Uniform(UniformBase uniformBase) - { - this.uniformBase = uniformBase; - } - - /// - /// Gets or sets the value of this uniform. - /// - public T Value - { - get { return (T)uniformBase.Value; } - set { uniformBase.Value = value; } - } - - /// - /// Returns the value of the uniform. - /// - /// The uniform to retrieve the value of. - public static implicit operator T(Uniform filterUniform) - { - return filterUniform.Value; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Shaders +{ + public class Uniform + { + private readonly UniformBase uniformBase; + + internal Uniform(UniformBase uniformBase) + { + this.uniformBase = uniformBase; + } + + /// + /// Gets or sets the value of this uniform. + /// + public T Value + { + get { return (T)uniformBase.Value; } + set { uniformBase.Value = value; } + } + + /// + /// Returns the value of the uniform. + /// + /// The uniform to retrieve the value of. + public static implicit operator T(Uniform filterUniform) + { + return filterUniform.Value; + } + } +} diff --git a/osu.Framework/Graphics/Shaders/UniformBase.cs b/osu.Framework/Graphics/Shaders/UniformBase.cs index 1d1724c6a..54357d276 100644 --- a/osu.Framework/Graphics/Shaders/UniformBase.cs +++ b/osu.Framework/Graphics/Shaders/UniformBase.cs @@ -1,59 +1,59 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL; - -namespace osu.Framework.Graphics.Shaders -{ - internal class UniformBase - { - public string Name { get; } - - private object value; - - public object Value - { - get { return value; } - set - { - if (value == this.value) - return; - - this.value = value; - HasChanged = true; - - if (owner.IsBound) - Update(); - } - } - - private readonly int location; - private readonly ActiveUniformType type; - - public bool HasChanged { get; private set; } = true; - - private readonly Shader owner; - - public UniformBase(Shader owner, string name, int uniformLocation, ActiveUniformType type) - { - this.owner = owner; - Name = name; - location = uniformLocation; - this.type = type; - } - - public void Update() - { - if (!HasChanged) - return; - - HasChanged = false; - - if (Value == null) - return; - - GLWrapper.SetUniform(owner, type, location, Value); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL; + +namespace osu.Framework.Graphics.Shaders +{ + internal class UniformBase + { + public string Name { get; } + + private object value; + + public object Value + { + get { return value; } + set + { + if (value == this.value) + return; + + this.value = value; + HasChanged = true; + + if (owner.IsBound) + Update(); + } + } + + private readonly int location; + private readonly ActiveUniformType type; + + public bool HasChanged { get; private set; } = true; + + private readonly Shader owner; + + public UniformBase(Shader owner, string name, int uniformLocation, ActiveUniformType type) + { + this.owner = owner; + Name = name; + location = uniformLocation; + this.type = type; + } + + public void Update() + { + if (!HasChanged) + return; + + HasChanged = false; + + if (Value == null) + return; + + GLWrapper.SetUniform(owner, type, location, Value); + } + } +} diff --git a/osu.Framework/Graphics/Shapes/Box.cs b/osu.Framework/Graphics/Shapes/Box.cs index e9c2b9f56..2e37b59ca 100644 --- a/osu.Framework/Graphics/Shapes/Box.cs +++ b/osu.Framework/Graphics/Shapes/Box.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; - -namespace osu.Framework.Graphics.Shapes -{ - /// - /// A simple rectangular box. Can be colored using the property. - /// - public class Box : Sprite - { - public Box() - { - Texture = Texture.WhitePixel; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Framework.Graphics.Shapes +{ + /// + /// A simple rectangular box. Can be colored using the property. + /// + public class Box : Sprite + { + public Box() + { + Texture = Texture.WhitePixel; + } + } +} diff --git a/osu.Framework/Graphics/Shapes/Circle.cs b/osu.Framework/Graphics/Shapes/Circle.cs index 4a21cefd0..5cb59e0ff 100644 --- a/osu.Framework/Graphics/Shapes/Circle.cs +++ b/osu.Framework/Graphics/Shapes/Circle.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Graphics.Shapes -{ - /// - /// A simple with a fill using a . Can be coloured using the property. - /// - public class Circle : CircularContainer - { - public Circle() - { - Masking = true; - - AddInternal(new Box { RelativeSizeAxes = Axes.Both }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Graphics.Shapes +{ + /// + /// A simple with a fill using a . Can be coloured using the property. + /// + public class Circle : CircularContainer + { + public Circle() + { + Masking = true; + + AddInternal(new Box { RelativeSizeAxes = Axes.Both }); + } + } +} diff --git a/osu.Framework/Graphics/Shapes/EquilateralTriangle.cs b/osu.Framework/Graphics/Shapes/EquilateralTriangle.cs index 2cb7cc83a..c4d65ce00 100644 --- a/osu.Framework/Graphics/Shapes/EquilateralTriangle.cs +++ b/osu.Framework/Graphics/Shapes/EquilateralTriangle.cs @@ -1,36 +1,36 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Graphics.Shapes -{ - /// - /// A triangle which has all side lengths and angles equal. - /// - public class EquilateralTriangle : Triangle - { - /// - /// For equilateral triangles, height = cos(30) * sidelength = ~0.866 * sidelength. - /// This is applied to the side length of the triangle to determine the height. - /// - private const float sidelength_to_height_factor = 0.866f; - - /// - /// The size of this triangle. - /// - /// When setting the size, the Y-value is ignored (use if you desire a specific height instead). - /// - /// - public override Vector2 Size => new Vector2(base.Size.X, base.Size.X * sidelength_to_height_factor); - - /// - /// Sets the height of the triangle, adjusting the width as appropriate. - /// - public override float Height - { - get { return Width * sidelength_to_height_factor; } - set { Size = new Vector2(value / sidelength_to_height_factor); } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Graphics.Shapes +{ + /// + /// A triangle which has all side lengths and angles equal. + /// + public class EquilateralTriangle : Triangle + { + /// + /// For equilateral triangles, height = cos(30) * sidelength = ~0.866 * sidelength. + /// This is applied to the side length of the triangle to determine the height. + /// + private const float sidelength_to_height_factor = 0.866f; + + /// + /// The size of this triangle. + /// + /// When setting the size, the Y-value is ignored (use if you desire a specific height instead). + /// + /// + public override Vector2 Size => new Vector2(base.Size.X, base.Size.X * sidelength_to_height_factor); + + /// + /// Sets the height of the triangle, adjusting the width as appropriate. + /// + public override float Height + { + get { return Width * sidelength_to_height_factor; } + set { Size = new Vector2(value / sidelength_to_height_factor); } + } + } +} diff --git a/osu.Framework/Graphics/Shapes/Triangle.cs b/osu.Framework/Graphics/Shapes/Triangle.cs index 0039bea80..66dec05a3 100644 --- a/osu.Framework/Graphics/Shapes/Triangle.cs +++ b/osu.Framework/Graphics/Shapes/Triangle.cs @@ -1,46 +1,46 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Textures; -using OpenTK; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Sprites; - -namespace osu.Framework.Graphics.Shapes -{ - /// - /// Represents a sprite that is drawn in a triangle shape, instead of a rectangle shape. - /// - public class Triangle : Sprite - { - /// - /// Creates a new triangle with a white pixel as texture. - /// - public Triangle() - { - Texture = Texture.WhitePixel; - } - - public override RectangleF BoundingBox => toTriangle(ToParentSpace(LayoutRectangle)).AABBFloat; - - private static Primitives.Triangle toTriangle(Quad q) => new Primitives.Triangle( - (q.TopLeft + q.TopRight) / 2, - q.BottomLeft, - q.BottomRight); - - public override bool Contains(Vector2 screenSpacePos) => toTriangle(ScreenSpaceDrawQuad).Contains(screenSpacePos); - - protected override DrawNode CreateDrawNode() => new TriangleDrawNode(); - - private class TriangleDrawNode : SpriteDrawNode - { - protected override void Blit(Action vertexAction) - { - Texture.DrawTriangle(toTriangle(ScreenSpaceDrawQuad), DrawInfo.Colour, null, null, - new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height)); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Textures; +using OpenTK; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Sprites; + +namespace osu.Framework.Graphics.Shapes +{ + /// + /// Represents a sprite that is drawn in a triangle shape, instead of a rectangle shape. + /// + public class Triangle : Sprite + { + /// + /// Creates a new triangle with a white pixel as texture. + /// + public Triangle() + { + Texture = Texture.WhitePixel; + } + + public override RectangleF BoundingBox => toTriangle(ToParentSpace(LayoutRectangle)).AABBFloat; + + private static Primitives.Triangle toTriangle(Quad q) => new Primitives.Triangle( + (q.TopLeft + q.TopRight) / 2, + q.BottomLeft, + q.BottomRight); + + public override bool Contains(Vector2 screenSpacePos) => toTriangle(ScreenSpaceDrawQuad).Contains(screenSpacePos); + + protected override DrawNode CreateDrawNode() => new TriangleDrawNode(); + + private class TriangleDrawNode : SpriteDrawNode + { + protected override void Blit(Action vertexAction) + { + Texture.DrawTriangle(toTriangle(ScreenSpaceDrawQuad), DrawInfo.Colour, null, null, + new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height)); + } + } + } +} diff --git a/osu.Framework/Graphics/Sprites/IHasLineBaseHeight.cs b/osu.Framework/Graphics/Sprites/IHasLineBaseHeight.cs index a12e79640..0db73eb5a 100644 --- a/osu.Framework/Graphics/Sprites/IHasLineBaseHeight.cs +++ b/osu.Framework/Graphics/Sprites/IHasLineBaseHeight.cs @@ -1,16 +1,16 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Sprites -{ - /// - /// Objects implementing this interface have a line base height when used in a CustomizableTextContainer. - /// - public interface IHasLineBaseHeight - { - /// - /// The line base height this object has. - /// - float LineBaseHeight { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// Objects implementing this interface have a line base height when used in a CustomizableTextContainer. + /// + public interface IHasLineBaseHeight + { + /// + /// The line base height this object has. + /// + float LineBaseHeight { get; } + } +} diff --git a/osu.Framework/Graphics/Sprites/IHasText.cs b/osu.Framework/Graphics/Sprites/IHasText.cs index 35ad85a48..c606d3aad 100644 --- a/osu.Framework/Graphics/Sprites/IHasText.cs +++ b/osu.Framework/Graphics/Sprites/IHasText.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.Sprites -{ - /// - /// Interface for components that support reading and writing text. - /// - public interface IHasText : IDrawable - { - string Text { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// Interface for components that support reading and writing text. + /// + public interface IHasText : IDrawable + { + string Text { get; set; } + } +} diff --git a/osu.Framework/Graphics/Sprites/Sprite.cs b/osu.Framework/Graphics/Sprites/Sprite.cs index 4e92c8de4..db7bae5a5 100644 --- a/osu.Framework/Graphics/Sprites/Sprite.cs +++ b/osu.Framework/Graphics/Sprites/Sprite.cs @@ -1,142 +1,142 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Textures; -using OpenTK; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Allocation; - -namespace osu.Framework.Graphics.Sprites -{ - /// - /// A sprite that displays its texture. - /// - public class Sprite : Drawable - { - private Shader textureShader; - private Shader roundedTextureShader; - - /// - /// True if the texture should be tiled. If you had a 16x16 texture and scaled the sprite to be 64x64 the texture would be repeated in a 4x4 grid along the size of the sprite. - /// - public bool WrapTexture; - - /// - /// Maximum value that can be set for on either axis. - /// - public const int MAX_EDGE_SMOOTHNESS = 2; - - /// - /// Determines over how many pixels of width the border of the sprite is smoothed - /// in X and Y direction respectively. - /// IMPORTANT: When masking an edge-smoothed sprite some of the smooth transition - /// may be masked away. This should be counteracted by setting the MaskingSmoothness - /// of the masking container to a slightly larger value than EdgeSmoothness. - /// - public Vector2 EdgeSmoothness = Vector2.Zero; - - /// - /// True if the should be disposed when this sprite gets disposed. - /// - public bool CanDisposeTexture { get; protected set; } - - #region Disposal - - protected override void Dispose(bool isDisposing) - { - if (CanDisposeTexture && texture != null) - { - if (!(texture is TextureWhitePixel)) - texture.Dispose(); - texture = null; - } - - base.Dispose(isDisposing); - } - - #endregion - - protected override DrawNode CreateDrawNode() => new SpriteDrawNode(); - - protected override void ApplyDrawNode(DrawNode node) - { - SpriteDrawNode n = (SpriteDrawNode)node; - - n.ScreenSpaceDrawQuad = ScreenSpaceDrawQuad; - n.DrawRectangle = DrawRectangle; - n.Texture = Texture; - n.WrapTexture = WrapTexture; - - n.TextureShader = textureShader; - n.RoundedTextureShader = roundedTextureShader; - n.InflationAmount = inflationAmount; - - base.ApplyDrawNode(node); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); - roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); - } - - private Texture texture; - - /// - /// The texture that this sprite should draw. If is true and the texture gets replaced, the old texture will be disposed. - /// If this sprite's is (eg if it has not been set previously), the - /// of this sprite will be set to the size of the texture. - /// is automatically set to the aspect ratio of the given texture or 1 if the texture is null. - /// - public Texture Texture - { - get { return texture; } - set - { - if (value == texture) - return; - - if (texture != null && CanDisposeTexture) - texture.Dispose(); - - texture = value; - FillAspectRatio = (float)(texture?.Width ?? 1) / (texture?.Height ?? 1); - Invalidate(Invalidation.DrawNode); - - if (Size == Vector2.Zero) - Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); - } - } - - private Vector2 inflationAmount; - - protected override Quad ComputeScreenSpaceDrawQuad() - { - if (EdgeSmoothness == Vector2.Zero) - { - inflationAmount = Vector2.Zero; - return base.ComputeScreenSpaceDrawQuad(); - } - - if (EdgeSmoothness.X > MAX_EDGE_SMOOTHNESS || EdgeSmoothness.Y > MAX_EDGE_SMOOTHNESS) - throw new InvalidOperationException( - $"May not smooth more than {MAX_EDGE_SMOOTHNESS} or will leak neighboring textures in atlas. Tried to smooth by ({EdgeSmoothness.X}, {EdgeSmoothness.Y})."); - - Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); - - inflationAmount = new Vector2(scale.X * EdgeSmoothness.X, scale.Y * EdgeSmoothness.Y); - return ToScreenSpace(DrawRectangle.Inflate(inflationAmount)); - } - - public override string ToString() - { - string result = base.ToString(); - if (!string.IsNullOrEmpty(texture?.AssetName)) - result += $" tex: {texture?.AssetName}"; - return result; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Textures; +using OpenTK; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Allocation; + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// A sprite that displays its texture. + /// + public class Sprite : Drawable + { + private Shader textureShader; + private Shader roundedTextureShader; + + /// + /// True if the texture should be tiled. If you had a 16x16 texture and scaled the sprite to be 64x64 the texture would be repeated in a 4x4 grid along the size of the sprite. + /// + public bool WrapTexture; + + /// + /// Maximum value that can be set for on either axis. + /// + public const int MAX_EDGE_SMOOTHNESS = 2; + + /// + /// Determines over how many pixels of width the border of the sprite is smoothed + /// in X and Y direction respectively. + /// IMPORTANT: When masking an edge-smoothed sprite some of the smooth transition + /// may be masked away. This should be counteracted by setting the MaskingSmoothness + /// of the masking container to a slightly larger value than EdgeSmoothness. + /// + public Vector2 EdgeSmoothness = Vector2.Zero; + + /// + /// True if the should be disposed when this sprite gets disposed. + /// + public bool CanDisposeTexture { get; protected set; } + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + if (CanDisposeTexture && texture != null) + { + if (!(texture is TextureWhitePixel)) + texture.Dispose(); + texture = null; + } + + base.Dispose(isDisposing); + } + + #endregion + + protected override DrawNode CreateDrawNode() => new SpriteDrawNode(); + + protected override void ApplyDrawNode(DrawNode node) + { + SpriteDrawNode n = (SpriteDrawNode)node; + + n.ScreenSpaceDrawQuad = ScreenSpaceDrawQuad; + n.DrawRectangle = DrawRectangle; + n.Texture = Texture; + n.WrapTexture = WrapTexture; + + n.TextureShader = textureShader; + n.RoundedTextureShader = roundedTextureShader; + n.InflationAmount = inflationAmount; + + base.ApplyDrawNode(node); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + } + + private Texture texture; + + /// + /// The texture that this sprite should draw. If is true and the texture gets replaced, the old texture will be disposed. + /// If this sprite's is (eg if it has not been set previously), the + /// of this sprite will be set to the size of the texture. + /// is automatically set to the aspect ratio of the given texture or 1 if the texture is null. + /// + public Texture Texture + { + get { return texture; } + set + { + if (value == texture) + return; + + if (texture != null && CanDisposeTexture) + texture.Dispose(); + + texture = value; + FillAspectRatio = (float)(texture?.Width ?? 1) / (texture?.Height ?? 1); + Invalidate(Invalidation.DrawNode); + + if (Size == Vector2.Zero) + Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); + } + } + + private Vector2 inflationAmount; + + protected override Quad ComputeScreenSpaceDrawQuad() + { + if (EdgeSmoothness == Vector2.Zero) + { + inflationAmount = Vector2.Zero; + return base.ComputeScreenSpaceDrawQuad(); + } + + if (EdgeSmoothness.X > MAX_EDGE_SMOOTHNESS || EdgeSmoothness.Y > MAX_EDGE_SMOOTHNESS) + throw new InvalidOperationException( + $"May not smooth more than {MAX_EDGE_SMOOTHNESS} or will leak neighboring textures in atlas. Tried to smooth by ({EdgeSmoothness.X}, {EdgeSmoothness.Y})."); + + Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); + + inflationAmount = new Vector2(scale.X * EdgeSmoothness.X, scale.Y * EdgeSmoothness.Y); + return ToScreenSpace(DrawRectangle.Inflate(inflationAmount)); + } + + public override string ToString() + { + string result = base.ToString(); + if (!string.IsNullOrEmpty(texture?.AssetName)) + result += $" tex: {texture?.AssetName}"; + return result; + } + } +} diff --git a/osu.Framework/Graphics/Sprites/SpriteDrawNode.cs b/osu.Framework/Graphics/Sprites/SpriteDrawNode.cs index 42e4ed555..a3a61fc95 100644 --- a/osu.Framework/Graphics/Sprites/SpriteDrawNode.cs +++ b/osu.Framework/Graphics/Sprites/SpriteDrawNode.cs @@ -1,55 +1,55 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL; -using OpenTK; -using System; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.Sprites -{ - /// - /// Draw node containing all necessary information to draw a . - /// - public class SpriteDrawNode : DrawNode - { - public Texture Texture; - public Quad ScreenSpaceDrawQuad; - public RectangleF DrawRectangle; - public Vector2 InflationAmount; - public bool WrapTexture; - - public Shader TextureShader; - public Shader RoundedTextureShader; - - private bool needsRoundedShader => GLWrapper.IsMaskingActive || InflationAmount != Vector2.Zero; - - protected virtual void Blit(Action vertexAction) - { - Texture.DrawQuad(ScreenSpaceDrawQuad, DrawInfo.Colour, null, vertexAction, - new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height)); - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - if (Texture == null || Texture.IsDisposed) - return; - - Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; - - shader.Bind(); - - Texture.TextureGL.WrapMode = WrapTexture ? TextureWrapMode.Repeat : TextureWrapMode.ClampToEdge; - - Blit(vertexAction); - - shader.Unbind(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL; +using OpenTK; +using System; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// Draw node containing all necessary information to draw a . + /// + public class SpriteDrawNode : DrawNode + { + public Texture Texture; + public Quad ScreenSpaceDrawQuad; + public RectangleF DrawRectangle; + public Vector2 InflationAmount; + public bool WrapTexture; + + public Shader TextureShader; + public Shader RoundedTextureShader; + + private bool needsRoundedShader => GLWrapper.IsMaskingActive || InflationAmount != Vector2.Zero; + + protected virtual void Blit(Action vertexAction) + { + Texture.DrawQuad(ScreenSpaceDrawQuad, DrawInfo.Colour, null, vertexAction, + new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height)); + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + if (Texture == null || Texture.IsDisposed) + return; + + Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; + + shader.Bind(); + + Texture.TextureGL.WrapMode = WrapTexture ? TextureWrapMode.Repeat : TextureWrapMode.ClampToEdge; + + Blit(vertexAction); + + shader.Unbind(); + } + } +} diff --git a/osu.Framework/Graphics/Sprites/SpriteText.cs b/osu.Framework/Graphics/Sprites/SpriteText.cs index 6e5e331a4..bc71f0f93 100644 --- a/osu.Framework/Graphics/Sprites/SpriteText.cs +++ b/osu.Framework/Graphics/Sprites/SpriteText.cs @@ -1,397 +1,397 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Caching; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.IO.Stores; -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; - -namespace osu.Framework.Graphics.Sprites -{ - /// - /// A container for simple text rendering purposes. If more complex text rendering is required, use instead. - /// - public class SpriteText : FillFlowContainer, IHasCurrentValue, IHasLineBaseHeight, IHasText, IHasFilterTerms - { - public IEnumerable FilterTerms => new[] { Text }; - - private static readonly char[] default_fixed_width_exceptions = { '.', ':', ',' }; - - /// - /// An array of characters which should not get a fixed width in a instance. - /// - protected virtual char[] FixedWidthExceptionCharacters => default_fixed_width_exceptions; - - /// - /// Decide whether we want to make our SpriteText's vertical size to be (the full height) or precisely the size of used characters. - /// Set to false to allow better centering of individual characters/numerals/etc. - /// - public bool UseFullGlyphHeight = true; - - public override bool IsPresent => base.IsPresent && !string.IsNullOrEmpty(text); - - /// - /// True if the text should be wrapped if it gets too wide. Note that \n does NOT cause a line break. If you need explicit line breaks, use instead. - /// - public bool AllowMultiline - { - get { return Direction == FillDirection.Full; } - set { Direction = value ? FillDirection.Full : FillDirection.Horizontal; } - } - - private string font; - - /// - /// The name of the font to use when looking up textures for the individual characters. - /// - public string Font - { - get { return font; } - set - { - font = value; - layout.Invalidate(); - } - } - - private bool shadow; - - /// - /// True if a shadow should be displayed around the text. - /// - public bool Shadow - { - get { return shadow; } - set - { - if (shadow == value) return; - - shadow = value; - layout.Invalidate(); // Trigger a layout refresh - } - } - - - private Color4 shadowColour = new Color4(0f, 0f, 0f, 0.2f); - - /// - /// The colour of the shadow displayed around the text. A shadow will only be displayed if the property is set to true. - /// - public Color4 ShadowColour - { - get { return shadowColour; } - set - { - shadowColour = value; - if (shadow) - layout.Invalidate(); - } - } - - /// - /// Gets the base height of the font used by this text. If the font of this text is invalid, 0 is returned. - /// - public float LineBaseHeight - { - get - { - var baseHeight = store.GetBaseHeight(Font); - if (baseHeight.HasValue) - return baseHeight.Value * TextSize; - - if (string.IsNullOrEmpty(Text)) - return 0; - - return store.GetBaseHeight(Text[0]).GetValueOrDefault() * TextSize; - } - } - - private Cached layout = new Cached(); - - private float spaceWidth; - - private FontStore store; - - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - - /// - /// Creates a new sprite text. is set to by default. - /// - public SpriteText() - { - AutoSizeAxes = Axes.Both; - } - - private const float default_text_size = 20; - - private float textSize = default_text_size; - - /// - /// The size of the text in local space. This means that if TextSize is set to 16, a single line will have a height of 16. - /// - public float TextSize - { - get { return textSize; } - set - { - if (textSize == value) return; - - textSize = value; - - layout.Invalidate(); - } - } - - [BackgroundDependencyLoader] - private void load(FontStore store) - { - this.store = store; - - spaceWidth = CreateCharacterDrawable('.')?.DrawWidth * 2 ?? default_text_size; - - validateLayout(); - } - - private Bindable current; - - /// - /// Implements the interface. - /// - public Bindable Current - { - get { return current; } - set - { - if (current != null) - current.ValueChanged -= setText; - if (value != null) - { - value.ValueChanged += setText; - value.TriggerChange(); - } - - current = value; - } - } - - private void setText(string newText) - { - if (text == newText) - return; - - text = newText ?? string.Empty; - layout.Invalidate(); - } - - private string text = string.Empty; - - /// - /// Gets or sets the text to be displayed. - /// - public string Text - { - get { return text; } - set - { - if (current != null) - throw new InvalidOperationException($@"property {nameof(Text)} cannot be set manually if {nameof(Current)} set"); - - setText(value); - } - } - - private float? constantWidth; - /// - /// True if all characters should be spaced apart the same distance. - /// - public bool FixedWidth; - - protected override void Update() - { - base.Update(); - validateLayout(); - } - - private void validateLayout() - { - if (!layout.IsValid) - { - computeLayout(); - layout.Validate(); - } - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.Colour) > 0 && Shadow) - layout.Invalidate(); //we may need to recompute the shadow alpha if our text colour has changed (see shadowAlpha). - - return base.Invalidate(invalidation, source, shallPropagate); - } - - private string lastText; - private float lastShadowAlpha; - private string lastFont; - - private void computeLayout() - { - //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. - //squared result for quadratic fall-off seems to give the best result. - var avgColour = (Color4)DrawInfo.Colour.AverageColour; - float shadowAlpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); - - //we can't keep existing drawabled if our shadow has changed, as the shadow is applied in the add-loop. - //this could potentially be optimised if necessary. - bool allowKeepingExistingDrawables = shadowAlpha == lastShadowAlpha && font == lastFont; - - lastShadowAlpha = shadowAlpha; - lastFont = font; - - //keep sprites which haven't changed since last layout. - List keepDrawables = new List(); - - if (allowKeepingExistingDrawables) - { - if (lastText == text) - { - Children.ForEach(c => c.Scale = new Vector2(TextSize)); - return; - } - - int length = Math.Min(lastText?.Length ?? 0, text.Length); - keepDrawables.AddRange(Children.TakeWhile((n, i) => i < length && lastText[i] == text[i])); - RemoveRange(keepDrawables); //doesn't dispose - } - - Clear(); - - if (text.Length == 0) - return; - - if (FixedWidth && !constantWidth.HasValue) - constantWidth = CreateCharacterDrawable('D').DrawWidth; - - foreach (var k in keepDrawables) - { - k.Scale = new Vector2(TextSize); - Add(k); - } - - for (int index = keepDrawables.Count; index < text.Length; index++) - { - char c = text[index]; - - bool fixedWidth = FixedWidth && !FixedWidthExceptionCharacters.Contains(c); - - Drawable d; - - if (char.IsWhiteSpace(c)) - { - float width = fixedWidth ? constantWidth.GetValueOrDefault() : spaceWidth; - - switch ((int)c) - { - case 0x3000: //double-width space - width *= 2; - break; - } - - d = new Container - { - Size = new Vector2(width), - Scale = new Vector2(TextSize), - Colour = Color4.Transparent, - }; - } - else - { - d = CreateCharacterDrawable(c); - - if (fixedWidth) - { - d.Anchor = Anchor.TopCentre; - d.Origin = Anchor.TopCentre; - } - - var ctn = new Container - { - Size = new Vector2(fixedWidth ? constantWidth.GetValueOrDefault() : d.DrawSize.X, UseFullGlyphHeight ? 1 : d.DrawSize.Y), - Scale = new Vector2(TextSize), - Child = d - }; - - if (shadow && shadowAlpha > 0) - { - Drawable shadowDrawable = CreateCharacterDrawable(c); - shadowDrawable.Position = new Vector2(0, 0.06f); - shadowDrawable.Anchor = d.Anchor; - shadowDrawable.Origin = d.Origin; - shadowDrawable.Alpha = shadowAlpha; - shadowDrawable.Colour = shadowColour; - shadowDrawable.Depth = float.MaxValue; - ctn.Add(shadowDrawable); - } - - d = ctn; - } - - Add(d); - } - - lastText = text; - } - - /// - /// Creates a to use if the current font does not have a texture for a character. - /// - /// The to use if the current font does not have a texture for a character. - protected virtual Drawable CreateFallbackCharacterDrawable() => new Box - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Scale = new Vector2(0.7f) - }; - - /// - /// Creates a to use for a given character. - /// - /// The character the drawable should be created for. - /// The created for the given character. - protected virtual Drawable CreateCharacterDrawable(char c) - { - var tex = GetTextureForCharacter(c); - if (tex != null) - return new Sprite { Texture = tex }; - - return CreateFallbackCharacterDrawable(); - } - - /// - /// Gets the texture for the given character. - /// - /// The character to get the texture for. - /// The texture for the given character. - protected Texture GetTextureForCharacter(char c) - { - return store?.Get(getTextureName(c)) ?? store?.Get(getTextureName(c, false)); - } - - private string getTextureName(char c, bool useFont = true) => !useFont || string.IsNullOrEmpty(Font) ? c.ToString() : $@"{Font}/{c}"; - - public override string ToString() - { - return $@"""{Text}"" " + base.ToString(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.IO.Stores; +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Framework.Graphics.Sprites +{ + /// + /// A container for simple text rendering purposes. If more complex text rendering is required, use instead. + /// + public class SpriteText : FillFlowContainer, IHasCurrentValue, IHasLineBaseHeight, IHasText, IHasFilterTerms + { + public IEnumerable FilterTerms => new[] { Text }; + + private static readonly char[] default_fixed_width_exceptions = { '.', ':', ',' }; + + /// + /// An array of characters which should not get a fixed width in a instance. + /// + protected virtual char[] FixedWidthExceptionCharacters => default_fixed_width_exceptions; + + /// + /// Decide whether we want to make our SpriteText's vertical size to be (the full height) or precisely the size of used characters. + /// Set to false to allow better centering of individual characters/numerals/etc. + /// + public bool UseFullGlyphHeight = true; + + public override bool IsPresent => base.IsPresent && !string.IsNullOrEmpty(text); + + /// + /// True if the text should be wrapped if it gets too wide. Note that \n does NOT cause a line break. If you need explicit line breaks, use instead. + /// + public bool AllowMultiline + { + get { return Direction == FillDirection.Full; } + set { Direction = value ? FillDirection.Full : FillDirection.Horizontal; } + } + + private string font; + + /// + /// The name of the font to use when looking up textures for the individual characters. + /// + public string Font + { + get { return font; } + set + { + font = value; + layout.Invalidate(); + } + } + + private bool shadow; + + /// + /// True if a shadow should be displayed around the text. + /// + public bool Shadow + { + get { return shadow; } + set + { + if (shadow == value) return; + + shadow = value; + layout.Invalidate(); // Trigger a layout refresh + } + } + + + private Color4 shadowColour = new Color4(0f, 0f, 0f, 0.2f); + + /// + /// The colour of the shadow displayed around the text. A shadow will only be displayed if the property is set to true. + /// + public Color4 ShadowColour + { + get { return shadowColour; } + set + { + shadowColour = value; + if (shadow) + layout.Invalidate(); + } + } + + /// + /// Gets the base height of the font used by this text. If the font of this text is invalid, 0 is returned. + /// + public float LineBaseHeight + { + get + { + var baseHeight = store.GetBaseHeight(Font); + if (baseHeight.HasValue) + return baseHeight.Value * TextSize; + + if (string.IsNullOrEmpty(Text)) + return 0; + + return store.GetBaseHeight(Text[0]).GetValueOrDefault() * TextSize; + } + } + + private Cached layout = new Cached(); + + private float spaceWidth; + + private FontStore store; + + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + + /// + /// Creates a new sprite text. is set to by default. + /// + public SpriteText() + { + AutoSizeAxes = Axes.Both; + } + + private const float default_text_size = 20; + + private float textSize = default_text_size; + + /// + /// The size of the text in local space. This means that if TextSize is set to 16, a single line will have a height of 16. + /// + public float TextSize + { + get { return textSize; } + set + { + if (textSize == value) return; + + textSize = value; + + layout.Invalidate(); + } + } + + [BackgroundDependencyLoader] + private void load(FontStore store) + { + this.store = store; + + spaceWidth = CreateCharacterDrawable('.')?.DrawWidth * 2 ?? default_text_size; + + validateLayout(); + } + + private Bindable current; + + /// + /// Implements the interface. + /// + public Bindable Current + { + get { return current; } + set + { + if (current != null) + current.ValueChanged -= setText; + if (value != null) + { + value.ValueChanged += setText; + value.TriggerChange(); + } + + current = value; + } + } + + private void setText(string newText) + { + if (text == newText) + return; + + text = newText ?? string.Empty; + layout.Invalidate(); + } + + private string text = string.Empty; + + /// + /// Gets or sets the text to be displayed. + /// + public string Text + { + get { return text; } + set + { + if (current != null) + throw new InvalidOperationException($@"property {nameof(Text)} cannot be set manually if {nameof(Current)} set"); + + setText(value); + } + } + + private float? constantWidth; + /// + /// True if all characters should be spaced apart the same distance. + /// + public bool FixedWidth; + + protected override void Update() + { + base.Update(); + validateLayout(); + } + + private void validateLayout() + { + if (!layout.IsValid) + { + computeLayout(); + layout.Validate(); + } + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.Colour) > 0 && Shadow) + layout.Invalidate(); //we may need to recompute the shadow alpha if our text colour has changed (see shadowAlpha). + + return base.Invalidate(invalidation, source, shallPropagate); + } + + private string lastText; + private float lastShadowAlpha; + private string lastFont; + + private void computeLayout() + { + //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. + //squared result for quadratic fall-off seems to give the best result. + var avgColour = (Color4)DrawInfo.Colour.AverageColour; + float shadowAlpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); + + //we can't keep existing drawabled if our shadow has changed, as the shadow is applied in the add-loop. + //this could potentially be optimised if necessary. + bool allowKeepingExistingDrawables = shadowAlpha == lastShadowAlpha && font == lastFont; + + lastShadowAlpha = shadowAlpha; + lastFont = font; + + //keep sprites which haven't changed since last layout. + List keepDrawables = new List(); + + if (allowKeepingExistingDrawables) + { + if (lastText == text) + { + Children.ForEach(c => c.Scale = new Vector2(TextSize)); + return; + } + + int length = Math.Min(lastText?.Length ?? 0, text.Length); + keepDrawables.AddRange(Children.TakeWhile((n, i) => i < length && lastText[i] == text[i])); + RemoveRange(keepDrawables); //doesn't dispose + } + + Clear(); + + if (text.Length == 0) + return; + + if (FixedWidth && !constantWidth.HasValue) + constantWidth = CreateCharacterDrawable('D').DrawWidth; + + foreach (var k in keepDrawables) + { + k.Scale = new Vector2(TextSize); + Add(k); + } + + for (int index = keepDrawables.Count; index < text.Length; index++) + { + char c = text[index]; + + bool fixedWidth = FixedWidth && !FixedWidthExceptionCharacters.Contains(c); + + Drawable d; + + if (char.IsWhiteSpace(c)) + { + float width = fixedWidth ? constantWidth.GetValueOrDefault() : spaceWidth; + + switch ((int)c) + { + case 0x3000: //double-width space + width *= 2; + break; + } + + d = new Container + { + Size = new Vector2(width), + Scale = new Vector2(TextSize), + Colour = Color4.Transparent, + }; + } + else + { + d = CreateCharacterDrawable(c); + + if (fixedWidth) + { + d.Anchor = Anchor.TopCentre; + d.Origin = Anchor.TopCentre; + } + + var ctn = new Container + { + Size = new Vector2(fixedWidth ? constantWidth.GetValueOrDefault() : d.DrawSize.X, UseFullGlyphHeight ? 1 : d.DrawSize.Y), + Scale = new Vector2(TextSize), + Child = d + }; + + if (shadow && shadowAlpha > 0) + { + Drawable shadowDrawable = CreateCharacterDrawable(c); + shadowDrawable.Position = new Vector2(0, 0.06f); + shadowDrawable.Anchor = d.Anchor; + shadowDrawable.Origin = d.Origin; + shadowDrawable.Alpha = shadowAlpha; + shadowDrawable.Colour = shadowColour; + shadowDrawable.Depth = float.MaxValue; + ctn.Add(shadowDrawable); + } + + d = ctn; + } + + Add(d); + } + + lastText = text; + } + + /// + /// Creates a to use if the current font does not have a texture for a character. + /// + /// The to use if the current font does not have a texture for a character. + protected virtual Drawable CreateFallbackCharacterDrawable() => new Box + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Scale = new Vector2(0.7f) + }; + + /// + /// Creates a to use for a given character. + /// + /// The character the drawable should be created for. + /// The created for the given character. + protected virtual Drawable CreateCharacterDrawable(char c) + { + var tex = GetTextureForCharacter(c); + if (tex != null) + return new Sprite { Texture = tex }; + + return CreateFallbackCharacterDrawable(); + } + + /// + /// Gets the texture for the given character. + /// + /// The character to get the texture for. + /// The texture for the given character. + protected Texture GetTextureForCharacter(char c) + { + return store?.Get(getTextureName(c)) ?? store?.Get(getTextureName(c, false)); + } + + private string getTextureName(char c, bool useFont = true) => !useFont || string.IsNullOrEmpty(Font) ? c.ToString() : $@"{Font}/{c}"; + + public override string ToString() + { + return $@"""{Text}"" " + base.ToString(); + } + } +} diff --git a/osu.Framework/Graphics/Textures/PrefixTextureStore.cs b/osu.Framework/Graphics/Textures/PrefixTextureStore.cs index 943bdc213..95cdca868 100644 --- a/osu.Framework/Graphics/Textures/PrefixTextureStore.cs +++ b/osu.Framework/Graphics/Textures/PrefixTextureStore.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.IO.Stores; - -namespace osu.Framework.Graphics.Textures -{ - public class PrefixTextureStore : TextureStore - { - private readonly string prefix; - - public PrefixTextureStore(string prefix, IResourceStore stores) - : base(stores) - { - this.prefix = prefix; - } - - public override Texture Get(string name) - { - return base.Get($@"{prefix}-{name}"); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.IO.Stores; + +namespace osu.Framework.Graphics.Textures +{ + public class PrefixTextureStore : TextureStore + { + private readonly string prefix; + + public PrefixTextureStore(string prefix, IResourceStore stores) + : base(stores) + { + this.prefix = prefix; + } + + public override Texture Get(string name) + { + return base.Get($@"{prefix}-{name}"); + } + } +} diff --git a/osu.Framework/Graphics/Textures/RawTexture.cs b/osu.Framework/Graphics/Textures/RawTexture.cs index db8360573..b56dee7eb 100644 --- a/osu.Framework/Graphics/Textures/RawTexture.cs +++ b/osu.Framework/Graphics/Textures/RawTexture.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using PixelFormat = OpenTK.Graphics.ES30.PixelFormat; - -namespace osu.Framework.Graphics.Textures -{ - public class RawTexture - { - public int Width, Height; - public PixelFormat PixelFormat; - public byte[] Pixels; - - public static RawTexture FromStream(Stream stream) - { - using (Bitmap bitmap = new Bitmap(stream)) - { - var data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - - RawTexture t = new RawTexture - { - Width = bitmap.Width, - Height = bitmap.Height, - Pixels = new byte[data.Width * data.Height * 4], - PixelFormat = PixelFormat.Rgba - }; - - unsafe - { - //convert from BGRA (System.Drawing) to RGBA - //don't need to consider stride because we're in a raw format - var src = (byte*)data.Scan0; - - Debug.Assert(src != null); - - fixed (byte* pixels = t.Pixels) - { - var dest = pixels; - - int length = t.Pixels.Length / 4; - for (int i = 0; i < length; i++) - { - //BGRA -> RGBA - // ReSharper disable once PossibleNullReferenceException - dest[0] = src[2]; - dest[1] = src[1]; - dest[2] = src[0]; - dest[3] = src[3]; - - src += 4; - dest += 4; - } - } - } - - bitmap.UnlockBits(data); - - return t; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using PixelFormat = OpenTK.Graphics.ES30.PixelFormat; + +namespace osu.Framework.Graphics.Textures +{ + public class RawTexture + { + public int Width, Height; + public PixelFormat PixelFormat; + public byte[] Pixels; + + public static RawTexture FromStream(Stream stream) + { + using (Bitmap bitmap = new Bitmap(stream)) + { + var data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + RawTexture t = new RawTexture + { + Width = bitmap.Width, + Height = bitmap.Height, + Pixels = new byte[data.Width * data.Height * 4], + PixelFormat = PixelFormat.Rgba + }; + + unsafe + { + //convert from BGRA (System.Drawing) to RGBA + //don't need to consider stride because we're in a raw format + var src = (byte*)data.Scan0; + + Debug.Assert(src != null); + + fixed (byte* pixels = t.Pixels) + { + var dest = pixels; + + int length = t.Pixels.Length / 4; + for (int i = 0; i < length; i++) + { + //BGRA -> RGBA + // ReSharper disable once PossibleNullReferenceException + dest[0] = src[2]; + dest[1] = src[1]; + dest[2] = src[0]; + dest[3] = src[3]; + + src += 4; + dest += 4; + } + } + } + + bitmap.UnlockBits(data); + + return t; + } + } + } +} diff --git a/osu.Framework/Graphics/Textures/RawTextureLoaderStore.cs b/osu.Framework/Graphics/Textures/RawTextureLoaderStore.cs index 27f79c2ec..cf4c36c3f 100644 --- a/osu.Framework/Graphics/Textures/RawTextureLoaderStore.cs +++ b/osu.Framework/Graphics/Textures/RawTextureLoaderStore.cs @@ -1,83 +1,83 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Diagnostics; -using System.IO; -using System.Drawing; -using System.Drawing.Imaging; -using osu.Framework.IO.Stores; - -namespace osu.Framework.Graphics.Textures -{ - public class RawTextureLoaderStore : ResourceStore - { - private IResourceStore store { get; } - - public RawTextureLoaderStore(IResourceStore store) - { - this.store = store; - (store as ResourceStore)?.AddExtension(@"png"); - (store as ResourceStore)?.AddExtension(@"jpg"); - } - - private RawTexture loadOther(Stream stream) - { - RawTexture t = new RawTexture(); - using (var bmp = (Bitmap)Image.FromStream(stream)) - { - t.Pixels = new byte[bmp.Width * bmp.Height * 4]; - t.Width = bmp.Width; - t.Height = bmp.Height; - t.PixelFormat = OpenTK.Graphics.ES30.PixelFormat.Rgba; - var pixels = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), - ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); - try - { - unsafe - { - byte* p = (byte*)pixels.Scan0; - - Debug.Assert(p != null); - - int i = 0; - for (int y = 0; y < bmp.Height; y++) - { - for (int x = 0; x < bmp.Width; x++, i++) - { - int desti = i * 4; - int srci = y * pixels.Stride + x * 3; - // ReSharper disable once PossibleNullReferenceException - t.Pixels[desti] = p[srci + 2]; - t.Pixels[desti + 1] = p[srci + 1]; - t.Pixels[desti + 2] = p[srci + 0]; - t.Pixels[desti + 3] = 255; - } - } - } - } - finally - { - bmp.UnlockBits(pixels); - } - } - return t; - } - - public override RawTexture Get(string name) - { - try - { - using (var stream = store.GetStream(name)) - { - if (stream == null) return null; - - return RawTexture.FromStream(stream); - } - } - catch - { - return null; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Diagnostics; +using System.IO; +using System.Drawing; +using System.Drawing.Imaging; +using osu.Framework.IO.Stores; + +namespace osu.Framework.Graphics.Textures +{ + public class RawTextureLoaderStore : ResourceStore + { + private IResourceStore store { get; } + + public RawTextureLoaderStore(IResourceStore store) + { + this.store = store; + (store as ResourceStore)?.AddExtension(@"png"); + (store as ResourceStore)?.AddExtension(@"jpg"); + } + + private RawTexture loadOther(Stream stream) + { + RawTexture t = new RawTexture(); + using (var bmp = (Bitmap)Image.FromStream(stream)) + { + t.Pixels = new byte[bmp.Width * bmp.Height * 4]; + t.Width = bmp.Width; + t.Height = bmp.Height; + t.PixelFormat = OpenTK.Graphics.ES30.PixelFormat.Rgba; + var pixels = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), + ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); + try + { + unsafe + { + byte* p = (byte*)pixels.Scan0; + + Debug.Assert(p != null); + + int i = 0; + for (int y = 0; y < bmp.Height; y++) + { + for (int x = 0; x < bmp.Width; x++, i++) + { + int desti = i * 4; + int srci = y * pixels.Stride + x * 3; + // ReSharper disable once PossibleNullReferenceException + t.Pixels[desti] = p[srci + 2]; + t.Pixels[desti + 1] = p[srci + 1]; + t.Pixels[desti + 2] = p[srci + 0]; + t.Pixels[desti + 3] = 255; + } + } + } + } + finally + { + bmp.UnlockBits(pixels); + } + } + return t; + } + + public override RawTexture Get(string name) + { + try + { + using (var stream = store.GetStream(name)) + { + if (stream == null) return null; + + return RawTexture.FromStream(stream); + } + } + catch + { + return null; + } + } + } +} diff --git a/osu.Framework/Graphics/Textures/Texture.cs b/osu.Framework/Graphics/Textures/Texture.cs index ebab945b6..96dfa8eb5 100644 --- a/osu.Framework/Graphics/Textures/Texture.cs +++ b/osu.Framework/Graphics/Textures/Texture.cs @@ -1,204 +1,204 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.Runtime.InteropServices; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Primitives; -using OpenTK; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using Bitmap = System.Drawing.Bitmap; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; - -namespace osu.Framework.Graphics.Textures -{ - public class Texture : IDisposable - { - private static TextureWhitePixel whitePixel; - - public static Texture WhitePixel - { - get - { - if (whitePixel == null) - { - TextureAtlas atlas = new TextureAtlas(3, 3, true); - whitePixel = atlas.GetWhitePixel(); - whitePixel.SetData(new TextureUpload(new byte[] { 255, 255, 255, 255 })); - } - - return whitePixel; - } - } - - public TextureGL TextureGL; - public string Filename; - public string AssetName; - - /// - /// At what multiple of our expected resolution is our underlying texture? - /// - public float ScaleAdjust = 1; - - public bool Disposable = true; - public bool IsDisposed { get; private set; } - - public float DisplayWidth => Width / ScaleAdjust; - public float DisplayHeight => Height / ScaleAdjust; - - public Texture(TextureGL textureGl) => TextureGL = textureGl ?? throw new ArgumentNullException(nameof(textureGl)); - - public Texture(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) - : this(new TextureGLSingle(width, height, manualMipmaps, filteringMode)) - { - } - - #region Disposal - - ~Texture() - { - Dispose(false); - } - - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool isDisposing) - { - if (IsDisposed) - return; - IsDisposed = true; - } - - #endregion - - public int Width - { - get { return TextureGL.Width; } - set { TextureGL.Width = value; } - } - - public int Height - { - get { return TextureGL.Height; } - set { TextureGL.Height = value; } - } - - public Vector2 Size => new Vector2(Width, Height); - - /// - /// Turns a byte array representing BGRA colour values to a byte array representing RGBA colour values. - /// Checks whether all colour values are transparent as a byproduct. - /// - /// The bytes to process. - /// The amount of bytes to process. - /// Whether all colour values are transparent. - private static unsafe bool bgraToRgba(byte[] data, int length) - { - bool isTransparent = true; - - fixed (byte* dPtr = &data[0]) - { - byte* sp = dPtr; - byte* ep = dPtr + length; - - while (sp < ep) - { - *(uint*)sp = (uint)(*(sp + 2) | *(sp + 1) << 8 | *sp << 16 | *(sp + 3) << 24); - isTransparent &= *(sp + 3) == 0; - sp += 4; - } - } - - return isTransparent; - } - - public void SetData(TextureUpload upload) - { - TextureGL?.SetData(upload); - } - - public unsafe void SetData(Bitmap bitmap, int level = 0) - { - if (TextureGL == null) - return; - - int width = Math.Min(bitmap.Width, Width); - int height = Math.Min(bitmap.Height, Height); - - BitmapData bData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - - TextureUpload upload = new TextureUpload(width * height * 4) - { - Level = level, - Bounds = new RectangleI(0, 0, width, height) - }; - - byte[] data = upload.Data; - - const int bytes_per_pixel = 4; - byte* bDataPointer = (byte*)bData.Scan0; - - for (var y = 0; y < height; y++) - { - // This is why real scan-width is important to have! - IntPtr row = new IntPtr(bDataPointer + y * bData.Stride); - Marshal.Copy(row, data, width * bytes_per_pixel * y, width * bytes_per_pixel); - } - - bitmap.UnlockBits(bData); - - bool isTransparent = bgraToRgba(data, width * height * 4); - TextureGL.IsTransparent = isTransparent; - - if (!isTransparent) - SetData(upload); - else - upload.Dispose(); - } - - protected virtual RectangleF TextureBounds(RectangleF? textureRect = null) - { - RectangleF texRect = textureRect ?? new RectangleF(0, 0, DisplayWidth, DisplayHeight); - - if (ScaleAdjust != 1) - { - texRect.Width *= ScaleAdjust; - texRect.Height *= ScaleAdjust; - texRect.X *= ScaleAdjust; - texRect.Y *= ScaleAdjust; - } - - return texRect; - } - - public RectangleF GetTextureRect(RectangleF? textureRect = null) - { - return TextureGL.GetTextureRect(TextureBounds(textureRect)); - } - - public void DrawTriangle(Triangle vertexTriangle, ColourInfo colour, RectangleF? textureRect = null, Action vertexAction = null, Vector2? inflationPercentage = null) - { - if (TextureGL == null || !TextureGL.Bind()) return; - - TextureGL.DrawTriangle(vertexTriangle, TextureBounds(textureRect), colour, vertexAction, inflationPercentage); - } - - public void DrawQuad(Quad vertexQuad, ColourInfo colour, RectangleF? textureRect = null, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) - { - if (TextureGL == null || !TextureGL.Bind()) return; - - TextureGL.DrawQuad(vertexQuad, TextureBounds(textureRect), colour, vertexAction, inflationPercentage, blendRangeOverride); - } - - public override string ToString() => $@"{AssetName} ({Width}, {Height})"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Primitives; +using OpenTK; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; +using Bitmap = System.Drawing.Bitmap; +using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; + +namespace osu.Framework.Graphics.Textures +{ + public class Texture : IDisposable + { + private static TextureWhitePixel whitePixel; + + public static Texture WhitePixel + { + get + { + if (whitePixel == null) + { + TextureAtlas atlas = new TextureAtlas(3, 3, true); + whitePixel = atlas.GetWhitePixel(); + whitePixel.SetData(new TextureUpload(new byte[] { 255, 255, 255, 255 })); + } + + return whitePixel; + } + } + + public TextureGL TextureGL; + public string Filename; + public string AssetName; + + /// + /// At what multiple of our expected resolution is our underlying texture? + /// + public float ScaleAdjust = 1; + + public bool Disposable = true; + public bool IsDisposed { get; private set; } + + public float DisplayWidth => Width / ScaleAdjust; + public float DisplayHeight => Height / ScaleAdjust; + + public Texture(TextureGL textureGl) => TextureGL = textureGl ?? throw new ArgumentNullException(nameof(textureGl)); + + public Texture(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) + : this(new TextureGLSingle(width, height, manualMipmaps, filteringMode)) + { + } + + #region Disposal + + ~Texture() + { + Dispose(false); + } + + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (IsDisposed) + return; + IsDisposed = true; + } + + #endregion + + public int Width + { + get { return TextureGL.Width; } + set { TextureGL.Width = value; } + } + + public int Height + { + get { return TextureGL.Height; } + set { TextureGL.Height = value; } + } + + public Vector2 Size => new Vector2(Width, Height); + + /// + /// Turns a byte array representing BGRA colour values to a byte array representing RGBA colour values. + /// Checks whether all colour values are transparent as a byproduct. + /// + /// The bytes to process. + /// The amount of bytes to process. + /// Whether all colour values are transparent. + private static unsafe bool bgraToRgba(byte[] data, int length) + { + bool isTransparent = true; + + fixed (byte* dPtr = &data[0]) + { + byte* sp = dPtr; + byte* ep = dPtr + length; + + while (sp < ep) + { + *(uint*)sp = (uint)(*(sp + 2) | *(sp + 1) << 8 | *sp << 16 | *(sp + 3) << 24); + isTransparent &= *(sp + 3) == 0; + sp += 4; + } + } + + return isTransparent; + } + + public void SetData(TextureUpload upload) + { + TextureGL?.SetData(upload); + } + + public unsafe void SetData(Bitmap bitmap, int level = 0) + { + if (TextureGL == null) + return; + + int width = Math.Min(bitmap.Width, Width); + int height = Math.Min(bitmap.Height, Height); + + BitmapData bData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + TextureUpload upload = new TextureUpload(width * height * 4) + { + Level = level, + Bounds = new RectangleI(0, 0, width, height) + }; + + byte[] data = upload.Data; + + const int bytes_per_pixel = 4; + byte* bDataPointer = (byte*)bData.Scan0; + + for (var y = 0; y < height; y++) + { + // This is why real scan-width is important to have! + IntPtr row = new IntPtr(bDataPointer + y * bData.Stride); + Marshal.Copy(row, data, width * bytes_per_pixel * y, width * bytes_per_pixel); + } + + bitmap.UnlockBits(bData); + + bool isTransparent = bgraToRgba(data, width * height * 4); + TextureGL.IsTransparent = isTransparent; + + if (!isTransparent) + SetData(upload); + else + upload.Dispose(); + } + + protected virtual RectangleF TextureBounds(RectangleF? textureRect = null) + { + RectangleF texRect = textureRect ?? new RectangleF(0, 0, DisplayWidth, DisplayHeight); + + if (ScaleAdjust != 1) + { + texRect.Width *= ScaleAdjust; + texRect.Height *= ScaleAdjust; + texRect.X *= ScaleAdjust; + texRect.Y *= ScaleAdjust; + } + + return texRect; + } + + public RectangleF GetTextureRect(RectangleF? textureRect = null) + { + return TextureGL.GetTextureRect(TextureBounds(textureRect)); + } + + public void DrawTriangle(Triangle vertexTriangle, ColourInfo colour, RectangleF? textureRect = null, Action vertexAction = null, Vector2? inflationPercentage = null) + { + if (TextureGL == null || !TextureGL.Bind()) return; + + TextureGL.DrawTriangle(vertexTriangle, TextureBounds(textureRect), colour, vertexAction, inflationPercentage); + } + + public void DrawQuad(Quad vertexQuad, ColourInfo colour, RectangleF? textureRect = null, Action vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null) + { + if (TextureGL == null || !TextureGL.Bind()) return; + + TextureGL.DrawQuad(vertexQuad, TextureBounds(textureRect), colour, vertexAction, inflationPercentage, blendRangeOverride); + } + + public override string ToString() => $@"{AssetName} ({Width}, {Height})"; + } +} diff --git a/osu.Framework/Graphics/Textures/TextureAtlas.cs b/osu.Framework/Graphics/Textures/TextureAtlas.cs index d4d749538..b00e99b13 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas.cs @@ -1,122 +1,122 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Graphics.OpenGL.Textures; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Logging; - -namespace osu.Framework.Graphics.Textures -{ - public class TextureAtlas - { - // We are adding an extra padding on top of the padding required by - // mipmap blending in order to support smooth edges without antialiasing which requires - // inflating texture rectangles. - private const int padding = (1 << TextureGLSingle.MAX_MIPMAP_LEVELS) + Sprite.MAX_EDGE_SMOOTHNESS * 2; - - private readonly List subTextureBounds = new List(); - private TextureGLSingle atlasTexture; - - private readonly int atlasWidth; - private readonly int atlasHeight; - - private int currentY; - - private int mipmapLevels => (int)Math.Log(atlasWidth, 2); - - private readonly bool manualMipmaps; - private readonly All filteringMode; - private readonly object textureRetrievalLock = new object(); - - public TextureAtlas(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) - { - atlasWidth = width; - atlasHeight = height; - this.manualMipmaps = manualMipmaps; - this.filteringMode = filteringMode; - } - - public void Reset() - { - subTextureBounds.Clear(); - currentY = 0; - - //may be zero in a headless context. - if (atlasWidth == 0 || atlasHeight == 0) - return; - - if (atlasTexture == null) - Logger.Log($"New TextureAtlas initialised {atlasWidth}x{atlasHeight}", LoggingTarget.Runtime, LogLevel.Debug); - - atlasTexture = new TextureGLAtlas(atlasWidth, atlasHeight, manualMipmaps, filteringMode); - - using (var whiteTex = Add(3, 3)) - { - //add an empty white rect to use for solid box drawing (shader optimisation). - byte[] white = new byte[whiteTex.Width * whiteTex.Height * 4]; - for (int i = 0; i < white.Length; i++) - white[i] = 255; - whiteTex.SetData(new TextureUpload(white)); - } - } - - private Vector2I findPosition(int width, int height) - { - if (atlasHeight == 0 || atlasWidth == 0) return Vector2I.Zero; - - if (currentY + height > atlasHeight) - { - Logger.Log($"TextureAtlas size exceeded; generating new {atlasWidth}x{atlasHeight} texture", LoggingTarget.Performance); - Reset(); - } - - // Super naive implementation only going from left to right. - Vector2I res = new Vector2I(0, currentY); - - int maxY = currentY; - foreach (RectangleI bounds in subTextureBounds) - { - // +1 is required to prevent aliasing issues with sub-pixel positions while drawing. Bordering edged of other textures can show without it. - res.X = Math.Max(res.X, bounds.Right + padding); - maxY = Math.Max(maxY, bounds.Bottom); - } - - if (res.X + width > atlasWidth) - { - // +1 is required to prevent aliasing issues with sub-pixel positions while drawing. Bordering edged of other textures can show without it. - currentY = maxY + padding; - subTextureBounds.Clear(); - res = findPosition(width, height); - } - - return res; - } - - internal Texture Add(int width, int height) - { - lock (textureRetrievalLock) - { - if (atlasTexture == null) - Reset(); - - Vector2I position = findPosition(width, height); - RectangleI bounds = new RectangleI(position.X, position.Y, width, height); - subTextureBounds.Add(bounds); - - return new Texture(new TextureGLSub(bounds, atlasTexture)); - } - } - - internal TextureWhitePixel GetWhitePixel() - { - if (atlasTexture == null) - Reset(); - - return new TextureWhitePixel(new TextureGLAtlasWhite(atlasTexture)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.OpenGL.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Logging; + +namespace osu.Framework.Graphics.Textures +{ + public class TextureAtlas + { + // We are adding an extra padding on top of the padding required by + // mipmap blending in order to support smooth edges without antialiasing which requires + // inflating texture rectangles. + private const int padding = (1 << TextureGLSingle.MAX_MIPMAP_LEVELS) + Sprite.MAX_EDGE_SMOOTHNESS * 2; + + private readonly List subTextureBounds = new List(); + private TextureGLSingle atlasTexture; + + private readonly int atlasWidth; + private readonly int atlasHeight; + + private int currentY; + + private int mipmapLevels => (int)Math.Log(atlasWidth, 2); + + private readonly bool manualMipmaps; + private readonly All filteringMode; + private readonly object textureRetrievalLock = new object(); + + public TextureAtlas(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) + { + atlasWidth = width; + atlasHeight = height; + this.manualMipmaps = manualMipmaps; + this.filteringMode = filteringMode; + } + + public void Reset() + { + subTextureBounds.Clear(); + currentY = 0; + + //may be zero in a headless context. + if (atlasWidth == 0 || atlasHeight == 0) + return; + + if (atlasTexture == null) + Logger.Log($"New TextureAtlas initialised {atlasWidth}x{atlasHeight}", LoggingTarget.Runtime, LogLevel.Debug); + + atlasTexture = new TextureGLAtlas(atlasWidth, atlasHeight, manualMipmaps, filteringMode); + + using (var whiteTex = Add(3, 3)) + { + //add an empty white rect to use for solid box drawing (shader optimisation). + byte[] white = new byte[whiteTex.Width * whiteTex.Height * 4]; + for (int i = 0; i < white.Length; i++) + white[i] = 255; + whiteTex.SetData(new TextureUpload(white)); + } + } + + private Vector2I findPosition(int width, int height) + { + if (atlasHeight == 0 || atlasWidth == 0) return Vector2I.Zero; + + if (currentY + height > atlasHeight) + { + Logger.Log($"TextureAtlas size exceeded; generating new {atlasWidth}x{atlasHeight} texture", LoggingTarget.Performance); + Reset(); + } + + // Super naive implementation only going from left to right. + Vector2I res = new Vector2I(0, currentY); + + int maxY = currentY; + foreach (RectangleI bounds in subTextureBounds) + { + // +1 is required to prevent aliasing issues with sub-pixel positions while drawing. Bordering edged of other textures can show without it. + res.X = Math.Max(res.X, bounds.Right + padding); + maxY = Math.Max(maxY, bounds.Bottom); + } + + if (res.X + width > atlasWidth) + { + // +1 is required to prevent aliasing issues with sub-pixel positions while drawing. Bordering edged of other textures can show without it. + currentY = maxY + padding; + subTextureBounds.Clear(); + res = findPosition(width, height); + } + + return res; + } + + internal Texture Add(int width, int height) + { + lock (textureRetrievalLock) + { + if (atlasTexture == null) + Reset(); + + Vector2I position = findPosition(width, height); + RectangleI bounds = new RectangleI(position.X, position.Y, width, height); + subTextureBounds.Add(bounds); + + return new Texture(new TextureGLSub(bounds, atlasTexture)); + } + } + + internal TextureWhitePixel GetWhitePixel() + { + if (atlasTexture == null) + Reset(); + + return new TextureWhitePixel(new TextureGLAtlasWhite(atlasTexture)); + } + } +} diff --git a/osu.Framework/Graphics/Textures/TextureLoader.cs b/osu.Framework/Graphics/Textures/TextureLoader.cs index 8d5b5846b..25931ffc8 100644 --- a/osu.Framework/Graphics/Textures/TextureLoader.cs +++ b/osu.Framework/Graphics/Textures/TextureLoader.cs @@ -1,96 +1,96 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Drawing; -using System.IO; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL.Textures; - -namespace osu.Framework.Graphics.Textures -{ - public static class TextureLoader - { - /// - /// Creates a texture from a bitmap. - /// - /// The bitmap to create the texture from. - /// The atlas to add the texture to. - /// The created texture. - public static Texture FromBitmap(Bitmap bitmap, TextureAtlas atlas = null) - { - if (bitmap == null) - return null; - - //int usableWidth = Math.Min(GLWrapper.MaxTextureSize, bitmap.Width); - //int usableHeight = Math.Min(GLWrapper.MaxTextureSize, bitmap.Height); - - Texture tex = atlas == null ? new Texture(bitmap.Width, bitmap.Height) : atlas.Add(bitmap.Width, bitmap.Height); - tex.SetData(bitmap); - - return tex; - } - - /// - /// Creates a texture from a data stream representing a bitmap. - /// - /// The data stream containing the texture data. - /// The atlas to add the texture to. - /// The created texture. - public static Texture FromStream(Stream stream, TextureAtlas atlas = null) - { - if (stream == null || stream.Length == 0) - return null; - - try - { - using (Bitmap b = (Bitmap)Image.FromStream(stream, false, false)) - return FromBitmap(b, atlas); - } - catch (ArgumentException) - { - return null; - } - } - - /// - /// Creates a texture from bytes representing a bitmap. - /// - /// The bytes representing a bitmap. - /// The atlas to add the texture to. - /// The created texture. - public static Texture FromBytes(byte[] data, TextureAtlas atlas = null) - { - //todo: can be optimised with TextureUpload here to avoid allocations. - if (data == null) - return null; - - using (MemoryStream ms = new MemoryStream(data)) - return FromStream(ms, atlas); - } - - /// - /// Creates a texture from bytes laid out in BGRA format, row major. - /// - /// The raw bytes containing the texture in provided format, row major. - /// Width of the texture in pixels. - /// Height of the texture in pixels. - /// The atlas to add the texture to. - /// The pixel format of the data. - /// The created texture. - public static Texture FromRawBytes(byte[] data, int width, int height, TextureAtlas atlas = null, PixelFormat format = PixelFormat.Rgba) - { - if (data == null) - return null; - - Texture tex = atlas == null ? new Texture(width, height) : atlas.Add(width, height); - - var upload = new TextureUpload(data) - { - Format = format - }; - tex.SetData(upload); - return tex; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Drawing; +using System.IO; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL.Textures; + +namespace osu.Framework.Graphics.Textures +{ + public static class TextureLoader + { + /// + /// Creates a texture from a bitmap. + /// + /// The bitmap to create the texture from. + /// The atlas to add the texture to. + /// The created texture. + public static Texture FromBitmap(Bitmap bitmap, TextureAtlas atlas = null) + { + if (bitmap == null) + return null; + + //int usableWidth = Math.Min(GLWrapper.MaxTextureSize, bitmap.Width); + //int usableHeight = Math.Min(GLWrapper.MaxTextureSize, bitmap.Height); + + Texture tex = atlas == null ? new Texture(bitmap.Width, bitmap.Height) : atlas.Add(bitmap.Width, bitmap.Height); + tex.SetData(bitmap); + + return tex; + } + + /// + /// Creates a texture from a data stream representing a bitmap. + /// + /// The data stream containing the texture data. + /// The atlas to add the texture to. + /// The created texture. + public static Texture FromStream(Stream stream, TextureAtlas atlas = null) + { + if (stream == null || stream.Length == 0) + return null; + + try + { + using (Bitmap b = (Bitmap)Image.FromStream(stream, false, false)) + return FromBitmap(b, atlas); + } + catch (ArgumentException) + { + return null; + } + } + + /// + /// Creates a texture from bytes representing a bitmap. + /// + /// The bytes representing a bitmap. + /// The atlas to add the texture to. + /// The created texture. + public static Texture FromBytes(byte[] data, TextureAtlas atlas = null) + { + //todo: can be optimised with TextureUpload here to avoid allocations. + if (data == null) + return null; + + using (MemoryStream ms = new MemoryStream(data)) + return FromStream(ms, atlas); + } + + /// + /// Creates a texture from bytes laid out in BGRA format, row major. + /// + /// The raw bytes containing the texture in provided format, row major. + /// Width of the texture in pixels. + /// Height of the texture in pixels. + /// The atlas to add the texture to. + /// The pixel format of the data. + /// The created texture. + public static Texture FromRawBytes(byte[] data, int width, int height, TextureAtlas atlas = null, PixelFormat format = PixelFormat.Rgba) + { + if (data == null) + return null; + + Texture tex = atlas == null ? new Texture(width, height) : atlas.Add(width, height); + + var upload = new TextureUpload(data) + { + Format = format + }; + tex.SetData(upload); + return tex; + } + } +} diff --git a/osu.Framework/Graphics/Textures/TextureStore.cs b/osu.Framework/Graphics/Textures/TextureStore.cs index 8f11db4c7..388189041 100644 --- a/osu.Framework/Graphics/Textures/TextureStore.cs +++ b/osu.Framework/Graphics/Textures/TextureStore.cs @@ -1,78 +1,78 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Concurrent; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.IO.Stores; -using System; -using System.Threading; -using osu.Framework.Graphics.Primitives; -using OpenTK.Graphics.ES30; - -namespace osu.Framework.Graphics.Textures -{ - public class TextureStore : ResourceStore - { - private readonly ConcurrentDictionary> textureCache = new ConcurrentDictionary>(); - - private readonly All filteringMode; - private readonly TextureAtlas atlas; - - /// - /// Decides at what resolution multiple this texturestore is providing sprites at. - /// ie. if we are providing high resolution (at 2x the resolution of standard 1366x768) sprites this should be 2. - /// - public float ScaleAdjust = 2; - - public TextureStore(IResourceStore store = null, bool useAtlas = true, All filteringMode = All.Linear) - : base(store) - { - this.filteringMode = filteringMode; - AddExtension(@"png"); - AddExtension(@"jpg"); - - if (useAtlas) - atlas = new TextureAtlas(GLWrapper.MaxTextureSize, GLWrapper.MaxTextureSize, filteringMode: filteringMode); - } - - private Texture getTexture(string name) - { - RawTexture raw = base.Get($@"{name}"); - if (raw == null) return null; - - Texture tex = atlas != null ? atlas.Add(raw.Width, raw.Height) : new Texture(raw.Width, raw.Height, filteringMode: filteringMode); - tex.SetData(new TextureUpload(raw.Pixels) - { - Bounds = new RectangleI(0, 0, raw.Width, raw.Height), - Format = raw.PixelFormat, - }); - - return tex; - } - - /// - /// Retrieves a texture from the store and adds it to the atlas. - /// - /// The name of the texture. - /// The texture. - public new virtual Texture Get(string name) - { - if (string.IsNullOrEmpty(name)) return null; - - var cachedTex = textureCache.GetOrAdd(name, n => - //Laziness ensure we are only ever creating the texture once (and blocking on other access until it is done). - new Lazy(() => getTexture(name)?.TextureGL, LazyThreadSafetyMode.ExecutionAndPublication)).Value; - - if (cachedTex == null) return null; - - //use existing TextureGL (but provide a new texture instance). - var tex = new Texture(cachedTex) - { - ScaleAdjust = ScaleAdjust - }; - - return tex; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Concurrent; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.IO.Stores; +using System; +using System.Threading; +using osu.Framework.Graphics.Primitives; +using OpenTK.Graphics.ES30; + +namespace osu.Framework.Graphics.Textures +{ + public class TextureStore : ResourceStore + { + private readonly ConcurrentDictionary> textureCache = new ConcurrentDictionary>(); + + private readonly All filteringMode; + private readonly TextureAtlas atlas; + + /// + /// Decides at what resolution multiple this texturestore is providing sprites at. + /// ie. if we are providing high resolution (at 2x the resolution of standard 1366x768) sprites this should be 2. + /// + public float ScaleAdjust = 2; + + public TextureStore(IResourceStore store = null, bool useAtlas = true, All filteringMode = All.Linear) + : base(store) + { + this.filteringMode = filteringMode; + AddExtension(@"png"); + AddExtension(@"jpg"); + + if (useAtlas) + atlas = new TextureAtlas(GLWrapper.MaxTextureSize, GLWrapper.MaxTextureSize, filteringMode: filteringMode); + } + + private Texture getTexture(string name) + { + RawTexture raw = base.Get($@"{name}"); + if (raw == null) return null; + + Texture tex = atlas != null ? atlas.Add(raw.Width, raw.Height) : new Texture(raw.Width, raw.Height, filteringMode: filteringMode); + tex.SetData(new TextureUpload(raw.Pixels) + { + Bounds = new RectangleI(0, 0, raw.Width, raw.Height), + Format = raw.PixelFormat, + }); + + return tex; + } + + /// + /// Retrieves a texture from the store and adds it to the atlas. + /// + /// The name of the texture. + /// The texture. + public new virtual Texture Get(string name) + { + if (string.IsNullOrEmpty(name)) return null; + + var cachedTex = textureCache.GetOrAdd(name, n => + //Laziness ensure we are only ever creating the texture once (and blocking on other access until it is done). + new Lazy(() => getTexture(name)?.TextureGL, LazyThreadSafetyMode.ExecutionAndPublication)).Value; + + if (cachedTex == null) return null; + + //use existing TextureGL (but provide a new texture instance). + var tex = new Texture(cachedTex) + { + ScaleAdjust = ScaleAdjust + }; + + return tex; + } + } +} diff --git a/osu.Framework/Graphics/Textures/TextureWhitePixel.cs b/osu.Framework/Graphics/Textures/TextureWhitePixel.cs index aa62f37b6..f944d0c3d 100644 --- a/osu.Framework/Graphics/Textures/TextureWhitePixel.cs +++ b/osu.Framework/Graphics/Textures/TextureWhitePixel.cs @@ -1,34 +1,34 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.OpenGL.Textures; -using osu.Framework.Graphics.Primitives; -using System; - -namespace osu.Framework.Graphics.Textures -{ - internal class TextureWhitePixel : Texture - { - public TextureWhitePixel(TextureGL textureGl) - : base(textureGl) - { - } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - throw new InvalidOperationException($"May not dispose {nameof(TextureWhitePixel)} explicitly."); - base.Dispose(false); - } - - protected override RectangleF TextureBounds(RectangleF? textureRect = null) - { - // We need non-zero texture bounds for EdgeSmoothness to work correctly. - // Let's be very conservative and use a tenth of the size of a pixel in the - // largest possible texture. - float smallestPixelTenth = 0.1f / GLWrapper.MaxTextureSize; - return new RectangleF(0, 0, smallestPixelTenth, smallestPixelTenth); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Primitives; +using System; + +namespace osu.Framework.Graphics.Textures +{ + internal class TextureWhitePixel : Texture + { + public TextureWhitePixel(TextureGL textureGl) + : base(textureGl) + { + } + + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + throw new InvalidOperationException($"May not dispose {nameof(TextureWhitePixel)} explicitly."); + base.Dispose(false); + } + + protected override RectangleF TextureBounds(RectangleF? textureRect = null) + { + // We need non-zero texture bounds for EdgeSmoothness to work correctly. + // Let's be very conservative and use a tenth of the size of a pixel in the + // largest possible texture. + float smallestPixelTenth = 0.1f / GLWrapper.MaxTextureSize; + return new RectangleF(0, 0, smallestPixelTenth, smallestPixelTenth); + } + } +} diff --git a/osu.Framework/Graphics/TransformSequenceExtensions.cs b/osu.Framework/Graphics/TransformSequenceExtensions.cs index bcfcffa1b..29433eee0 100644 --- a/osu.Framework/Graphics/TransformSequenceExtensions.cs +++ b/osu.Framework/Graphics/TransformSequenceExtensions.cs @@ -1,236 +1,236 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Threading; -using System; - -namespace osu.Framework.Graphics -{ - public static class TransformSequenceExtensions - { - public static TransformSequence Expire(this TransformSequence t) - where T : Drawable => - t.Append(o => o.Expire()); - - public static TransformSequence Schedule(this TransformSequence t, Action scheduledAction) - where T : Drawable => - t.Append(o => o.Schedule(scheduledAction)); - - public static TransformSequence Schedule(this TransformSequence t, Action scheduledAction, out ScheduledDelegate scheduledDelegate) - where T : Drawable => - t.Append(o => o.Schedule(scheduledAction), out scheduledDelegate); - - public static TransformSequence Spin(this TransformSequence t, double revolutionDuration, RotationDirection direction, float startRotation = 0) - where T : Drawable => - t.Loop(d => d.RotateTo(startRotation).RotateTo(startRotation + (direction == RotationDirection.Clockwise ? 360 : -360), revolutionDuration)); - - public static TransformSequence Spin(this TransformSequence t, double revolutionDuration, RotationDirection direction, float startRotation, int numRevolutions) - where T : Drawable => - t.Loop(0, numRevolutions, d => d.RotateTo(startRotation).RotateTo(startRotation + (direction == RotationDirection.Clockwise ? 360 : -360), revolutionDuration)); - - /// - /// Smoothly adjusts to 1 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeIn(this TransformSequence t, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeIn(duration, easing)); - - /// - /// Smoothly adjusts from 0 to 1 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeInFromZero(this TransformSequence t, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeInFromZero(duration, easing)); - - /// - /// Smoothly adjusts to 0 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeOut(this TransformSequence t, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeOut(duration, easing)); - - /// - /// Smoothly adjusts from 1 to 0 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeOutFromOne(this TransformSequence t, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeOutFromOne(duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeTo(this TransformSequence t, float newAlpha, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeTo(newAlpha, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeColour(this TransformSequence t, ColourInfo newColour, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FadeColour(newColour, duration, easing)); - - /// - /// Instantaneously flashes , then smoothly changes it back over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FlashColour(this TransformSequence t, ColourInfo flashColour, double duration, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.FlashColour(flashColour, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence RotateTo(this TransformSequence t, float newRotation, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.RotateTo(newRotation, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ScaleTo(this TransformSequence t, float newScale, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ScaleTo(newScale, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ScaleTo(this TransformSequence t, Vector2 newScale, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ScaleTo(newScale, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeTo(this TransformSequence t, float newSize, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ResizeTo(newSize, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeTo(this TransformSequence t, Vector2 newSize, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ResizeTo(newSize, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeWidthTo(this TransformSequence t, float newWidth, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ResizeWidthTo(newWidth, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeHeightTo(this TransformSequence t, float newHeight, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.ResizeHeightTo(newHeight, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveTo(this TransformSequence t, Vector2 newPosition, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.MoveTo(newPosition, duration, easing)); - - /// - /// Smoothly adjusts or over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveTo(this TransformSequence t, Direction direction, float destination, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.MoveTo(direction, destination, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToX(this TransformSequence t, float destination, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.MoveToX(destination, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToY(this TransformSequence t, float destination, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.MoveToY(destination, duration, easing)); - - /// - /// Smoothly adjusts by an offset to its final value over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToOffset(this TransformSequence t, Vector2 offset, double duration = 0, Easing easing = Easing.None) - where T : Drawable => - t.Append(o => o.MoveToOffset(offset, duration, easing)); - - /// - /// Smoothly adjusts the alpha channel of the colour of over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeEdgeEffectTo(this TransformSequence t, float newAlpha, double duration, Easing easing = Easing.None) - where T : IContainer => - t.Append(o => o.FadeEdgeEffectTo(newAlpha, duration, easing)); - - /// - /// Smoothly adjusts the colour of over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeEdgeEffectTo(this TransformSequence t, Color4 newColour, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - t.Append(o => o.FadeEdgeEffectTo(newColour, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformRelativeChildSizeTo(this TransformSequence t, Vector2 newSize, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - t.Append(o => o.TransformRelativeChildSizeTo(newSize, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformRelativeChildOffsetTo(this TransformSequence t, Vector2 newOffset, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - t.Append(o => o.TransformRelativeChildOffsetTo(newOffset, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence BlurTo(this TransformSequence t, Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None) - where T : IBufferedContainer => - t.Append(o => o.BlurTo(newBlurSigma, duration, easing)); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformSpacingTo(this TransformSequence t, Vector2 newSpacing, double duration = 0, Easing easing = Easing.None) - where T : IFillFlowContainer => - t.Append(o => o.TransformSpacingTo(newSpacing, duration, easing)); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Threading; +using System; + +namespace osu.Framework.Graphics +{ + public static class TransformSequenceExtensions + { + public static TransformSequence Expire(this TransformSequence t) + where T : Drawable => + t.Append(o => o.Expire()); + + public static TransformSequence Schedule(this TransformSequence t, Action scheduledAction) + where T : Drawable => + t.Append(o => o.Schedule(scheduledAction)); + + public static TransformSequence Schedule(this TransformSequence t, Action scheduledAction, out ScheduledDelegate scheduledDelegate) + where T : Drawable => + t.Append(o => o.Schedule(scheduledAction), out scheduledDelegate); + + public static TransformSequence Spin(this TransformSequence t, double revolutionDuration, RotationDirection direction, float startRotation = 0) + where T : Drawable => + t.Loop(d => d.RotateTo(startRotation).RotateTo(startRotation + (direction == RotationDirection.Clockwise ? 360 : -360), revolutionDuration)); + + public static TransformSequence Spin(this TransformSequence t, double revolutionDuration, RotationDirection direction, float startRotation, int numRevolutions) + where T : Drawable => + t.Loop(0, numRevolutions, d => d.RotateTo(startRotation).RotateTo(startRotation + (direction == RotationDirection.Clockwise ? 360 : -360), revolutionDuration)); + + /// + /// Smoothly adjusts to 1 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeIn(this TransformSequence t, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeIn(duration, easing)); + + /// + /// Smoothly adjusts from 0 to 1 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeInFromZero(this TransformSequence t, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeInFromZero(duration, easing)); + + /// + /// Smoothly adjusts to 0 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeOut(this TransformSequence t, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeOut(duration, easing)); + + /// + /// Smoothly adjusts from 1 to 0 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeOutFromOne(this TransformSequence t, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeOutFromOne(duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeTo(this TransformSequence t, float newAlpha, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeTo(newAlpha, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeColour(this TransformSequence t, ColourInfo newColour, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FadeColour(newColour, duration, easing)); + + /// + /// Instantaneously flashes , then smoothly changes it back over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FlashColour(this TransformSequence t, ColourInfo flashColour, double duration, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.FlashColour(flashColour, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence RotateTo(this TransformSequence t, float newRotation, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.RotateTo(newRotation, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ScaleTo(this TransformSequence t, float newScale, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ScaleTo(newScale, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ScaleTo(this TransformSequence t, Vector2 newScale, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ScaleTo(newScale, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeTo(this TransformSequence t, float newSize, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ResizeTo(newSize, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeTo(this TransformSequence t, Vector2 newSize, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ResizeTo(newSize, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeWidthTo(this TransformSequence t, float newWidth, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ResizeWidthTo(newWidth, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeHeightTo(this TransformSequence t, float newHeight, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.ResizeHeightTo(newHeight, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveTo(this TransformSequence t, Vector2 newPosition, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.MoveTo(newPosition, duration, easing)); + + /// + /// Smoothly adjusts or over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveTo(this TransformSequence t, Direction direction, float destination, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.MoveTo(direction, destination, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToX(this TransformSequence t, float destination, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.MoveToX(destination, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToY(this TransformSequence t, float destination, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.MoveToY(destination, duration, easing)); + + /// + /// Smoothly adjusts by an offset to its final value over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToOffset(this TransformSequence t, Vector2 offset, double duration = 0, Easing easing = Easing.None) + where T : Drawable => + t.Append(o => o.MoveToOffset(offset, duration, easing)); + + /// + /// Smoothly adjusts the alpha channel of the colour of over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeEdgeEffectTo(this TransformSequence t, float newAlpha, double duration, Easing easing = Easing.None) + where T : IContainer => + t.Append(o => o.FadeEdgeEffectTo(newAlpha, duration, easing)); + + /// + /// Smoothly adjusts the colour of over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeEdgeEffectTo(this TransformSequence t, Color4 newColour, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + t.Append(o => o.FadeEdgeEffectTo(newColour, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformRelativeChildSizeTo(this TransformSequence t, Vector2 newSize, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + t.Append(o => o.TransformRelativeChildSizeTo(newSize, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformRelativeChildOffsetTo(this TransformSequence t, Vector2 newOffset, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + t.Append(o => o.TransformRelativeChildOffsetTo(newOffset, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence BlurTo(this TransformSequence t, Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None) + where T : IBufferedContainer => + t.Append(o => o.BlurTo(newBlurSigma, duration, easing)); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformSpacingTo(this TransformSequence t, Vector2 newSpacing, double duration = 0, Easing easing = Easing.None) + where T : IFillFlowContainer => + t.Append(o => o.TransformSpacingTo(newSpacing, duration, easing)); + } +} diff --git a/osu.Framework/Graphics/TransformableExtensions.cs b/osu.Framework/Graphics/TransformableExtensions.cs index 8d32a6c16..e85ea77c3 100644 --- a/osu.Framework/Graphics/TransformableExtensions.cs +++ b/osu.Framework/Graphics/TransformableExtensions.cs @@ -1,411 +1,411 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; -using System; -using System.Linq; - -namespace osu.Framework.Graphics -{ - public static class TransformableExtensions - { - /// - /// Transforms a given property or field member of a given to . - /// The value of the given member is smoothly changed over time using the given for tweening. - /// - /// The type of the to apply the to. - /// The value type which is being transformed. - /// The to apply the to. - /// The property or field name of the member ot to transform. - /// The value to transform to. - /// The transform duration. - /// The transform easing to be used for tweening. - /// A to which further transforms can be added. - public static TransformSequence TransformTo(this TThis t, string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) - where TThis : ITransformable => - t.TransformTo(t.MakeTransform(propertyOrFieldName, newValue, duration, easing)); - - /// - /// Applies a to a given . - /// - /// The type of the to apply the to. - /// The to apply the to. - /// The transform to use. - /// A to which further transforms can be added. - public static TransformSequence TransformTo(this TThis t, Transform transform) where TThis : ITransformable - { - var result = new TransformSequence(t); - result.Add(transform); - t.AddTransform(transform); - return result; - } - - /// - /// Creates a for smoothly changing - /// over time using the given for tweening. - /// - /// is invoked as part of this method. - /// - /// The type of the the can be applied to. - /// The value type which is being transformed. - /// The the will be applied to. - /// The property or field name of the member ot to transform. - /// The value to transform to. - /// The transform duration. - /// The transform easing to be used for tweening. - /// The resulting . - public static Transform MakeTransform(this TThis t, string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) - where TThis : ITransformable => - t.PopulateTransform(new TransformCustom(propertyOrFieldName), newValue, duration, easing); - - /// - /// Populates a newly created with necessary values. - /// All s must be populated by this method prior to being used. - /// - /// The type of the the can be applied to. - /// The value type which is being transformed. - /// The the will be applied to. - /// The transform to populate. - /// The value to transform to. - /// The transform duration. - /// The transform easing to be used for tweening. - /// The populated . - public static Transform PopulateTransform(this TThis t, Transform transform, TValue newValue, double duration = 0, Easing easing = Easing.None) - where TThis : ITransformable - { - if (duration < 0) - throw new ArgumentOutOfRangeException(nameof(duration), $"{nameof(duration)} must be positive."); - - if (transform.Target != null) - throw new InvalidOperationException($"May not {nameof(PopulateTransform)} the same {nameof(Transform)} more than once."); - - transform.Target = t; - - double startTime = t.TransformStartTime; - - transform.StartTime = startTime; - transform.EndTime = startTime + duration; - transform.EndValue = newValue; - transform.Easing = easing; - - return transform; - } - - /// - /// Applies via TransformSequence.Append(IEnumerable{Generator})/>. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The optional Generators for s to be appended. - /// This . - public static TransformSequence Animate(this T transformable, params TransformSequence.Generator[] childGenerators) where T : ITransformable => - transformable.Delay(0, childGenerators); - - /// - /// Advances the start time of future appended s by milliseconds. - /// Then, are appended via TransformSequence.Append(IEnumerable{Generator})/>. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The delay to advance the start time by. - /// The optional Generators for s to be appended. - /// This . - public static TransformSequence Delay(this T transformable, double delay, params TransformSequence.Generator[] childGenerators) where T : ITransformable => - new TransformSequence(transformable).Delay(delay, childGenerators); - - /// - /// Returns a which waits for all existing transforms to finish. - /// - /// A which has a delay waiting for all transforms to be completed. - public static TransformSequence DelayUntilTransformsFinished(this T transformable) - where T : Transformable - { - return transformable.Delay(Math.Max(0, transformable.LatestTransformEndTime - transformable.Time.Current)); - } - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated times - /// with milliseconds between iterations. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The pause between iterations in milliseconds. - /// The number of iterations. - /// The functions to generate the s to be looped. - /// This . - public static TransformSequence Loop(this T transformable, double pause, int numIters, params TransformSequence.Generator[] childGenerators) - where T : ITransformable => - transformable.Delay(0).Loop(pause, numIters, childGenerators); - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated indefinitely with - /// milliseconds between iterations. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The pause between iterations in milliseconds. - /// The functions to generate the s to be looped. - /// This . - public static TransformSequence Loop(this T transformable, double pause, params TransformSequence.Generator[] childGenerators) - where T : ITransformable => - transformable.Delay(0).Loop(pause, childGenerators); - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated indefinitely. - /// milliseconds between iterations. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The functions to generate the s to be looped. - /// This . - public static TransformSequence Loop(this T transformable, params TransformSequence.Generator[] childGenerators) - where T : ITransformable => - transformable.Delay(0).Loop(childGenerators); - - /// - /// Append a looping to this to repeat indefinitely with - /// milliseconds between iterations. - /// - /// The type of the the can be applied to. - /// The the will be applied to. - /// The pause between iterations in milliseconds. - /// This . - public static TransformSequence Loop(this T transformable, double pause = 0) - where T : ITransformable => - transformable.Delay(0).Loop(pause); - - /// - /// Rotate over one full rotation with provided parameters. - /// - /// A to which further transforms can be added. - public static TransformSequence Spin(this T drawable, double revolutionDuration, RotationDirection direction, float startRotation = 0) where T : Drawable => - drawable.Delay(0).Spin(revolutionDuration, direction, startRotation); - - /// - /// Rotate times with provided parameters. - /// - /// A to which further transforms can be added. - public static TransformSequence Spin(this T drawable, double revolutionDuration, RotationDirection direction, float startRotation, int numRevolutions) where T : Drawable => - drawable.Delay(0).Spin(revolutionDuration, direction, startRotation, numRevolutions); - - /// - /// Smoothly adjusts to 1 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeIn(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.FadeTo(1, duration, easing); - - /// - /// Smoothly adjusts from 0 to 1 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeInFromZero(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.FadeTo(0).FadeIn(duration, easing); - - /// - /// Smoothly adjusts to 0 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeOut(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.FadeTo(0, duration, easing); - - /// - /// Smoothly adjusts from 1 to 0 over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeOutFromOne(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.FadeTo(1).FadeOut(duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeTo(this T drawable, float newAlpha, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Alpha), newAlpha, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeColour(this T drawable, ColourInfo newColour, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Colour), newColour, duration, easing); - - /// - /// Instantaneously flashes , then smoothly changes it back over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FlashColour(this T drawable, ColourInfo flashColour, double duration, Easing easing = Easing.None) where T : Drawable - { - ColourInfo endValue = (drawable.Transforms.LastOrDefault(t => t.TargetMember == nameof(drawable.Colour)) as Transform)?.EndValue ?? drawable.Colour; - return drawable.FadeColour(flashColour).FadeColour(endValue, duration, easing); - } - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence RotateTo(this T drawable, float newRotation, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Rotation), newRotation, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ScaleTo(this T drawable, float newScale, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.ScaleTo(new Vector2(newScale), duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ScaleTo(this T drawable, Vector2 newScale, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Scale), newScale, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeTo(this T drawable, float newSize, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.ResizeTo(new Vector2(newSize), duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeTo(this T drawable, Vector2 newSize, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Size), newSize, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeWidthTo(this T drawable, float newWidth, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Width), newWidth, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence ResizeHeightTo(this T drawable, float newHeight, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Height), newHeight, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveTo(this T drawable, Vector2 newPosition, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Position), newPosition, duration, easing); - - /// - /// Smoothly adjusts or over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveTo(this T drawable, Direction direction, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable - { - switch (direction) - { - case Direction.Horizontal: - return drawable.MoveToX(destination, duration, easing); - case Direction.Vertical: - return drawable.MoveToY(destination, duration, easing); - } - - throw new InvalidOperationException($"Invalid direction ({direction}) passed to {nameof(MoveTo)}."); - } - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToX(this T drawable, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.X), destination, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToY(this T drawable, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.TransformTo(nameof(drawable.Y), destination, duration, easing); - - /// - /// Smoothly adjusts by an offset to its final value over time. - /// - /// A to which further transforms can be added. - public static TransformSequence MoveToOffset(this T drawable, Vector2 offset, double duration = 0, Easing easing = Easing.None) where T : Drawable => - drawable.MoveTo(((drawable.Transforms.LastOrDefault(t => t.TargetMember == nameof(drawable.Position)) as Transform)?.EndValue ?? drawable.Position) + offset, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformRelativeChildSizeTo(this T container, Vector2 newSize, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - container.TransformTo(nameof(container.RelativeChildSize), newSize, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformRelativeChildOffsetTo(this T container, Vector2 newOffset, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - container.TransformTo(nameof(container.RelativeChildOffset), newOffset, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence BlurTo(this T bufferedContainer, Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None) - where T : IBufferedContainer => - bufferedContainer.TransformTo(nameof(bufferedContainer.BlurSigma), newBlurSigma, duration, easing); - - /// - /// Smoothly adjusts over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformSpacingTo(this T flowContainer, Vector2 newSpacing, double duration = 0, Easing easing = Easing.None) - where T : IFillFlowContainer => - flowContainer.TransformTo(nameof(flowContainer.Spacing), newSpacing, duration, easing); - - /// - /// Smoothly adjusts the alpha channel of the colour of over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeEdgeEffectTo(this T container, float newAlpha, double duration = 0, Easing easing = Easing.None) - where T : IContainer - { - Color4 targetColour = container.EdgeEffect.Colour; - targetColour.A = newAlpha; - return container.FadeEdgeEffectTo(targetColour, duration, easing); - } - - /// - /// Smoothly adjusts the colour of over time. - /// - /// A to which further transforms can be added. - public static TransformSequence FadeEdgeEffectTo(this T container, Color4 newColour, double duration = 0, Easing easing = Easing.None) where T : IContainer - { - var effect = container.EdgeEffect; - effect.Colour = newColour; - return container.TweenEdgeEffectTo(effect, duration, easing); - } - - /// - /// Smoothly adjusts all parameters of over time. - /// - /// A to which further transforms can be added. - public static TransformSequence TweenEdgeEffectTo(this T container, EdgeEffectParameters newParameters, double duration = 0, Easing easing = Easing.None) - where T : IContainer => - container.TransformTo(nameof(container.EdgeEffect), newParameters, duration, easing); - - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using System; +using System.Linq; + +namespace osu.Framework.Graphics +{ + public static class TransformableExtensions + { + /// + /// Transforms a given property or field member of a given to . + /// The value of the given member is smoothly changed over time using the given for tweening. + /// + /// The type of the to apply the to. + /// The value type which is being transformed. + /// The to apply the to. + /// The property or field name of the member ot to transform. + /// The value to transform to. + /// The transform duration. + /// The transform easing to be used for tweening. + /// A to which further transforms can be added. + public static TransformSequence TransformTo(this TThis t, string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) + where TThis : ITransformable => + t.TransformTo(t.MakeTransform(propertyOrFieldName, newValue, duration, easing)); + + /// + /// Applies a to a given . + /// + /// The type of the to apply the to. + /// The to apply the to. + /// The transform to use. + /// A to which further transforms can be added. + public static TransformSequence TransformTo(this TThis t, Transform transform) where TThis : ITransformable + { + var result = new TransformSequence(t); + result.Add(transform); + t.AddTransform(transform); + return result; + } + + /// + /// Creates a for smoothly changing + /// over time using the given for tweening. + /// + /// is invoked as part of this method. + /// + /// The type of the the can be applied to. + /// The value type which is being transformed. + /// The the will be applied to. + /// The property or field name of the member ot to transform. + /// The value to transform to. + /// The transform duration. + /// The transform easing to be used for tweening. + /// The resulting . + public static Transform MakeTransform(this TThis t, string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) + where TThis : ITransformable => + t.PopulateTransform(new TransformCustom(propertyOrFieldName), newValue, duration, easing); + + /// + /// Populates a newly created with necessary values. + /// All s must be populated by this method prior to being used. + /// + /// The type of the the can be applied to. + /// The value type which is being transformed. + /// The the will be applied to. + /// The transform to populate. + /// The value to transform to. + /// The transform duration. + /// The transform easing to be used for tweening. + /// The populated . + public static Transform PopulateTransform(this TThis t, Transform transform, TValue newValue, double duration = 0, Easing easing = Easing.None) + where TThis : ITransformable + { + if (duration < 0) + throw new ArgumentOutOfRangeException(nameof(duration), $"{nameof(duration)} must be positive."); + + if (transform.Target != null) + throw new InvalidOperationException($"May not {nameof(PopulateTransform)} the same {nameof(Transform)} more than once."); + + transform.Target = t; + + double startTime = t.TransformStartTime; + + transform.StartTime = startTime; + transform.EndTime = startTime + duration; + transform.EndValue = newValue; + transform.Easing = easing; + + return transform; + } + + /// + /// Applies via TransformSequence.Append(IEnumerable{Generator})/>. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The optional Generators for s to be appended. + /// This . + public static TransformSequence Animate(this T transformable, params TransformSequence.Generator[] childGenerators) where T : ITransformable => + transformable.Delay(0, childGenerators); + + /// + /// Advances the start time of future appended s by milliseconds. + /// Then, are appended via TransformSequence.Append(IEnumerable{Generator})/>. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The delay to advance the start time by. + /// The optional Generators for s to be appended. + /// This . + public static TransformSequence Delay(this T transformable, double delay, params TransformSequence.Generator[] childGenerators) where T : ITransformable => + new TransformSequence(transformable).Delay(delay, childGenerators); + + /// + /// Returns a which waits for all existing transforms to finish. + /// + /// A which has a delay waiting for all transforms to be completed. + public static TransformSequence DelayUntilTransformsFinished(this T transformable) + where T : Transformable + { + return transformable.Delay(Math.Max(0, transformable.LatestTransformEndTime - transformable.Time.Current)); + } + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated times + /// with milliseconds between iterations. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The pause between iterations in milliseconds. + /// The number of iterations. + /// The functions to generate the s to be looped. + /// This . + public static TransformSequence Loop(this T transformable, double pause, int numIters, params TransformSequence.Generator[] childGenerators) + where T : ITransformable => + transformable.Delay(0).Loop(pause, numIters, childGenerators); + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated indefinitely with + /// milliseconds between iterations. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The pause between iterations in milliseconds. + /// The functions to generate the s to be looped. + /// This . + public static TransformSequence Loop(this T transformable, double pause, params TransformSequence.Generator[] childGenerators) + where T : ITransformable => + transformable.Delay(0).Loop(pause, childGenerators); + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated indefinitely. + /// milliseconds between iterations. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The functions to generate the s to be looped. + /// This . + public static TransformSequence Loop(this T transformable, params TransformSequence.Generator[] childGenerators) + where T : ITransformable => + transformable.Delay(0).Loop(childGenerators); + + /// + /// Append a looping to this to repeat indefinitely with + /// milliseconds between iterations. + /// + /// The type of the the can be applied to. + /// The the will be applied to. + /// The pause between iterations in milliseconds. + /// This . + public static TransformSequence Loop(this T transformable, double pause = 0) + where T : ITransformable => + transformable.Delay(0).Loop(pause); + + /// + /// Rotate over one full rotation with provided parameters. + /// + /// A to which further transforms can be added. + public static TransformSequence Spin(this T drawable, double revolutionDuration, RotationDirection direction, float startRotation = 0) where T : Drawable => + drawable.Delay(0).Spin(revolutionDuration, direction, startRotation); + + /// + /// Rotate times with provided parameters. + /// + /// A to which further transforms can be added. + public static TransformSequence Spin(this T drawable, double revolutionDuration, RotationDirection direction, float startRotation, int numRevolutions) where T : Drawable => + drawable.Delay(0).Spin(revolutionDuration, direction, startRotation, numRevolutions); + + /// + /// Smoothly adjusts to 1 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeIn(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.FadeTo(1, duration, easing); + + /// + /// Smoothly adjusts from 0 to 1 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeInFromZero(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.FadeTo(0).FadeIn(duration, easing); + + /// + /// Smoothly adjusts to 0 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeOut(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.FadeTo(0, duration, easing); + + /// + /// Smoothly adjusts from 1 to 0 over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeOutFromOne(this T drawable, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.FadeTo(1).FadeOut(duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeTo(this T drawable, float newAlpha, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Alpha), newAlpha, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeColour(this T drawable, ColourInfo newColour, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Colour), newColour, duration, easing); + + /// + /// Instantaneously flashes , then smoothly changes it back over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FlashColour(this T drawable, ColourInfo flashColour, double duration, Easing easing = Easing.None) where T : Drawable + { + ColourInfo endValue = (drawable.Transforms.LastOrDefault(t => t.TargetMember == nameof(drawable.Colour)) as Transform)?.EndValue ?? drawable.Colour; + return drawable.FadeColour(flashColour).FadeColour(endValue, duration, easing); + } + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence RotateTo(this T drawable, float newRotation, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Rotation), newRotation, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ScaleTo(this T drawable, float newScale, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.ScaleTo(new Vector2(newScale), duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ScaleTo(this T drawable, Vector2 newScale, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Scale), newScale, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeTo(this T drawable, float newSize, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.ResizeTo(new Vector2(newSize), duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeTo(this T drawable, Vector2 newSize, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Size), newSize, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeWidthTo(this T drawable, float newWidth, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Width), newWidth, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence ResizeHeightTo(this T drawable, float newHeight, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Height), newHeight, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveTo(this T drawable, Vector2 newPosition, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Position), newPosition, duration, easing); + + /// + /// Smoothly adjusts or over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveTo(this T drawable, Direction direction, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable + { + switch (direction) + { + case Direction.Horizontal: + return drawable.MoveToX(destination, duration, easing); + case Direction.Vertical: + return drawable.MoveToY(destination, duration, easing); + } + + throw new InvalidOperationException($"Invalid direction ({direction}) passed to {nameof(MoveTo)}."); + } + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToX(this T drawable, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.X), destination, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToY(this T drawable, float destination, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.TransformTo(nameof(drawable.Y), destination, duration, easing); + + /// + /// Smoothly adjusts by an offset to its final value over time. + /// + /// A to which further transforms can be added. + public static TransformSequence MoveToOffset(this T drawable, Vector2 offset, double duration = 0, Easing easing = Easing.None) where T : Drawable => + drawable.MoveTo(((drawable.Transforms.LastOrDefault(t => t.TargetMember == nameof(drawable.Position)) as Transform)?.EndValue ?? drawable.Position) + offset, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformRelativeChildSizeTo(this T container, Vector2 newSize, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + container.TransformTo(nameof(container.RelativeChildSize), newSize, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformRelativeChildOffsetTo(this T container, Vector2 newOffset, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + container.TransformTo(nameof(container.RelativeChildOffset), newOffset, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence BlurTo(this T bufferedContainer, Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None) + where T : IBufferedContainer => + bufferedContainer.TransformTo(nameof(bufferedContainer.BlurSigma), newBlurSigma, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformSpacingTo(this T flowContainer, Vector2 newSpacing, double duration = 0, Easing easing = Easing.None) + where T : IFillFlowContainer => + flowContainer.TransformTo(nameof(flowContainer.Spacing), newSpacing, duration, easing); + + /// + /// Smoothly adjusts the alpha channel of the colour of over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeEdgeEffectTo(this T container, float newAlpha, double duration = 0, Easing easing = Easing.None) + where T : IContainer + { + Color4 targetColour = container.EdgeEffect.Colour; + targetColour.A = newAlpha; + return container.FadeEdgeEffectTo(targetColour, duration, easing); + } + + /// + /// Smoothly adjusts the colour of over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeEdgeEffectTo(this T container, Color4 newColour, double duration = 0, Easing easing = Easing.None) where T : IContainer + { + var effect = container.EdgeEffect; + effect.Colour = newColour; + return container.TweenEdgeEffectTo(effect, duration, easing); + } + + /// + /// Smoothly adjusts all parameters of over time. + /// + /// A to which further transforms can be added. + public static TransformSequence TweenEdgeEffectTo(this T container, EdgeEffectParameters newParameters, double duration = 0, Easing easing = Easing.None) + where T : IContainer => + container.TransformTo(nameof(container.EdgeEffect), newParameters, duration, easing); + + } +} diff --git a/osu.Framework/Graphics/Transforms/ITransformable.cs b/osu.Framework/Graphics/Transforms/ITransformable.cs index fe9c5e48e..b073157d3 100644 --- a/osu.Framework/Graphics/Transforms/ITransformable.cs +++ b/osu.Framework/Graphics/Transforms/ITransformable.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; - -namespace osu.Framework.Graphics.Transforms -{ - public interface ITransformable - { - InvokeOnDisposal BeginDelayedSequence(double delay, bool recursive = false); - - InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false); - - double TransformStartTime { get; } - - void AddTransform(Transform transform); - - void RemoveTransform(Transform toRemove); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; + +namespace osu.Framework.Graphics.Transforms +{ + public interface ITransformable + { + InvokeOnDisposal BeginDelayedSequence(double delay, bool recursive = false); + + InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false); + + double TransformStartTime { get; } + + void AddTransform(Transform transform); + + void RemoveTransform(Transform toRemove); + } +} diff --git a/osu.Framework/Graphics/Transforms/Transform.cs b/osu.Framework/Graphics/Transforms/Transform.cs index 1520690ed..cfe0de5f3 100644 --- a/osu.Framework/Graphics/Transforms/Transform.cs +++ b/osu.Framework/Graphics/Transforms/Transform.cs @@ -1,99 +1,99 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Transforms -{ - public abstract class Transform - { - internal ulong TransformID; - - /// - /// Whether this has been applied to an . - /// - internal bool Applied; - - /// - /// Whether this has been applied completely to an . - /// Used to track whether we still need to apply for targets which allow rewind. - /// - internal bool AppliedToEnd; - - /// - /// Whether this can be rewound. - /// - public bool Rewindable = true; - - public Easing Easing; - - public abstract ITransformable TargetTransformable { get; } - - public double StartTime { get; internal set; } - public double EndTime { get; internal set; } - - public bool IsLooping { get; internal set; } - public double LoopDelay { get; internal set; } - - public abstract string TargetMember { get; } - - public abstract void Apply(double time); - - public abstract void ReadIntoStartValue(); - - internal bool HasStartValue; - - public Action OnComplete; - - public Action OnAbort; - - public Transform Clone() => (Transform)MemberwiseClone(); - - public static readonly IComparer COMPARER = new TransformTimeComparer(); - - private class TransformTimeComparer : IComparer - { - public int Compare(Transform x, Transform y) - { - if (x == null) throw new ArgumentNullException(nameof(x)); - if (y == null) throw new ArgumentNullException(nameof(y)); - - int compare = x.StartTime.CompareTo(y.StartTime); - if (compare != 0) return compare; - - compare = x.TransformID.CompareTo(y.TransformID); - - return compare; - } - } - } - - public abstract class Transform : Transform - { - public TValue StartValue { get; protected set; } - public TValue EndValue { get; protected internal set; } - } - - public abstract class Transform : Transform - where T : ITransformable - { - public override ITransformable TargetTransformable => Target; - - public T Target { get; internal set; } - - public sealed override void Apply(double time) - { - Apply(Target, time); - Applied = true; - } - - public sealed override void ReadIntoStartValue() => ReadIntoStartValue(Target); - - protected abstract void Apply(T d, double time); - - protected abstract void ReadIntoStartValue(T d); - - public override string ToString() => $"{Target.GetType().Name}.{TargetMember} {StartTime}-{EndTime}ms {StartValue} -> {EndValue}"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Transforms +{ + public abstract class Transform + { + internal ulong TransformID; + + /// + /// Whether this has been applied to an . + /// + internal bool Applied; + + /// + /// Whether this has been applied completely to an . + /// Used to track whether we still need to apply for targets which allow rewind. + /// + internal bool AppliedToEnd; + + /// + /// Whether this can be rewound. + /// + public bool Rewindable = true; + + public Easing Easing; + + public abstract ITransformable TargetTransformable { get; } + + public double StartTime { get; internal set; } + public double EndTime { get; internal set; } + + public bool IsLooping { get; internal set; } + public double LoopDelay { get; internal set; } + + public abstract string TargetMember { get; } + + public abstract void Apply(double time); + + public abstract void ReadIntoStartValue(); + + internal bool HasStartValue; + + public Action OnComplete; + + public Action OnAbort; + + public Transform Clone() => (Transform)MemberwiseClone(); + + public static readonly IComparer COMPARER = new TransformTimeComparer(); + + private class TransformTimeComparer : IComparer + { + public int Compare(Transform x, Transform y) + { + if (x == null) throw new ArgumentNullException(nameof(x)); + if (y == null) throw new ArgumentNullException(nameof(y)); + + int compare = x.StartTime.CompareTo(y.StartTime); + if (compare != 0) return compare; + + compare = x.TransformID.CompareTo(y.TransformID); + + return compare; + } + } + } + + public abstract class Transform : Transform + { + public TValue StartValue { get; protected set; } + public TValue EndValue { get; protected internal set; } + } + + public abstract class Transform : Transform + where T : ITransformable + { + public override ITransformable TargetTransformable => Target; + + public T Target { get; internal set; } + + public sealed override void Apply(double time) + { + Apply(Target, time); + Applied = true; + } + + public sealed override void ReadIntoStartValue() => ReadIntoStartValue(Target); + + protected abstract void Apply(T d, double time); + + protected abstract void ReadIntoStartValue(T d); + + public override string ToString() => $"{Target.GetType().Name}.{TargetMember} {StartTime}-{EndTime}ms {StartValue} -> {EndValue}"; + } +} diff --git a/osu.Framework/Graphics/Transforms/TransformCustom.cs b/osu.Framework/Graphics/Transforms/TransformCustom.cs index d7ecaa3df..496f5ecd6 100644 --- a/osu.Framework/Graphics/Transforms/TransformCustom.cs +++ b/osu.Framework/Graphics/Transforms/TransformCustom.cs @@ -1,176 +1,176 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.MathUtils; -using System; -using System.Collections.Concurrent; -using System.Reflection.Emit; -using osu.Framework.Extensions.TypeExtensions; -using System.Reflection; -using System.Linq; -using System.Diagnostics; - -namespace osu.Framework.Graphics.Transforms -{ - public delegate TValue InterpolationFunc(double time, TValue startValue, TValue endValue, double startTime, double endTime, Easing easingType); - - /// - /// A transform which operates on arbitrary fields or properties of a given target. - /// - /// The type of the field or property to operate upon. - /// The type of the target to operate upon. - internal class TransformCustom : Transform where T : ITransformable - { - private delegate TValue ReadFunc(T transformable); - private delegate void WriteFunc(T transformable, TValue value); - - private struct Accessor - { - public ReadFunc Read; - public WriteFunc Write; - } - - private static readonly ConcurrentDictionary accessors = new ConcurrentDictionary(); - private static readonly InterpolationFunc interpolation_func; - - static TransformCustom() - { - interpolation_func = - (InterpolationFunc)typeof(Interpolation).GetMethod( - nameof(Interpolation.ValueAt), - typeof(InterpolationFunc) - .GetMethod(nameof(InterpolationFunc.Invoke)) - ?.GetParameters().Select(p => p.ParameterType).ToArray() - )?.CreateDelegate(typeof(InterpolationFunc)); - } - - private static ReadFunc createFieldGetter(FieldInfo field) - { - string methodName = $"{typeof(T).ReadableName()}.{field.Name}.get_{Guid.NewGuid():N}"; - DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(TValue), new[] { typeof(T) }, true); - ILGenerator gen = setterMethod.GetILGenerator(); - gen.Emit(OpCodes.Ldarg_0); - gen.Emit(OpCodes.Ldfld, field); - gen.Emit(OpCodes.Ret); - return (ReadFunc)setterMethod.CreateDelegate(typeof(ReadFunc)); - } - - private static WriteFunc createFieldSetter(FieldInfo field) - { - string methodName = $"{typeof(T).ReadableName()}.{field.Name}.set_{Guid.NewGuid():N}"; - DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(T), typeof(TValue) }, true); - ILGenerator gen = setterMethod.GetILGenerator(); - gen.Emit(OpCodes.Ldarg_0); - gen.Emit(OpCodes.Ldarg_1); - gen.Emit(OpCodes.Stfld, field); - gen.Emit(OpCodes.Ret); - return (WriteFunc)setterMethod.CreateDelegate(typeof(WriteFunc)); - } - - private static Accessor findAccessor(Type type, string propertyOrFieldName) - { - PropertyInfo property = type.GetProperty(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (property != null) - { - if (property.PropertyType != typeof(TValue)) - throw new InvalidOperationException( - $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} " + - $"since its type should be {typeof(TValue).ReadableName()}, but is {property.PropertyType.ReadableName()}."); - - var getter = property.GetGetMethod(true); - var setter = property.GetSetMethod(true); - - if (getter == null || setter == null) - throw new InvalidOperationException( - $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} " + - "since it needs to have both a getter and a setter."); - - if (getter.IsStatic || setter.IsStatic) - throw new NotSupportedException( - $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); - - return new Accessor - { - Read = (ReadFunc)getter.CreateDelegate(typeof(ReadFunc)), - Write = (WriteFunc)setter.CreateDelegate(typeof(WriteFunc)), - }; - } - - FieldInfo field = type.GetField(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - if (field != null) - { - if (field.FieldType != typeof(TValue)) - throw new InvalidOperationException( - $"Cannot create {nameof(TransformCustom)} for field {type.ReadableName()}.{propertyOrFieldName} " + - $"since its type should be {typeof(TValue).ReadableName()}, but is {field.FieldType.ReadableName()}."); - - if (field.IsStatic) - throw new NotSupportedException( - $"Cannot create {nameof(TransformCustom)} for field {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); - - return new Accessor - { - Read = createFieldGetter(field), - Write = createFieldSetter(field), - }; - } - - if (type.BaseType == null) - throw new InvalidOperationException($"Cannot create {nameof(TransformCustom)} for non-existent property or field {typeof(T).ReadableName()}.{propertyOrFieldName}."); - - // Private members aren't visible unless we check the base type explicitly, so let's try our luck. - return findAccessor(type.BaseType, propertyOrFieldName); - } - - private static Accessor getAccessor(string propertyOrFieldName) => accessors.GetOrAdd(propertyOrFieldName, _ => findAccessor(typeof(T), propertyOrFieldName)); - - private readonly Accessor accessor; - private readonly InterpolationFunc interpolationFunc; - - /// - /// Creates a new instance operating on a property or field of . The property or field is - /// denoted by its name, passed as . - /// By default, an interpolation method "ValueAt" from with suitable signature is - /// picked for interpolating between and - /// according to , - /// , and a current time. - /// Optionally, or when no suitable "ValueAt" from exists, a custom function can be supplied - /// via . - /// - /// The property or field name to be operated upon. - /// - /// The function to be used for interpolating between and - /// according to , - /// , and a current time. - /// If null, an interpolation method "ValueAt" from with a suitable signature is picked. - /// If none exists, then this parameter must not be null. - /// - public TransformCustom(string propertyOrFieldName, InterpolationFunc interpolationFunc = null) - { - TargetMember = propertyOrFieldName; - - accessor = getAccessor(propertyOrFieldName); - Trace.Assert(accessor.Read != null && accessor.Write != null, $"Failed to populate {nameof(accessor)}."); - - this.interpolationFunc = interpolationFunc ?? interpolation_func; - - if (this.interpolationFunc == null) - throw new InvalidOperationException( - $"Need to pass a custom {nameof(interpolationFunc)} since no default {nameof(Interpolation)}.{nameof(Interpolation.ValueAt)} exists."); - } - - private TValue valueAt(double time) - { - if (time < StartTime) return StartValue; - if (time >= EndTime) return EndValue; - - return interpolationFunc(time, StartValue, EndValue, StartTime, EndTime, Easing); - } - - public override string TargetMember { get; } - - protected override void Apply(T d, double time) => accessor.Write(d, valueAt(time)); - - protected override void ReadIntoStartValue(T d) => StartValue = accessor.Read(d); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.MathUtils; +using System; +using System.Collections.Concurrent; +using System.Reflection.Emit; +using osu.Framework.Extensions.TypeExtensions; +using System.Reflection; +using System.Linq; +using System.Diagnostics; + +namespace osu.Framework.Graphics.Transforms +{ + public delegate TValue InterpolationFunc(double time, TValue startValue, TValue endValue, double startTime, double endTime, Easing easingType); + + /// + /// A transform which operates on arbitrary fields or properties of a given target. + /// + /// The type of the field or property to operate upon. + /// The type of the target to operate upon. + internal class TransformCustom : Transform where T : ITransformable + { + private delegate TValue ReadFunc(T transformable); + private delegate void WriteFunc(T transformable, TValue value); + + private struct Accessor + { + public ReadFunc Read; + public WriteFunc Write; + } + + private static readonly ConcurrentDictionary accessors = new ConcurrentDictionary(); + private static readonly InterpolationFunc interpolation_func; + + static TransformCustom() + { + interpolation_func = + (InterpolationFunc)typeof(Interpolation).GetMethod( + nameof(Interpolation.ValueAt), + typeof(InterpolationFunc) + .GetMethod(nameof(InterpolationFunc.Invoke)) + ?.GetParameters().Select(p => p.ParameterType).ToArray() + )?.CreateDelegate(typeof(InterpolationFunc)); + } + + private static ReadFunc createFieldGetter(FieldInfo field) + { + string methodName = $"{typeof(T).ReadableName()}.{field.Name}.get_{Guid.NewGuid():N}"; + DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(TValue), new[] { typeof(T) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldfld, field); + gen.Emit(OpCodes.Ret); + return (ReadFunc)setterMethod.CreateDelegate(typeof(ReadFunc)); + } + + private static WriteFunc createFieldSetter(FieldInfo field) + { + string methodName = $"{typeof(T).ReadableName()}.{field.Name}.set_{Guid.NewGuid():N}"; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(T), typeof(TValue) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stfld, field); + gen.Emit(OpCodes.Ret); + return (WriteFunc)setterMethod.CreateDelegate(typeof(WriteFunc)); + } + + private static Accessor findAccessor(Type type, string propertyOrFieldName) + { + PropertyInfo property = type.GetProperty(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (property != null) + { + if (property.PropertyType != typeof(TValue)) + throw new InvalidOperationException( + $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} " + + $"since its type should be {typeof(TValue).ReadableName()}, but is {property.PropertyType.ReadableName()}."); + + var getter = property.GetGetMethod(true); + var setter = property.GetSetMethod(true); + + if (getter == null || setter == null) + throw new InvalidOperationException( + $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} " + + "since it needs to have both a getter and a setter."); + + if (getter.IsStatic || setter.IsStatic) + throw new NotSupportedException( + $"Cannot create {nameof(TransformCustom)} for property {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); + + return new Accessor + { + Read = (ReadFunc)getter.CreateDelegate(typeof(ReadFunc)), + Write = (WriteFunc)setter.CreateDelegate(typeof(WriteFunc)), + }; + } + + FieldInfo field = type.GetField(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + if (field != null) + { + if (field.FieldType != typeof(TValue)) + throw new InvalidOperationException( + $"Cannot create {nameof(TransformCustom)} for field {type.ReadableName()}.{propertyOrFieldName} " + + $"since its type should be {typeof(TValue).ReadableName()}, but is {field.FieldType.ReadableName()}."); + + if (field.IsStatic) + throw new NotSupportedException( + $"Cannot create {nameof(TransformCustom)} for field {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); + + return new Accessor + { + Read = createFieldGetter(field), + Write = createFieldSetter(field), + }; + } + + if (type.BaseType == null) + throw new InvalidOperationException($"Cannot create {nameof(TransformCustom)} for non-existent property or field {typeof(T).ReadableName()}.{propertyOrFieldName}."); + + // Private members aren't visible unless we check the base type explicitly, so let's try our luck. + return findAccessor(type.BaseType, propertyOrFieldName); + } + + private static Accessor getAccessor(string propertyOrFieldName) => accessors.GetOrAdd(propertyOrFieldName, _ => findAccessor(typeof(T), propertyOrFieldName)); + + private readonly Accessor accessor; + private readonly InterpolationFunc interpolationFunc; + + /// + /// Creates a new instance operating on a property or field of . The property or field is + /// denoted by its name, passed as . + /// By default, an interpolation method "ValueAt" from with suitable signature is + /// picked for interpolating between and + /// according to , + /// , and a current time. + /// Optionally, or when no suitable "ValueAt" from exists, a custom function can be supplied + /// via . + /// + /// The property or field name to be operated upon. + /// + /// The function to be used for interpolating between and + /// according to , + /// , and a current time. + /// If null, an interpolation method "ValueAt" from with a suitable signature is picked. + /// If none exists, then this parameter must not be null. + /// + public TransformCustom(string propertyOrFieldName, InterpolationFunc interpolationFunc = null) + { + TargetMember = propertyOrFieldName; + + accessor = getAccessor(propertyOrFieldName); + Trace.Assert(accessor.Read != null && accessor.Write != null, $"Failed to populate {nameof(accessor)}."); + + this.interpolationFunc = interpolationFunc ?? interpolation_func; + + if (this.interpolationFunc == null) + throw new InvalidOperationException( + $"Need to pass a custom {nameof(interpolationFunc)} since no default {nameof(Interpolation)}.{nameof(Interpolation.ValueAt)} exists."); + } + + private TValue valueAt(double time) + { + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return interpolationFunc(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + + public override string TargetMember { get; } + + protected override void Apply(T d, double time) => accessor.Write(d, valueAt(time)); + + protected override void ReadIntoStartValue(T d) => StartValue = accessor.Read(d); + } +} diff --git a/osu.Framework/Graphics/Transforms/TransformSequence.cs b/osu.Framework/Graphics/Transforms/TransformSequence.cs index 9168a60ef..2ec11bb50 100644 --- a/osu.Framework/Graphics/Transforms/TransformSequence.cs +++ b/osu.Framework/Graphics/Transforms/TransformSequence.cs @@ -1,421 +1,421 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Graphics.Transforms -{ - /// - /// A sequence of s all operating upon the same - /// of type . - /// Exposes various operations to extend the sequence by additional such as - /// delays, loops, continuations, and events. - /// - /// - /// The type of the the s in this sequence operate upon. - /// - public class TransformSequence where T : ITransformable - { - /// - /// A delegate that generates a new on a given . - /// - /// The to generate a for. - /// The generated . - public delegate TransformSequence Generator(T origin); - - private readonly T origin; - - private readonly List transforms = new List(); - - private bool hasCompleted = true; - - private readonly double startTime; - private double currentTime; - private double endTime => Math.Max(currentTime, lastEndTime); - - private Transform last; - private double lastEndTime; - - private bool hasEnd => lastEndTime != double.PositiveInfinity; - - /// - /// Creates a new empty attached to a given . - /// - /// The to attach the new to. - public TransformSequence(T origin) - { - if (origin == null) - throw new NullReferenceException($"May not create a {nameof(TransformSequence)} with a null {nameof(origin)}."); - - this.origin = origin; - startTime = currentTime = lastEndTime = origin.TransformStartTime; - } - - private void onLoopingTransform() - { - // As soon as we have an infinitely looping transform, - // completion no longer makes sense. - if (last != null) - last.OnComplete = null; - - last = null; - lastEndTime = double.PositiveInfinity; - hasCompleted = false; - } - - public TransformSequence TransformTo(string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) => - Append(o => o.TransformTo(propertyOrFieldName, newValue, duration, easing)); - - /// - /// Adds an existing operating on to this . - /// - /// The to add. - internal void Add(Transform transform) - { - if (!ReferenceEquals(transform.TargetTransformable, origin)) - throw new InvalidOperationException( - $"{nameof(transform)} must operate upon {nameof(origin)}={origin}, but operates upon {transform.TargetTransformable}."); - - transforms.Add(transform); - - transform.OnComplete = null; - transform.OnAbort = onTransformAborted; - - if (transform.IsLooping) - onLoopingTransform(); - - // Update last transform for completion callback - if (last == null || transform.EndTime > lastEndTime) - { - if (last != null) - last.OnComplete = null; - - last = transform; - last.OnComplete = onTransformsComplete; - lastEndTime = last.EndTime; - hasCompleted = false; - } - } - - /// - /// Appends multiple s to this . - /// - /// The s to be appended. - /// This . - public TransformSequence Append(IEnumerable childGenerators) - { - foreach (var p in childGenerators) - Append(p); - - return this; - } - - /// - /// Appends a s to this . - /// The is invoked within a - /// such that the generated starts at the correct point in time. - /// Its s are then merged into this . - /// - /// The to be appended. - /// This . - public TransformSequence Append(Generator childGenerator) - { - TransformSequence child; - using (origin.BeginAbsoluteSequence(currentTime)) - child = childGenerator(origin); - - if (!ReferenceEquals(child.origin, origin)) - throw new InvalidOperationException($"May not append {nameof(TransformSequence)} with different origin."); - - var oldLast = last; - foreach (var t in child.transforms) - Add(t); - - // If we flatten a child into ourselves that already completed, then - // we need to make sure to update the hasCompleted value, too, since - // the already completed final transform will no longer fire any events. - if (oldLast != last) - hasCompleted = child.hasCompleted; - - return this; - } - - /// - /// Invokes inside a - /// such that is the current time of this . - /// It is the respondibility of to make appropriate use of . - /// - /// The return type of . - /// The function to be invoked. - /// The resulting value of the invocation of . - /// This . - public TransformSequence Append(Func originFunc, out U result) - { - using (origin.BeginAbsoluteSequence(currentTime)) - result = originFunc(origin); - - return this; - } - - /// - /// Invokes inside a - /// such that is the current time of this . - /// It is the respondibility of to make appropriate use of . - /// - /// The function to be invoked. - /// This . - public TransformSequence Append(Action originAction) - { - using (origin.BeginAbsoluteSequence(currentTime)) - originAction(origin); - - return this; - } - - private void onTransformAborted() - { - if (transforms.Count == 0) - return; - - // No need for OnAbort events to trigger anymore, since - // we are already aware of the abortion. - foreach (var t in transforms) - { - t.OnAbort = null; - t.TargetTransformable.RemoveTransform(t); - } - - transforms.Clear(); - last = null; - - onAbort?.Invoke(); - } - - private void onTransformsComplete() - { - hasCompleted = true; - onComplete?.Invoke(); - } - - private void subscribeComplete(Action func) - { - if (onComplete != null) - throw new InvalidOperationException( - "May not subscribe completion multiple times." + - $"This exception is also caused by calling {nameof(Then)} or {nameof(Finally)} on an infinitely looping {nameof(TransformSequence)}."); - - onComplete = func; - - // Completion can be immediately triggered by instant transforms, - // and therefore when subscribing we need to take into account - // potential previous completions. - if (hasCompleted) - func(); - } - - private void subscribeAbort(Action func) - { - if (onAbort != null) - throw new InvalidOperationException("May not subscribe abort multiple times."); - - // No need to worry about new transforms immediately aborting, so - // we can just subscribe here and be sure abort couldn't have been - // triggered already. - onAbort = func; - } - - private Action onComplete; - private Action onAbort; - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated times - /// with milliseconds between iterations. - /// - /// The pause between iterations in milliseconds. - /// The number of iterations. - /// The functions to generate the s to be looped. - /// This . - public TransformSequence Loop(double pause, int numIters, params Generator[] childGenerators) - { - Append(o => - { - var childSequence = new TransformSequence(o); - childSequence.Append(childGenerators); - childSequence.Loop(pause, numIters); - return childSequence; - }); - - return this; - } - - /// - /// Repeats all s within this - /// times with milliseconds between iterations. - /// - /// The pause between iterations in milliseconds. - /// The number of iterations. - /// This . - public TransformSequence Loop(double pause, int numIters) - { - if (numIters < 1) - throw new InvalidOperationException($"May not {nameof(Loop)} for fewer than 1 iteration ({numIters} attempted)."); - - if (!hasEnd) - throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); - - var iterDuration = endTime - startTime + pause; - var toLoop = transforms.ToArray(); - - // Duplicate existing transforms numIters times - for (int i = 1; i < numIters; ++i) - { - foreach (var t in toLoop) - { - var clone = t.Clone(); - clone.StartTime += i * iterDuration; - clone.EndTime += i * iterDuration; - Add(clone); - t.TargetTransformable.AddTransform(clone); - } - } - - return this; - } - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated indefinitely. - /// - /// The functions to generate the s to be looped. - /// This . - public TransformSequence Loop(params Generator[] childGenerators) => Loop(0, childGenerators); - - /// - /// Append a looping to this . - /// All s generated by are appended to - /// this and then repeated indefinitely with - /// milliseconds between iterations. - /// - /// The pause between iterations in milliseconds. - /// The functions to generate the s to be looped. - /// This . - public TransformSequence Loop(double pause, params Generator[] childGenerators) - { - Append(o => - { - var childSequence = new TransformSequence(o); - childSequence.Append(childGenerators); - childSequence.Loop(pause); - return childSequence; - }); - - return this; - } - - /// - /// Repeats all s within this indefinitely. - /// - /// The pause between iterations in milliseconds. - /// This . - public TransformSequence Loop(double pause = 0) - { - if (!hasEnd) - throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); - - var iterDuration = endTime - startTime + pause; - foreach (var t in transforms) - { - t.IsLooping = true; - t.LoopDelay = iterDuration; - t.AppliedToEnd = false; // we want to force a reprocess of this transform. it may have been applied-to-end in the Add, but not correctly looped as a result. - } - - onLoopingTransform(); - return this; - } - - /// - /// Advances the start time of future appended s to the latest end time of all - /// s in this . - /// Then, are appended via . - /// - /// The optional s for s to be appended. - /// This . - public TransformSequence Then(params Generator[] childGenerators) => Then(0, childGenerators); - - /// - /// Advances the start time of future appended s to the latest end time of all - /// s in this plus milliseconds. - /// Then, are appended via . - /// - /// The delay after the latest end time of all s. - /// The optional s for s to be appended. - /// This . - public TransformSequence Then(double delay, params Generator[] childGenerators) - { - if (!hasEnd) - throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); - - // "Then" simply sets the currentTime to endTime to continue where the last transform left off, - // followed by a subsequent delay call. - currentTime = endTime; - return Delay(delay, childGenerators); - } - - /// - /// Advances the start time of future appended s by milliseconds. - /// Then, are appended via . - /// - /// The delay to advance the start time by. - /// The optional s for s to be appended. - /// This . - public TransformSequence Delay(double delay, params Generator[] childGenerators) - { - // After a delay statement, future transforms are appended after a currentTime which got offset by a delay. - currentTime += delay; - return Append(childGenerators); - } - - /// - /// Registers a callback which is triggered once all s in this - /// complete successfully. - /// If all s already completed successfully at the point of this call, then - /// is triggered immediately. - /// Only a single callback function may be registered. - /// - /// The callback function. - public void OnComplete(Action function) - { - if (!hasEnd) - throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); - - subscribeComplete(() => function(origin)); - } - - /// - /// Registers a callback which is triggered once any in this - /// is aborted (e.g. by another overriding it). - /// Only a single callback function may be registered. - /// - /// The callback function. - public void OnAbort(Action function) => subscribeAbort(() => function(origin)); - - /// - /// Registers a callback which is triggered once any in this - /// is aborted or when all s complete successfully. - /// This is equivalent with calling both and . - /// Only a single callback function may be registered. - /// - /// The callback function. - public void Finally(Action function) - { - if (hasEnd) - OnComplete(function); - OnAbort(function); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Graphics.Transforms +{ + /// + /// A sequence of s all operating upon the same + /// of type . + /// Exposes various operations to extend the sequence by additional such as + /// delays, loops, continuations, and events. + /// + /// + /// The type of the the s in this sequence operate upon. + /// + public class TransformSequence where T : ITransformable + { + /// + /// A delegate that generates a new on a given . + /// + /// The to generate a for. + /// The generated . + public delegate TransformSequence Generator(T origin); + + private readonly T origin; + + private readonly List transforms = new List(); + + private bool hasCompleted = true; + + private readonly double startTime; + private double currentTime; + private double endTime => Math.Max(currentTime, lastEndTime); + + private Transform last; + private double lastEndTime; + + private bool hasEnd => lastEndTime != double.PositiveInfinity; + + /// + /// Creates a new empty attached to a given . + /// + /// The to attach the new to. + public TransformSequence(T origin) + { + if (origin == null) + throw new NullReferenceException($"May not create a {nameof(TransformSequence)} with a null {nameof(origin)}."); + + this.origin = origin; + startTime = currentTime = lastEndTime = origin.TransformStartTime; + } + + private void onLoopingTransform() + { + // As soon as we have an infinitely looping transform, + // completion no longer makes sense. + if (last != null) + last.OnComplete = null; + + last = null; + lastEndTime = double.PositiveInfinity; + hasCompleted = false; + } + + public TransformSequence TransformTo(string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) => + Append(o => o.TransformTo(propertyOrFieldName, newValue, duration, easing)); + + /// + /// Adds an existing operating on to this . + /// + /// The to add. + internal void Add(Transform transform) + { + if (!ReferenceEquals(transform.TargetTransformable, origin)) + throw new InvalidOperationException( + $"{nameof(transform)} must operate upon {nameof(origin)}={origin}, but operates upon {transform.TargetTransformable}."); + + transforms.Add(transform); + + transform.OnComplete = null; + transform.OnAbort = onTransformAborted; + + if (transform.IsLooping) + onLoopingTransform(); + + // Update last transform for completion callback + if (last == null || transform.EndTime > lastEndTime) + { + if (last != null) + last.OnComplete = null; + + last = transform; + last.OnComplete = onTransformsComplete; + lastEndTime = last.EndTime; + hasCompleted = false; + } + } + + /// + /// Appends multiple s to this . + /// + /// The s to be appended. + /// This . + public TransformSequence Append(IEnumerable childGenerators) + { + foreach (var p in childGenerators) + Append(p); + + return this; + } + + /// + /// Appends a s to this . + /// The is invoked within a + /// such that the generated starts at the correct point in time. + /// Its s are then merged into this . + /// + /// The to be appended. + /// This . + public TransformSequence Append(Generator childGenerator) + { + TransformSequence child; + using (origin.BeginAbsoluteSequence(currentTime)) + child = childGenerator(origin); + + if (!ReferenceEquals(child.origin, origin)) + throw new InvalidOperationException($"May not append {nameof(TransformSequence)} with different origin."); + + var oldLast = last; + foreach (var t in child.transforms) + Add(t); + + // If we flatten a child into ourselves that already completed, then + // we need to make sure to update the hasCompleted value, too, since + // the already completed final transform will no longer fire any events. + if (oldLast != last) + hasCompleted = child.hasCompleted; + + return this; + } + + /// + /// Invokes inside a + /// such that is the current time of this . + /// It is the respondibility of to make appropriate use of . + /// + /// The return type of . + /// The function to be invoked. + /// The resulting value of the invocation of . + /// This . + public TransformSequence Append(Func originFunc, out U result) + { + using (origin.BeginAbsoluteSequence(currentTime)) + result = originFunc(origin); + + return this; + } + + /// + /// Invokes inside a + /// such that is the current time of this . + /// It is the respondibility of to make appropriate use of . + /// + /// The function to be invoked. + /// This . + public TransformSequence Append(Action originAction) + { + using (origin.BeginAbsoluteSequence(currentTime)) + originAction(origin); + + return this; + } + + private void onTransformAborted() + { + if (transforms.Count == 0) + return; + + // No need for OnAbort events to trigger anymore, since + // we are already aware of the abortion. + foreach (var t in transforms) + { + t.OnAbort = null; + t.TargetTransformable.RemoveTransform(t); + } + + transforms.Clear(); + last = null; + + onAbort?.Invoke(); + } + + private void onTransformsComplete() + { + hasCompleted = true; + onComplete?.Invoke(); + } + + private void subscribeComplete(Action func) + { + if (onComplete != null) + throw new InvalidOperationException( + "May not subscribe completion multiple times." + + $"This exception is also caused by calling {nameof(Then)} or {nameof(Finally)} on an infinitely looping {nameof(TransformSequence)}."); + + onComplete = func; + + // Completion can be immediately triggered by instant transforms, + // and therefore when subscribing we need to take into account + // potential previous completions. + if (hasCompleted) + func(); + } + + private void subscribeAbort(Action func) + { + if (onAbort != null) + throw new InvalidOperationException("May not subscribe abort multiple times."); + + // No need to worry about new transforms immediately aborting, so + // we can just subscribe here and be sure abort couldn't have been + // triggered already. + onAbort = func; + } + + private Action onComplete; + private Action onAbort; + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated times + /// with milliseconds between iterations. + /// + /// The pause between iterations in milliseconds. + /// The number of iterations. + /// The functions to generate the s to be looped. + /// This . + public TransformSequence Loop(double pause, int numIters, params Generator[] childGenerators) + { + Append(o => + { + var childSequence = new TransformSequence(o); + childSequence.Append(childGenerators); + childSequence.Loop(pause, numIters); + return childSequence; + }); + + return this; + } + + /// + /// Repeats all s within this + /// times with milliseconds between iterations. + /// + /// The pause between iterations in milliseconds. + /// The number of iterations. + /// This . + public TransformSequence Loop(double pause, int numIters) + { + if (numIters < 1) + throw new InvalidOperationException($"May not {nameof(Loop)} for fewer than 1 iteration ({numIters} attempted)."); + + if (!hasEnd) + throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); + + var iterDuration = endTime - startTime + pause; + var toLoop = transforms.ToArray(); + + // Duplicate existing transforms numIters times + for (int i = 1; i < numIters; ++i) + { + foreach (var t in toLoop) + { + var clone = t.Clone(); + clone.StartTime += i * iterDuration; + clone.EndTime += i * iterDuration; + Add(clone); + t.TargetTransformable.AddTransform(clone); + } + } + + return this; + } + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated indefinitely. + /// + /// The functions to generate the s to be looped. + /// This . + public TransformSequence Loop(params Generator[] childGenerators) => Loop(0, childGenerators); + + /// + /// Append a looping to this . + /// All s generated by are appended to + /// this and then repeated indefinitely with + /// milliseconds between iterations. + /// + /// The pause between iterations in milliseconds. + /// The functions to generate the s to be looped. + /// This . + public TransformSequence Loop(double pause, params Generator[] childGenerators) + { + Append(o => + { + var childSequence = new TransformSequence(o); + childSequence.Append(childGenerators); + childSequence.Loop(pause); + return childSequence; + }); + + return this; + } + + /// + /// Repeats all s within this indefinitely. + /// + /// The pause between iterations in milliseconds. + /// This . + public TransformSequence Loop(double pause = 0) + { + if (!hasEnd) + throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); + + var iterDuration = endTime - startTime + pause; + foreach (var t in transforms) + { + t.IsLooping = true; + t.LoopDelay = iterDuration; + t.AppliedToEnd = false; // we want to force a reprocess of this transform. it may have been applied-to-end in the Add, but not correctly looped as a result. + } + + onLoopingTransform(); + return this; + } + + /// + /// Advances the start time of future appended s to the latest end time of all + /// s in this . + /// Then, are appended via . + /// + /// The optional s for s to be appended. + /// This . + public TransformSequence Then(params Generator[] childGenerators) => Then(0, childGenerators); + + /// + /// Advances the start time of future appended s to the latest end time of all + /// s in this plus milliseconds. + /// Then, are appended via . + /// + /// The delay after the latest end time of all s. + /// The optional s for s to be appended. + /// This . + public TransformSequence Then(double delay, params Generator[] childGenerators) + { + if (!hasEnd) + throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); + + // "Then" simply sets the currentTime to endTime to continue where the last transform left off, + // followed by a subsequent delay call. + currentTime = endTime; + return Delay(delay, childGenerators); + } + + /// + /// Advances the start time of future appended s by milliseconds. + /// Then, are appended via . + /// + /// The delay to advance the start time by. + /// The optional s for s to be appended. + /// This . + public TransformSequence Delay(double delay, params Generator[] childGenerators) + { + // After a delay statement, future transforms are appended after a currentTime which got offset by a delay. + currentTime += delay; + return Append(childGenerators); + } + + /// + /// Registers a callback which is triggered once all s in this + /// complete successfully. + /// If all s already completed successfully at the point of this call, then + /// is triggered immediately. + /// Only a single callback function may be registered. + /// + /// The callback function. + public void OnComplete(Action function) + { + if (!hasEnd) + throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); + + subscribeComplete(() => function(origin)); + } + + /// + /// Registers a callback which is triggered once any in this + /// is aborted (e.g. by another overriding it). + /// Only a single callback function may be registered. + /// + /// The callback function. + public void OnAbort(Action function) => subscribeAbort(() => function(origin)); + + /// + /// Registers a callback which is triggered once any in this + /// is aborted or when all s complete successfully. + /// This is equivalent with calling both and . + /// Only a single callback function may be registered. + /// + /// The callback function. + public void Finally(Action function) + { + if (hasEnd) + OnComplete(function); + OnAbort(function); + } + } +} diff --git a/osu.Framework/Graphics/Transforms/Transformable.cs b/osu.Framework/Graphics/Transforms/Transformable.cs index c4565d4cb..3b8d8a89a 100644 --- a/osu.Framework/Graphics/Transforms/Transformable.cs +++ b/osu.Framework/Graphics/Transforms/Transformable.cs @@ -1,455 +1,455 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Lists; -using osu.Framework.Timing; -using System.Linq; -using osu.Framework.Allocation; -using System.Collections.Generic; -using System.Diagnostics; -using osu.Framework.MathUtils; - -namespace osu.Framework.Graphics.Transforms -{ - /// - /// A type of object which can have s operating upon it. - /// An implementer of this class must call to - /// update and apply its s. - /// - public abstract class Transformable : ITransformable - { - /// - /// The clock that is used to provide the timing for this object's s. - /// - public abstract IFrameBasedClock Clock { get; set; } - - /// - /// The current frame's time as observed by this class's . - /// - public FrameTimeInfo Time => Clock.TimeInfo; - - /// - /// The starting time to use for new s. - /// - public double TransformStartTime => (Clock?.CurrentTime ?? 0) + TransformDelay; - - /// - /// Delay from the current time until new s are started, in milliseconds. - /// - protected double TransformDelay { get; private set; } - - private SortedList transformsLazy; - - private SortedList transforms => transformsLazy ?? (transformsLazy = new SortedList(Transform.COMPARER)); - - /// - /// A lazily-initialized list of s applied to this object. - /// - public IReadOnlyList Transforms => transforms; - - /// - /// The end time in milliseconds of the latest transform enqueued for this . - /// Will return the current time value if no transforms are present. - /// - public double LatestTransformEndTime - { - get - { - //expiry should happen either at the end of the last transform or using the current sequence delay (whichever is highest). - double max = TransformStartTime; - foreach (Transform t in Transforms) - if (t.EndTime > max) - max = t.EndTime + 1; //adding 1ms here ensures we can expire on the current frame without issue. - - return max; - } - } - - /// - /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. - /// - public virtual bool RemoveCompletedTransforms { get; internal set; } = true; - - /// - /// Resets and processes updates to this class based on loaded s. - /// - protected void UpdateTransforms() - { - TransformDelay = 0; - updateTransforms(Time.Current); - } - - private List removalActionsLazy; - private List removalActions => removalActionsLazy ?? (removalActionsLazy = new List()); - - private double lastUpdateTransformsTime; - - /// - /// Process updates to this class based on loaded s. This does not reset . - /// This is used for performing extra updates on s when new s are added. - /// - private void updateTransforms(double time) - { - bool rewinding = lastUpdateTransformsTime > time; - lastUpdateTransformsTime = time; - - if (transformsLazy == null) - return; - - if (rewinding && !RemoveCompletedTransforms) - { - var appliedToEndReverts = new List(); - - // Under the case that completed transforms are not removed, reversing the clock is permitted. - // We need to first look back through all the transforms and apply the start values of the ones that were previously - // applied, but now exist in the future relative to the current time. - for (int i = transformsLazy.Count - 1; i >= 0; i--) - { - var t = transformsLazy[i]; - - // rewind logic needs to only run on transforms which have been applied at least once. - if (!t.Applied) - continue; - - // some specific transforms can be marked as non-rewindable. - if (!t.Rewindable) - continue; - - if (time >= t.StartTime) - { - // we are in the middle of this transform, so we want to mark as not-completely-applied. - // note that we should only do this for the last transform of each TargetMemeber to avoid incorrect application order. - // the actual application will be in the main loop below now that AppliedToEnd is false. - if (!appliedToEndReverts.Contains(t.TargetMember)) - { - if (time < t.EndTime) - t.AppliedToEnd = false; - appliedToEndReverts.Add(t.TargetMember); - } - } - else - { - // we are before the start time of this transform, so we want to eagerly apply the value at current time and mark as not-yet-applied. - // this transform will not be applied again unless we play forward in the future. - t.Apply(time); - t.Applied = false; - t.AppliedToEnd = false; - } - } - } - - for (int i = 0; i < transformsLazy.Count; ++i) - { - var t = transformsLazy[i]; - - var tCanRewind = !RemoveCompletedTransforms && t.Rewindable; - - if (time < t.StartTime) - break; - - if (!t.Applied) - { - // This is the first time we are updating this transform. - // We will find other still active transforms which act on the same target member and remove them. - // Since following transforms acting on the same target member are immediately removed when a - // new one is added, we can be sure that previous transforms were added before this one and can - // be safely removed. - for (int j = 0; j < i; ++j) - { - var u = transformsLazy[j]; - if (u.TargetMember != t.TargetMember) continue; - - if (!u.AppliedToEnd) - // we may have applied the existing transforms too far into the future. - // we want to prepare to potentially read into the newly activated transform's StartTime, - // so we should re-apply using its StartTime as a basis. - u.Apply(t.StartTime); - - if (!tCanRewind) - { - transformsLazy.RemoveAt(j--); - i--; - - removalActions.Add(u.OnAbort); - } - else - u.AppliedToEnd = true; - } - } - - if (!t.HasStartValue) - { - t.ReadIntoStartValue(); - t.HasStartValue = true; - } - - if (!t.AppliedToEnd) - { - t.Apply(time); - - t.AppliedToEnd = time >= t.EndTime; - - if (t.AppliedToEnd) - { - if (!tCanRewind) - transformsLazy.RemoveAt(i--); - - if (t.IsLooping) - { - if (tCanRewind) - { - t.IsLooping = false; - t = t.Clone(); - } - - t.AppliedToEnd = false; - t.Applied = false; - t.HasStartValue = false; - - t.IsLooping = true; - - t.StartTime += t.LoopDelay; - t.EndTime += t.LoopDelay; - - // this could be added back at a lower index than where we are currently iterating, but - // running the same transform twice isn't a huge deal. - transformsLazy.Add(t); - } - else if (t.OnComplete != null) - removalActions.Add(t.OnComplete); - } - } - } - - invokePendingRemovalActions(); - } - - private void invokePendingRemovalActions() - { - if (removalActionsLazy?.Count > 0) - { - Debug.Assert(removalActionsLazy != null); - - var toRemove = removalActionsLazy.ToArray(); - removalActionsLazy.Clear(); - - foreach (var action in toRemove) - action(); - } - } - - /// - /// Removes a . - /// - /// The to remove. - public void RemoveTransform(Transform toRemove) - { - if (transformsLazy == null || !transformsLazy.Remove(toRemove)) - return; - - toRemove.OnAbort?.Invoke(); - } - - /// - /// Clears s. - /// - /// Whether we also clear the s of children. - /// - /// An optional name of s to clear. - /// Null for clearing all s. - /// - public virtual void ClearTransforms(bool propagateChildren = false, string targetMember = null) => ClearTransformsAfter(double.NegativeInfinity, propagateChildren, targetMember); - - /// - /// Removes s that start after . - /// - /// The time to clear s after. - /// Whether to also clear such s of children. - /// - /// An optional name of s to clear. - /// Null for clearing all s. - /// - public virtual void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) - { - if (transformsLazy == null) - return; - - Transform[] toAbort; - if (targetMember == null) - { - toAbort = transformsLazy.Where(t => t.StartTime >= time).ToArray(); - transformsLazy.RemoveAll(t => t.StartTime >= time); - } - else - { - toAbort = transformsLazy.Where(t => t.StartTime >= time && t.TargetMember == targetMember).ToArray(); - transformsLazy.RemoveAll(t => t.StartTime >= time && t.TargetMember == targetMember); - } - - foreach (var t in toAbort) - t.OnAbort?.Invoke(); - } - - /// - /// Applies s at a point in time. This may only be called if is set to false. - /// - /// This does not change the clock time. - /// - /// - /// The time to apply s at. - /// Whether to also apply children's s at . - public virtual void ApplyTransformsAt(double time, bool propagateChildren = false) - { - if (RemoveCompletedTransforms) throw new InvalidOperationException($"Cannot arbitrarily apply transforms with {nameof(RemoveCompletedTransforms)} active."); - updateTransforms(time); - } - - /// - /// Finishes specified s, using their . - /// - /// Whether we also finish the s of children. - /// - /// An optional name of s to finish. - /// Null for finishing all s. - /// - public virtual void FinishTransforms(bool propagateChildren = false, string targetMember = null) - { - if (transformsLazy == null) - return; - - Func toFlushPredicate; - if (targetMember == null) - toFlushPredicate = t => !t.IsLooping; - else - toFlushPredicate = t => !t.IsLooping && t.TargetMember == targetMember; - - // Flush is undefined for endlessly looping transforms - var toFlush = transformsLazy.Where(toFlushPredicate).ToArray(); - - transformsLazy.RemoveAll(t => toFlushPredicate(t)); - - foreach (Transform t in toFlush) - { - t.Apply(t.EndTime); - t.OnComplete?.Invoke(); - } - } - - /// - /// Add a delay duration to , in milliseconds. - /// - /// The delay duration to add. - /// Whether we also delay down the child tree. - /// This - internal virtual void AddDelay(double duration, bool propagateChildren = false) => TransformDelay += duration; - - /// - /// Start a sequence of s with a (cumulative) relative delay applied. - /// - /// The offset in milliseconds from current time. Note that this stacks with other nested sequences. - /// Whether this should be applied to all children. - /// A to be used in a using() statement. - public InvokeOnDisposal BeginDelayedSequence(double delay, bool recursive = false) - { - if (delay == 0) - return null; - - AddDelay(delay, recursive); - double newTransformDelay = TransformDelay; - - return new InvokeOnDisposal(() => - { - if (!Precision.AlmostEquals(newTransformDelay, TransformDelay)) - throw new InvalidOperationException( - $"{nameof(TransformStartTime)} at the end of delayed sequence is not the same as at the beginning, but should be. " + - $"(begin={newTransformDelay} end={TransformDelay})"); - - AddDelay(-delay, recursive); - }); - } - - /// - /// Start a sequence of s from an absolute time value (adjusts ). - /// - /// The new value for . - /// Whether this should be applied to all children. - /// A to be used in a using() statement. - /// Absolute sequences should never be nested inside another existing sequence. - public virtual InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false) - { - double oldTransformDelay = TransformDelay; - double newTransformDelay = TransformDelay = newTransformStartTime - (Clock?.CurrentTime ?? 0); - - return new InvokeOnDisposal(() => - { - if (!Precision.AlmostEquals(newTransformDelay, TransformDelay)) - throw new InvalidOperationException( - $"{nameof(TransformStartTime)} at the end of absolute sequence is not the same as at the beginning, but should be. " + - $"(begin={newTransformDelay} end={TransformDelay})"); - - TransformDelay = oldTransformDelay; - }); - } - - /// - /// Used to assign a monotonically increasing ID to s as they are added. This member is - /// incremented whenever a is added. - /// - private ulong currentTransformID; - - /// - /// Adds to this object a which was previously populated using this object via - /// . - /// Added s are immediately applied, and therefore have an immediate effect on this object if the current time of this - /// object falls within and . - /// If is null, e.g. because this object has just been constructed, then the given transform will be finished instantaneously. - /// - /// The to be added. - public void AddTransform(Transform transform) - { - if (transform == null) - throw new ArgumentNullException(nameof(transform)); - - if (!ReferenceEquals(transform.TargetTransformable, this)) - throw new InvalidOperationException( - $"{nameof(transform)} must have been populated via {nameof(TransformableExtensions)}.{nameof(TransformableExtensions.PopulateTransform)} " + - "using this object prior to being added."); - - if (Clock == null) - { - transform.Apply(transform.EndTime); - transform.OnComplete?.Invoke(); - return; - } - - Debug.Assert(!(transform.TransformID == 0 && transforms.Contains(transform)), $"Zero-id {nameof(Transform)}s should never be contained already."); - - // This contains check may be optimized away in the future, should it become a bottleneck - if (transform.TransformID != 0 && transforms.Contains(transform)) - throw new InvalidOperationException($"{nameof(Transformable)} may not contain the same {nameof(Transform)} more than once."); - - transform.TransformID = ++currentTransformID; - int insertionIndex = transforms.Add(transform); - - // Remove all existing following transforms touching the same property as this one. - for (int i = insertionIndex + 1; i < transformsLazy.Count; ++i) - { - var t = transformsLazy[i]; - if (t.TargetMember == transform.TargetMember) - { - transformsLazy.RemoveAt(i--); - if (t.OnAbort != null) - removalActions.Add(t.OnAbort); - } - } - - invokePendingRemovalActions(); - - // If our newly added transform could have an immediate effect, then let's - // make this effect happen immediately. - if (transform.StartTime < Time.Current || transform.EndTime <= Time.Current) - updateTransforms(Time.Current); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Lists; +using osu.Framework.Timing; +using System.Linq; +using osu.Framework.Allocation; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.MathUtils; + +namespace osu.Framework.Graphics.Transforms +{ + /// + /// A type of object which can have s operating upon it. + /// An implementer of this class must call to + /// update and apply its s. + /// + public abstract class Transformable : ITransformable + { + /// + /// The clock that is used to provide the timing for this object's s. + /// + public abstract IFrameBasedClock Clock { get; set; } + + /// + /// The current frame's time as observed by this class's . + /// + public FrameTimeInfo Time => Clock.TimeInfo; + + /// + /// The starting time to use for new s. + /// + public double TransformStartTime => (Clock?.CurrentTime ?? 0) + TransformDelay; + + /// + /// Delay from the current time until new s are started, in milliseconds. + /// + protected double TransformDelay { get; private set; } + + private SortedList transformsLazy; + + private SortedList transforms => transformsLazy ?? (transformsLazy = new SortedList(Transform.COMPARER)); + + /// + /// A lazily-initialized list of s applied to this object. + /// + public IReadOnlyList Transforms => transforms; + + /// + /// The end time in milliseconds of the latest transform enqueued for this . + /// Will return the current time value if no transforms are present. + /// + public double LatestTransformEndTime + { + get + { + //expiry should happen either at the end of the last transform or using the current sequence delay (whichever is highest). + double max = TransformStartTime; + foreach (Transform t in Transforms) + if (t.EndTime > max) + max = t.EndTime + 1; //adding 1ms here ensures we can expire on the current frame without issue. + + return max; + } + } + + /// + /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. + /// + public virtual bool RemoveCompletedTransforms { get; internal set; } = true; + + /// + /// Resets and processes updates to this class based on loaded s. + /// + protected void UpdateTransforms() + { + TransformDelay = 0; + updateTransforms(Time.Current); + } + + private List removalActionsLazy; + private List removalActions => removalActionsLazy ?? (removalActionsLazy = new List()); + + private double lastUpdateTransformsTime; + + /// + /// Process updates to this class based on loaded s. This does not reset . + /// This is used for performing extra updates on s when new s are added. + /// + private void updateTransforms(double time) + { + bool rewinding = lastUpdateTransformsTime > time; + lastUpdateTransformsTime = time; + + if (transformsLazy == null) + return; + + if (rewinding && !RemoveCompletedTransforms) + { + var appliedToEndReverts = new List(); + + // Under the case that completed transforms are not removed, reversing the clock is permitted. + // We need to first look back through all the transforms and apply the start values of the ones that were previously + // applied, but now exist in the future relative to the current time. + for (int i = transformsLazy.Count - 1; i >= 0; i--) + { + var t = transformsLazy[i]; + + // rewind logic needs to only run on transforms which have been applied at least once. + if (!t.Applied) + continue; + + // some specific transforms can be marked as non-rewindable. + if (!t.Rewindable) + continue; + + if (time >= t.StartTime) + { + // we are in the middle of this transform, so we want to mark as not-completely-applied. + // note that we should only do this for the last transform of each TargetMemeber to avoid incorrect application order. + // the actual application will be in the main loop below now that AppliedToEnd is false. + if (!appliedToEndReverts.Contains(t.TargetMember)) + { + if (time < t.EndTime) + t.AppliedToEnd = false; + appliedToEndReverts.Add(t.TargetMember); + } + } + else + { + // we are before the start time of this transform, so we want to eagerly apply the value at current time and mark as not-yet-applied. + // this transform will not be applied again unless we play forward in the future. + t.Apply(time); + t.Applied = false; + t.AppliedToEnd = false; + } + } + } + + for (int i = 0; i < transformsLazy.Count; ++i) + { + var t = transformsLazy[i]; + + var tCanRewind = !RemoveCompletedTransforms && t.Rewindable; + + if (time < t.StartTime) + break; + + if (!t.Applied) + { + // This is the first time we are updating this transform. + // We will find other still active transforms which act on the same target member and remove them. + // Since following transforms acting on the same target member are immediately removed when a + // new one is added, we can be sure that previous transforms were added before this one and can + // be safely removed. + for (int j = 0; j < i; ++j) + { + var u = transformsLazy[j]; + if (u.TargetMember != t.TargetMember) continue; + + if (!u.AppliedToEnd) + // we may have applied the existing transforms too far into the future. + // we want to prepare to potentially read into the newly activated transform's StartTime, + // so we should re-apply using its StartTime as a basis. + u.Apply(t.StartTime); + + if (!tCanRewind) + { + transformsLazy.RemoveAt(j--); + i--; + + removalActions.Add(u.OnAbort); + } + else + u.AppliedToEnd = true; + } + } + + if (!t.HasStartValue) + { + t.ReadIntoStartValue(); + t.HasStartValue = true; + } + + if (!t.AppliedToEnd) + { + t.Apply(time); + + t.AppliedToEnd = time >= t.EndTime; + + if (t.AppliedToEnd) + { + if (!tCanRewind) + transformsLazy.RemoveAt(i--); + + if (t.IsLooping) + { + if (tCanRewind) + { + t.IsLooping = false; + t = t.Clone(); + } + + t.AppliedToEnd = false; + t.Applied = false; + t.HasStartValue = false; + + t.IsLooping = true; + + t.StartTime += t.LoopDelay; + t.EndTime += t.LoopDelay; + + // this could be added back at a lower index than where we are currently iterating, but + // running the same transform twice isn't a huge deal. + transformsLazy.Add(t); + } + else if (t.OnComplete != null) + removalActions.Add(t.OnComplete); + } + } + } + + invokePendingRemovalActions(); + } + + private void invokePendingRemovalActions() + { + if (removalActionsLazy?.Count > 0) + { + Debug.Assert(removalActionsLazy != null); + + var toRemove = removalActionsLazy.ToArray(); + removalActionsLazy.Clear(); + + foreach (var action in toRemove) + action(); + } + } + + /// + /// Removes a . + /// + /// The to remove. + public void RemoveTransform(Transform toRemove) + { + if (transformsLazy == null || !transformsLazy.Remove(toRemove)) + return; + + toRemove.OnAbort?.Invoke(); + } + + /// + /// Clears s. + /// + /// Whether we also clear the s of children. + /// + /// An optional name of s to clear. + /// Null for clearing all s. + /// + public virtual void ClearTransforms(bool propagateChildren = false, string targetMember = null) => ClearTransformsAfter(double.NegativeInfinity, propagateChildren, targetMember); + + /// + /// Removes s that start after . + /// + /// The time to clear s after. + /// Whether to also clear such s of children. + /// + /// An optional name of s to clear. + /// Null for clearing all s. + /// + public virtual void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) + { + if (transformsLazy == null) + return; + + Transform[] toAbort; + if (targetMember == null) + { + toAbort = transformsLazy.Where(t => t.StartTime >= time).ToArray(); + transformsLazy.RemoveAll(t => t.StartTime >= time); + } + else + { + toAbort = transformsLazy.Where(t => t.StartTime >= time && t.TargetMember == targetMember).ToArray(); + transformsLazy.RemoveAll(t => t.StartTime >= time && t.TargetMember == targetMember); + } + + foreach (var t in toAbort) + t.OnAbort?.Invoke(); + } + + /// + /// Applies s at a point in time. This may only be called if is set to false. + /// + /// This does not change the clock time. + /// + /// + /// The time to apply s at. + /// Whether to also apply children's s at . + public virtual void ApplyTransformsAt(double time, bool propagateChildren = false) + { + if (RemoveCompletedTransforms) throw new InvalidOperationException($"Cannot arbitrarily apply transforms with {nameof(RemoveCompletedTransforms)} active."); + updateTransforms(time); + } + + /// + /// Finishes specified s, using their . + /// + /// Whether we also finish the s of children. + /// + /// An optional name of s to finish. + /// Null for finishing all s. + /// + public virtual void FinishTransforms(bool propagateChildren = false, string targetMember = null) + { + if (transformsLazy == null) + return; + + Func toFlushPredicate; + if (targetMember == null) + toFlushPredicate = t => !t.IsLooping; + else + toFlushPredicate = t => !t.IsLooping && t.TargetMember == targetMember; + + // Flush is undefined for endlessly looping transforms + var toFlush = transformsLazy.Where(toFlushPredicate).ToArray(); + + transformsLazy.RemoveAll(t => toFlushPredicate(t)); + + foreach (Transform t in toFlush) + { + t.Apply(t.EndTime); + t.OnComplete?.Invoke(); + } + } + + /// + /// Add a delay duration to , in milliseconds. + /// + /// The delay duration to add. + /// Whether we also delay down the child tree. + /// This + internal virtual void AddDelay(double duration, bool propagateChildren = false) => TransformDelay += duration; + + /// + /// Start a sequence of s with a (cumulative) relative delay applied. + /// + /// The offset in milliseconds from current time. Note that this stacks with other nested sequences. + /// Whether this should be applied to all children. + /// A to be used in a using() statement. + public InvokeOnDisposal BeginDelayedSequence(double delay, bool recursive = false) + { + if (delay == 0) + return null; + + AddDelay(delay, recursive); + double newTransformDelay = TransformDelay; + + return new InvokeOnDisposal(() => + { + if (!Precision.AlmostEquals(newTransformDelay, TransformDelay)) + throw new InvalidOperationException( + $"{nameof(TransformStartTime)} at the end of delayed sequence is not the same as at the beginning, but should be. " + + $"(begin={newTransformDelay} end={TransformDelay})"); + + AddDelay(-delay, recursive); + }); + } + + /// + /// Start a sequence of s from an absolute time value (adjusts ). + /// + /// The new value for . + /// Whether this should be applied to all children. + /// A to be used in a using() statement. + /// Absolute sequences should never be nested inside another existing sequence. + public virtual InvokeOnDisposal BeginAbsoluteSequence(double newTransformStartTime, bool recursive = false) + { + double oldTransformDelay = TransformDelay; + double newTransformDelay = TransformDelay = newTransformStartTime - (Clock?.CurrentTime ?? 0); + + return new InvokeOnDisposal(() => + { + if (!Precision.AlmostEquals(newTransformDelay, TransformDelay)) + throw new InvalidOperationException( + $"{nameof(TransformStartTime)} at the end of absolute sequence is not the same as at the beginning, but should be. " + + $"(begin={newTransformDelay} end={TransformDelay})"); + + TransformDelay = oldTransformDelay; + }); + } + + /// + /// Used to assign a monotonically increasing ID to s as they are added. This member is + /// incremented whenever a is added. + /// + private ulong currentTransformID; + + /// + /// Adds to this object a which was previously populated using this object via + /// . + /// Added s are immediately applied, and therefore have an immediate effect on this object if the current time of this + /// object falls within and . + /// If is null, e.g. because this object has just been constructed, then the given transform will be finished instantaneously. + /// + /// The to be added. + public void AddTransform(Transform transform) + { + if (transform == null) + throw new ArgumentNullException(nameof(transform)); + + if (!ReferenceEquals(transform.TargetTransformable, this)) + throw new InvalidOperationException( + $"{nameof(transform)} must have been populated via {nameof(TransformableExtensions)}.{nameof(TransformableExtensions.PopulateTransform)} " + + "using this object prior to being added."); + + if (Clock == null) + { + transform.Apply(transform.EndTime); + transform.OnComplete?.Invoke(); + return; + } + + Debug.Assert(!(transform.TransformID == 0 && transforms.Contains(transform)), $"Zero-id {nameof(Transform)}s should never be contained already."); + + // This contains check may be optimized away in the future, should it become a bottleneck + if (transform.TransformID != 0 && transforms.Contains(transform)) + throw new InvalidOperationException($"{nameof(Transformable)} may not contain the same {nameof(Transform)} more than once."); + + transform.TransformID = ++currentTransformID; + int insertionIndex = transforms.Add(transform); + + // Remove all existing following transforms touching the same property as this one. + for (int i = insertionIndex + 1; i < transformsLazy.Count; ++i) + { + var t = transformsLazy[i]; + if (t.TargetMember == transform.TargetMember) + { + transformsLazy.RemoveAt(i--); + if (t.OnAbort != null) + removalActions.Add(t.OnAbort); + } + } + + invokePendingRemovalActions(); + + // If our newly added transform could have an immediate effect, then let's + // make this effect happen immediately. + if (transform.StartTime < Time.Current || transform.EndTime <= Time.Current) + updateTransforms(Time.Current); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/BasicCheckbox.cs b/osu.Framework/Graphics/UserInterface/BasicCheckbox.cs index 704e6e05a..5bee0ecaa 100644 --- a/osu.Framework/Graphics/UserInterface/BasicCheckbox.cs +++ b/osu.Framework/Graphics/UserInterface/BasicCheckbox.cs @@ -1,80 +1,80 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions.Color4Extensions; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Framework.Graphics.UserInterface -{ - public class BasicCheckbox : Checkbox - { - public Color4 CheckedColor { get; set; } = Color4.White; - public Color4 UncheckedColor { get; set; } = Color4.White.Opacity(0.2f); - - public int FadeDuration { get; set; } = 50; - - public string LabelText - { - get { return labelSpriteText?.Text; } - set - { - if (labelSpriteText != null) - labelSpriteText.Text = value; - } - } - - public MarginPadding LabelPadding - { - get { return labelSpriteText?.Padding ?? new MarginPadding(); } - set - { - if (labelSpriteText != null) - labelSpriteText.Padding = value; - } - } - - private readonly SpriteText labelSpriteText; - - public BasicCheckbox() - { - Box box; - - AutoSizeAxes = Axes.Both; - - Child = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - labelSpriteText = new SpriteText - { - Padding = new MarginPadding - { - Left = 10 - }, - Depth = float.MinValue - }, - new Container - { - BorderColour= Color4.White, - BorderThickness = 3, - Masking = true, - Size = new Vector2(20, 20), - Child = box = new Box - { - RelativeSizeAxes = Axes.Both - } - } - } - }; - - Current.ValueChanged += c => box.FadeColour(c ? CheckedColor : UncheckedColor, FadeDuration); - Current.TriggerChange(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions.Color4Extensions; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; + +namespace osu.Framework.Graphics.UserInterface +{ + public class BasicCheckbox : Checkbox + { + public Color4 CheckedColor { get; set; } = Color4.White; + public Color4 UncheckedColor { get; set; } = Color4.White.Opacity(0.2f); + + public int FadeDuration { get; set; } = 50; + + public string LabelText + { + get { return labelSpriteText?.Text; } + set + { + if (labelSpriteText != null) + labelSpriteText.Text = value; + } + } + + public MarginPadding LabelPadding + { + get { return labelSpriteText?.Padding ?? new MarginPadding(); } + set + { + if (labelSpriteText != null) + labelSpriteText.Padding = value; + } + } + + private readonly SpriteText labelSpriteText; + + public BasicCheckbox() + { + Box box; + + AutoSizeAxes = Axes.Both; + + Child = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + labelSpriteText = new SpriteText + { + Padding = new MarginPadding + { + Left = 10 + }, + Depth = float.MinValue + }, + new Container + { + BorderColour= Color4.White, + BorderThickness = 3, + Masking = true, + Size = new Vector2(20, 20), + Child = box = new Box + { + RelativeSizeAxes = Axes.Both + } + } + } + }; + + Current.ValueChanged += c => box.FadeColour(c ? CheckedColor : UncheckedColor, FadeDuration); + Current.TriggerChange(); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs index dbb688e31..f2f007bd1 100644 --- a/osu.Framework/Graphics/UserInterface/BasicDropdown.cs +++ b/osu.Framework/Graphics/UserInterface/BasicDropdown.cs @@ -1,63 +1,63 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Sprites; -using OpenTK.Graphics; - -namespace osu.Framework.Graphics.UserInterface -{ - public class BasicDropdown : Dropdown - { - protected override DropdownMenu CreateMenu() => new BasicDropdownMenu(); - - protected override DropdownHeader CreateHeader() => new BasicDropdownHeader(); - - public BasicDropdown() - { - Header.CornerRadius = 4; - } - - public class BasicDropdownHeader : DropdownHeader - { - private readonly SpriteText label; - - protected internal override string Label - { - get { return label.Text; } - set { label.Text = value; } - } - - public BasicDropdownHeader() - { - Foreground.Padding = new MarginPadding(4); - BackgroundColour = new Color4(255, 255, 255, 100); - BackgroundColourHover = Color4.HotPink; - Children = new[] - { - label = new SpriteText(), - }; - } - } - - private class BasicDropdownMenu : DropdownMenu - { - public BasicDropdownMenu() - { - CornerRadius = 4; - } - - protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableBasicDropdownMenuItem(item); - - private class DrawableBasicDropdownMenuItem : DrawableDropdownMenuItem - { - public DrawableBasicDropdownMenuItem(MenuItem item) - : base(item) - { - Foreground.Padding = new MarginPadding(2); - } - - protected override Drawable CreateContent() => new SpriteText(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Sprites; +using OpenTK.Graphics; + +namespace osu.Framework.Graphics.UserInterface +{ + public class BasicDropdown : Dropdown + { + protected override DropdownMenu CreateMenu() => new BasicDropdownMenu(); + + protected override DropdownHeader CreateHeader() => new BasicDropdownHeader(); + + public BasicDropdown() + { + Header.CornerRadius = 4; + } + + public class BasicDropdownHeader : DropdownHeader + { + private readonly SpriteText label; + + protected internal override string Label + { + get { return label.Text; } + set { label.Text = value; } + } + + public BasicDropdownHeader() + { + Foreground.Padding = new MarginPadding(4); + BackgroundColour = new Color4(255, 255, 255, 100); + BackgroundColourHover = Color4.HotPink; + Children = new[] + { + label = new SpriteText(), + }; + } + } + + private class BasicDropdownMenu : DropdownMenu + { + public BasicDropdownMenu() + { + CornerRadius = 4; + } + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableBasicDropdownMenuItem(item); + + private class DrawableBasicDropdownMenuItem : DrawableDropdownMenuItem + { + public DrawableBasicDropdownMenuItem(MenuItem item) + : base(item) + { + Foreground.Padding = new MarginPadding(2); + } + + protected override Drawable CreateContent() => new SpriteText(); + } + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs index b0977888f..0b2ed8e21 100644 --- a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs @@ -1,54 +1,54 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Framework.Graphics.UserInterface -{ - public class BasicSliderBar : SliderBar - where T : struct, IComparable, IConvertible - { - public Color4 Color - { - get { return Box.Colour; } - set { Box.Colour = value; } - } - - public Color4 SelectionColor - { - get { return SelectionBox.Colour; } - set { SelectionBox.Colour = value; } - } - - protected readonly Box SelectionBox; - protected readonly Box Box; - - public BasicSliderBar() - { - CornerRadius = 4; - Masking = true; - - Children = new Drawable[] - { - Box = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.DarkMagenta, - }, - SelectionBox = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - } - }; - } - - protected override void UpdateValue(float value) - { - SelectionBox.ScaleTo(new Vector2(value, 1), 300, Easing.OutQuint); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Graphics.UserInterface +{ + public class BasicSliderBar : SliderBar + where T : struct, IComparable, IConvertible + { + public Color4 Color + { + get { return Box.Colour; } + set { Box.Colour = value; } + } + + public Color4 SelectionColor + { + get { return SelectionBox.Colour; } + set { SelectionBox.Colour = value; } + } + + protected readonly Box SelectionBox; + protected readonly Box Box; + + public BasicSliderBar() + { + CornerRadius = 4; + Masking = true; + + Children = new Drawable[] + { + Box = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkMagenta, + }, + SelectionBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + } + }; + } + + protected override void UpdateValue(float value) + { + SelectionBox.ScaleTo(new Vector2(value, 1), 300, Easing.OutQuint); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/Button.cs b/osu.Framework/Graphics/UserInterface/Button.cs index 1da5b4b05..b6836c5e0 100644 --- a/osu.Framework/Graphics/UserInterface/Button.cs +++ b/osu.Framework/Graphics/UserInterface/Button.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Framework.Graphics.UserInterface -{ - public class Button : ClickableContainer - { - public string Text - { - get { return SpriteText?.Text; } - set - { - if (SpriteText != null) - SpriteText.Text = value; - } - } - - public Color4 BackgroundColour - { - get { return Background.Colour; } - set { Background.FadeColour(value); } - } - - protected override Container Content => content; - - private readonly Container content; - - protected Box Background; - protected SpriteText SpriteText; - - public Button() - { - AddInternal(content = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - Background = new Box - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - SpriteText = CreateText(), - } - }); - } - - protected virtual SpriteText CreateText() => new SpriteText - { - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - }; - - protected override bool OnClick(InputState state) - { - if (Enabled.Value) - { - var flash = new Box - { - RelativeSizeAxes = Axes.Both - }; - - Add(flash); - - flash.Colour = Background.Colour; - flash.Blending = BlendingMode.Additive; - flash.FadeOutFromOne(200); - flash.Expire(); - } - - return base.OnClick(state); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Graphics.UserInterface +{ + public class Button : ClickableContainer + { + public string Text + { + get { return SpriteText?.Text; } + set + { + if (SpriteText != null) + SpriteText.Text = value; + } + } + + public Color4 BackgroundColour + { + get { return Background.Colour; } + set { Background.FadeColour(value); } + } + + protected override Container Content => content; + + private readonly Container content; + + protected Box Background; + protected SpriteText SpriteText; + + public Button() + { + AddInternal(content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + SpriteText = CreateText(), + } + }); + } + + protected virtual SpriteText CreateText() => new SpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }; + + protected override bool OnClick(InputState state) + { + if (Enabled.Value) + { + var flash = new Box + { + RelativeSizeAxes = Axes.Both + }; + + Add(flash); + + flash.Colour = Background.Colour; + flash.Blending = BlendingMode.Additive; + flash.FadeOutFromOne(200); + flash.Expire(); + } + + return base.OnClick(state); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/Checkbox.cs b/osu.Framework/Graphics/UserInterface/Checkbox.cs index 8abed9d52..6126c18ef 100644 --- a/osu.Framework/Graphics/UserInterface/Checkbox.cs +++ b/osu.Framework/Graphics/UserInterface/Checkbox.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.UserInterface -{ - public abstract class Checkbox : Container, IHasCurrentValue - { - public Bindable Current { get; } = new Bindable(); - - protected override bool OnClick(InputState state) - { - if (!Current.Disabled) - Current.Value = !Current; - - base.OnClick(state); - return true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.UserInterface +{ + public abstract class Checkbox : Container, IHasCurrentValue + { + public Bindable Current { get; } = new Bindable(); + + protected override bool OnClick(InputState state) + { + if (!Current.Disabled) + Current.Value = !Current; + + base.OnClick(state); + return true; + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/CircularProgress.cs b/osu.Framework/Graphics/UserInterface/CircularProgress.cs index 02f855f23..2ba4e91d7 100644 --- a/osu.Framework/Graphics/UserInterface/CircularProgress.cs +++ b/osu.Framework/Graphics/UserInterface/CircularProgress.cs @@ -1,104 +1,104 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Textures; -using OpenTK; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Allocation; -using osu.Framework.Configuration; - -namespace osu.Framework.Graphics.UserInterface -{ - public class CircularProgress : Drawable, IHasCurrentValue - { - public Bindable Current { get; } = new Bindable(); - - public CircularProgress() - { - Current.ValueChanged += newValue => Invalidate(Invalidation.DrawNode); - } - - private Shader roundedTextureShader; - private Shader textureShader; - - private readonly CircularProgressDrawNodeSharedData pathDrawNodeSharedData = new CircularProgressDrawNodeSharedData(); - - public bool CanDisposeTexture { get; protected set; } - - #region Disposal - - protected override void Dispose(bool isDisposing) - { - if (CanDisposeTexture) - { - texture?.Dispose(); - texture = null; - } - - base.Dispose(isDisposing); - } - - #endregion - - protected override DrawNode CreateDrawNode() => new CircularProgressDrawNode(); - - protected override void ApplyDrawNode(DrawNode node) - { - CircularProgressDrawNode n = (CircularProgressDrawNode)node; - - n.Texture = Texture; - n.TextureShader = textureShader; - n.RoundedTextureShader = roundedTextureShader; - n.DrawSize = DrawSize; - - n.Shared = pathDrawNodeSharedData; - - n.Angle = (float)Current.Value * MathHelper.TwoPi; - n.InnerRadius = innerRadius; - - base.ApplyDrawNode(node); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); - textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); - } - - private Texture texture = Texture.WhitePixel; - - public Texture Texture - { - get { return texture; } - set - { - if (value == texture) - return; - - if (texture != null && CanDisposeTexture) - texture.Dispose(); - - texture = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float innerRadius = 1; - - /// - /// The inner fill radius, relative to the of the . - /// The value range is 0 to 1 where 0 is invisible and 1 is completely filled. - /// The entire texture still fills the disk without cropping it. - /// - public float InnerRadius - { - get => innerRadius; - set - { - innerRadius = MathHelper.Clamp(value, 0, 1); - Invalidate(Invalidation.DrawNode); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Textures; +using OpenTK; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Allocation; +using osu.Framework.Configuration; + +namespace osu.Framework.Graphics.UserInterface +{ + public class CircularProgress : Drawable, IHasCurrentValue + { + public Bindable Current { get; } = new Bindable(); + + public CircularProgress() + { + Current.ValueChanged += newValue => Invalidate(Invalidation.DrawNode); + } + + private Shader roundedTextureShader; + private Shader textureShader; + + private readonly CircularProgressDrawNodeSharedData pathDrawNodeSharedData = new CircularProgressDrawNodeSharedData(); + + public bool CanDisposeTexture { get; protected set; } + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + if (CanDisposeTexture) + { + texture?.Dispose(); + texture = null; + } + + base.Dispose(isDisposing); + } + + #endregion + + protected override DrawNode CreateDrawNode() => new CircularProgressDrawNode(); + + protected override void ApplyDrawNode(DrawNode node) + { + CircularProgressDrawNode n = (CircularProgressDrawNode)node; + + n.Texture = Texture; + n.TextureShader = textureShader; + n.RoundedTextureShader = roundedTextureShader; + n.DrawSize = DrawSize; + + n.Shared = pathDrawNodeSharedData; + + n.Angle = (float)Current.Value * MathHelper.TwoPi; + n.InnerRadius = innerRadius; + + base.ApplyDrawNode(node); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + roundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + textureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + } + + private Texture texture = Texture.WhitePixel; + + public Texture Texture + { + get { return texture; } + set + { + if (value == texture) + return; + + if (texture != null && CanDisposeTexture) + texture.Dispose(); + + texture = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float innerRadius = 1; + + /// + /// The inner fill radius, relative to the of the . + /// The value range is 0 to 1 where 0 is invisible and 1 is completely filled. + /// The entire texture still fills the disk without cropping it. + /// + public float InnerRadius + { + get => innerRadius; + set + { + innerRadius = MathHelper.Clamp(value, 0, 1); + Invalidate(Invalidation.DrawNode); + } + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/CircularProgressDrawNode.cs b/osu.Framework/Graphics/UserInterface/CircularProgressDrawNode.cs index 46499671e..65fc56172 100644 --- a/osu.Framework/Graphics/UserInterface/CircularProgressDrawNode.cs +++ b/osu.Framework/Graphics/UserInterface/CircularProgressDrawNode.cs @@ -1,139 +1,139 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.OpenGL; -using OpenTK; -using System; -using osu.Framework.Graphics.Batches; -using OpenTK.Graphics; -using osu.Framework.Extensions.MatrixExtensions; -using osu.Framework.Graphics.OpenGL.Vertices; - -namespace osu.Framework.Graphics.UserInterface -{ - public class CircularProgressDrawNodeSharedData - { - // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices - // per primitive (quads in this case). Otherwise overflowing the batch will result in wrong - // grouping of vertices into primitives. - public LinearBatch HalfCircleBatch = new LinearBatch(CircularProgressDrawNode.MAXRES * 100 * 4, 10, PrimitiveType.Quads); - } - - public class CircularProgressDrawNode : DrawNode - { - public const int MAXRES = 24; - public float Angle; - public float InnerRadius = 1; - - public Vector2 DrawSize; - public Texture Texture; - - public Shader TextureShader; - public Shader RoundedTextureShader; - - public CircularProgressDrawNodeSharedData Shared; - - private bool needsRoundedShader => GLWrapper.IsMaskingActive; - - private Vector2 pointOnCircle(float angle) => new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)); - private float angleToUnitInterval(float angle) => angle / MathHelper.TwoPi + (angle >= 0 ? 0 : 1); - - // Gets colour at the localPos position in the unit square of our Colour gradient box. - private Color4 colourAt(Vector2 localPos) => DrawInfo.Colour.HasSingleColour - ? (Color4)DrawInfo.Colour - : DrawInfo.Colour.Interpolate(localPos).Linear; - - private static readonly Vector2 origin = new Vector2(0.5f, 0.5f); - private void updateVertexBuffer() - { - const float start_angle = 0; - const float step = MathHelper.Pi / MAXRES; - - float dir = Math.Sign(Angle); - - int amountPoints = (int)Math.Ceiling(Math.Abs(Angle) / step); - - Matrix3 transformationMatrix = DrawInfo.Matrix; - MatrixExtensions.ScaleFromLeft(ref transformationMatrix, DrawSize); - - Vector2 current = origin + pointOnCircle(start_angle) * 0.5f; - Color4 currentColour = colourAt(current); - current = Vector2Extensions.Transform(current, transformationMatrix); - - Vector2 screenOrigin = Vector2Extensions.Transform(origin, transformationMatrix); - Color4 originColour = colourAt(origin); - - for (int i = 1; i <= amountPoints; i++) - { - // Clamps the angle so we don't overshoot. - // dir is used so negative angles result in negative angularOffset. - float angularOffset = dir * Math.Min(i * step, dir * Angle); - float normalisedStartAngle = amountPoints > 1 - ? (1 - 1 / Texture.Width) * ((float)(i - 1) / amountPoints * Angle / MathHelper.TwoPi + (dir > 0 ? 0 : 1)) - : 0; - float normalisedEndAngle = amountPoints > 1 - ? (1 - 1 / Texture.Width) * ((float)i / amountPoints * Angle / MathHelper.TwoPi + (dir > 0 ? 0 : 1)) - : 0; - - // First center point - Shared.HalfCircleBatch.Add(new TexturedVertex2D - { - Position = Vector2.Lerp(current, screenOrigin, InnerRadius), - TexturePosition = new Vector2((normalisedStartAngle + normalisedEndAngle) / 2, 0), - Colour = originColour - }); - - // First outer point. (Note that this uses the same `current` as the second point of previous iteration) - Shared.HalfCircleBatch.Add(new TexturedVertex2D - { - Position = new Vector2(current.X, current.Y), - TexturePosition = new Vector2(normalisedStartAngle, 1 - 1 / Texture.Height), - Colour = currentColour - }); - - // Update `current` - current = origin + pointOnCircle(start_angle + angularOffset) * 0.5f; - currentColour = colourAt(current); - current = Vector2Extensions.Transform(current, transformationMatrix); - - // Second outer point - Shared.HalfCircleBatch.Add(new TexturedVertex2D - { - Position = new Vector2(current.X, current.Y), - TexturePosition = new Vector2(normalisedEndAngle, 1 - 1 / Texture.Height), - Colour = currentColour - }); - - // Second center point - Shared.HalfCircleBatch.Add(new TexturedVertex2D - { - Position = Vector2.Lerp(current, screenOrigin, InnerRadius), - TexturePosition = new Vector2((normalisedStartAngle + normalisedEndAngle) / 2, 0), - Colour = originColour - }); - } - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - if (Texture == null || Texture.IsDisposed) - return; - - Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; - - shader.Bind(); - - Texture.TextureGL.WrapMode = TextureWrapMode.ClampToEdge; - Texture.TextureGL.Bind(); - - updateVertexBuffer(); - - shader.Unbind(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics.ES30; +using osu.Framework.Graphics.OpenGL; +using OpenTK; +using System; +using osu.Framework.Graphics.Batches; +using OpenTK.Graphics; +using osu.Framework.Extensions.MatrixExtensions; +using osu.Framework.Graphics.OpenGL.Vertices; + +namespace osu.Framework.Graphics.UserInterface +{ + public class CircularProgressDrawNodeSharedData + { + // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices + // per primitive (quads in this case). Otherwise overflowing the batch will result in wrong + // grouping of vertices into primitives. + public LinearBatch HalfCircleBatch = new LinearBatch(CircularProgressDrawNode.MAXRES * 100 * 4, 10, PrimitiveType.Quads); + } + + public class CircularProgressDrawNode : DrawNode + { + public const int MAXRES = 24; + public float Angle; + public float InnerRadius = 1; + + public Vector2 DrawSize; + public Texture Texture; + + public Shader TextureShader; + public Shader RoundedTextureShader; + + public CircularProgressDrawNodeSharedData Shared; + + private bool needsRoundedShader => GLWrapper.IsMaskingActive; + + private Vector2 pointOnCircle(float angle) => new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)); + private float angleToUnitInterval(float angle) => angle / MathHelper.TwoPi + (angle >= 0 ? 0 : 1); + + // Gets colour at the localPos position in the unit square of our Colour gradient box. + private Color4 colourAt(Vector2 localPos) => DrawInfo.Colour.HasSingleColour + ? (Color4)DrawInfo.Colour + : DrawInfo.Colour.Interpolate(localPos).Linear; + + private static readonly Vector2 origin = new Vector2(0.5f, 0.5f); + private void updateVertexBuffer() + { + const float start_angle = 0; + const float step = MathHelper.Pi / MAXRES; + + float dir = Math.Sign(Angle); + + int amountPoints = (int)Math.Ceiling(Math.Abs(Angle) / step); + + Matrix3 transformationMatrix = DrawInfo.Matrix; + MatrixExtensions.ScaleFromLeft(ref transformationMatrix, DrawSize); + + Vector2 current = origin + pointOnCircle(start_angle) * 0.5f; + Color4 currentColour = colourAt(current); + current = Vector2Extensions.Transform(current, transformationMatrix); + + Vector2 screenOrigin = Vector2Extensions.Transform(origin, transformationMatrix); + Color4 originColour = colourAt(origin); + + for (int i = 1; i <= amountPoints; i++) + { + // Clamps the angle so we don't overshoot. + // dir is used so negative angles result in negative angularOffset. + float angularOffset = dir * Math.Min(i * step, dir * Angle); + float normalisedStartAngle = amountPoints > 1 + ? (1 - 1 / Texture.Width) * ((float)(i - 1) / amountPoints * Angle / MathHelper.TwoPi + (dir > 0 ? 0 : 1)) + : 0; + float normalisedEndAngle = amountPoints > 1 + ? (1 - 1 / Texture.Width) * ((float)i / amountPoints * Angle / MathHelper.TwoPi + (dir > 0 ? 0 : 1)) + : 0; + + // First center point + Shared.HalfCircleBatch.Add(new TexturedVertex2D + { + Position = Vector2.Lerp(current, screenOrigin, InnerRadius), + TexturePosition = new Vector2((normalisedStartAngle + normalisedEndAngle) / 2, 0), + Colour = originColour + }); + + // First outer point. (Note that this uses the same `current` as the second point of previous iteration) + Shared.HalfCircleBatch.Add(new TexturedVertex2D + { + Position = new Vector2(current.X, current.Y), + TexturePosition = new Vector2(normalisedStartAngle, 1 - 1 / Texture.Height), + Colour = currentColour + }); + + // Update `current` + current = origin + pointOnCircle(start_angle + angularOffset) * 0.5f; + currentColour = colourAt(current); + current = Vector2Extensions.Transform(current, transformationMatrix); + + // Second outer point + Shared.HalfCircleBatch.Add(new TexturedVertex2D + { + Position = new Vector2(current.X, current.Y), + TexturePosition = new Vector2(normalisedEndAngle, 1 - 1 / Texture.Height), + Colour = currentColour + }); + + // Second center point + Shared.HalfCircleBatch.Add(new TexturedVertex2D + { + Position = Vector2.Lerp(current, screenOrigin, InnerRadius), + TexturePosition = new Vector2((normalisedStartAngle + normalisedEndAngle) / 2, 0), + Colour = originColour + }); + } + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + if (Texture == null || Texture.IsDisposed) + return; + + Shader shader = needsRoundedShader ? RoundedTextureShader : TextureShader; + + shader.Bind(); + + Texture.TextureGL.WrapMode = TextureWrapMode.ClampToEdge; + Texture.TextureGL.Bind(); + + updateVertexBuffer(); + + shader.Unbind(); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/Counter.cs b/osu.Framework/Graphics/UserInterface/Counter.cs index 7b42cbd07..2df7166ba 100644 --- a/osu.Framework/Graphics/UserInterface/Counter.cs +++ b/osu.Framework/Graphics/UserInterface/Counter.cs @@ -1,45 +1,45 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; - -namespace osu.Framework.Graphics.UserInterface -{ - /// - /// A drawable object that supports counting to values. - /// - public class Counter : CompositeDrawable - { - private double count; - /// - /// The current count. - /// - protected double Count - { - get { return count; } - private set - { - if (count == value) - return; - count = value; - - OnCountChanged(count); - } - } - - /// - /// Invoked when has changed. - /// - protected virtual void OnCountChanged(double count) { } - - public TransformSequence CountTo(double endCount, double duration = 0, Easing easing = Easing.None) - => this.TransformTo(nameof(Count), endCount, duration, easing); - } - - public static class CounterTransformSequenceExtensions - { - public static TransformSequence CountTo(this TransformSequence t, double endCount, double duration = 0, Easing easing = Easing.None) - => t.Append(o => o.CountTo(endCount, duration, easing)); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; + +namespace osu.Framework.Graphics.UserInterface +{ + /// + /// A drawable object that supports counting to values. + /// + public class Counter : CompositeDrawable + { + private double count; + /// + /// The current count. + /// + protected double Count + { + get { return count; } + private set + { + if (count == value) + return; + count = value; + + OnCountChanged(count); + } + } + + /// + /// Invoked when has changed. + /// + protected virtual void OnCountChanged(double count) { } + + public TransformSequence CountTo(double endCount, double duration = 0, Easing easing = Easing.None) + => this.TransformTo(nameof(Count), endCount, duration, easing); + } + + public static class CounterTransformSequenceExtensions + { + public static TransformSequence CountTo(this TransformSequence t, double endCount, double duration = 0, Easing easing = Easing.None) + => t.Append(o => o.CountTo(endCount, duration, easing)); + } +} diff --git a/osu.Framework/Graphics/UserInterface/Dropdown.cs b/osu.Framework/Graphics/UserInterface/Dropdown.cs index bba9bb810..b2e0a11ac 100644 --- a/osu.Framework/Graphics/UserInterface/Dropdown.cs +++ b/osu.Framework/Graphics/UserInterface/Dropdown.cs @@ -1,312 +1,312 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using OpenTK.Graphics; -using osu.Framework.Extensions.IEnumerableExtensions; - -namespace osu.Framework.Graphics.UserInterface -{ - /// - /// A drop-down menu to select from a group of values. - /// - /// Type of value to select. - public abstract class Dropdown : FillFlowContainer, IHasCurrentValue - { - protected internal DropdownHeader Header; - protected internal DropdownMenu Menu; - - /// - /// Creates the header part of the control. - /// - protected abstract DropdownHeader CreateHeader(); - - /// - /// A mapping from menu items to their values. - /// - private readonly Dictionary> itemMap = new Dictionary>(); - - protected IEnumerable> MenuItems => itemMap.Values; - - /// - /// Generate menu items by . - /// The part will become , - /// the part will become . - /// - public IEnumerable> Items - { - get { return MenuItems.Select(i => new KeyValuePair(i.Text, i.Value)); } - set - { - ClearItems(); - if (value == null) - return; - - foreach (var entry in value) - AddDropdownItem(entry.Key, entry.Value); - - if (Current.Value == null || !itemMap.Keys.Contains(Current.Value)) - Current.Value = itemMap.Keys.FirstOrDefault(); - else - Current.TriggerChange(); - } - } - - /// - /// Add a menu item directly. - /// - /// Text to display on the menu item. - /// Value selected by the menu item. - public void AddDropdownItem(string text, T value) - { - if (itemMap.ContainsKey(value)) - throw new ArgumentException($"The item {value} already exists in this {nameof(Dropdown)}."); - - var newItem = new DropdownMenuItem(text, value, () => - { - if (!Current.Disabled) - Current.Value = value; - - Menu.State = MenuState.Closed; - }); - - Menu.Add(newItem); - itemMap[value] = newItem; - } - - /// - /// Remove a menu item directly. - /// - /// Value of the menu item to be removed. - public bool RemoveDropdownItem(T value) - { - if (value == null) - return false; - - if (!itemMap.TryGetValue(value, out DropdownMenuItem item)) - return false; - - Menu.Remove(item); - itemMap.Remove(value); - - return true; - } - - public Bindable Current { get; } = new Bindable(); - - private DropdownMenuItem selectedItem; - - protected DropdownMenuItem SelectedItem - { - get { return selectedItem; } - set - { - selectedItem = value; - if (value != null) - Current.Value = value.Value; - } - } - - protected Dropdown() - { - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - - Children = new Drawable[] - { - Header = CreateHeader(), - Menu = CreateMenu() - }; - - Menu.RelativeSizeAxes = Axes.X; - - Header.Action = Menu.Toggle; - Current.ValueChanged += selectionChanged; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Header.Label = SelectedItem?.Text.Value; - } - - private void selectionChanged(T newSelection = default(T)) - { - // refresh if SelectedItem and SelectedValue mismatched - // null is not a valid value for Dictionary, so neither here - if ((SelectedItem == null || !EqualityComparer.Default.Equals(SelectedItem.Value, newSelection)) - && newSelection != null) - { - if (!itemMap.TryGetValue(newSelection, out selectedItem)) - throw new InvalidOperationException($"Attempted to update dropdown to a value which wasn't contained as an item ({newSelection})."); - } - - Menu.SelectItem(selectedItem); - Header.Label = selectedItem.Text; - } - - /// - /// Clear all the menu items. - /// - public void ClearItems() - { - itemMap.Clear(); - Menu.Clear(); - } - - /// - /// Hide the menu item of specified value. - /// - /// The value to hide. - internal void HideItem(T val) - { - if (itemMap.TryGetValue(val, out DropdownMenuItem item)) - { - Menu.HideItem(item); - updateHeaderVisibility(); - } - } - - /// - /// Show the menu item of specified value. - /// - /// The value to show. - internal void ShowItem(T val) - { - if (itemMap.TryGetValue(val, out DropdownMenuItem item)) - { - Menu.ShowItem(item); - updateHeaderVisibility(); - } - } - - private void updateHeaderVisibility() => Header.Alpha = Menu.AnyPresent ? 1 : 0; - - protected override bool OnHover(InputState state) => true; - - /// - /// Creates the menu body. - /// - protected virtual DropdownMenu CreateMenu() => new DropdownMenu(); - - #region DropdownMenu - public class DropdownMenu : Menu - { - public DropdownMenu() - : base(Direction.Vertical) - { - } - - /// - /// Selects an item from this . - /// - /// The item to select. - public void SelectItem(DropdownMenuItem item) - { - Children.OfType().ForEach(c => c.IsSelected = c.Item == item); - } - - /// - /// Shows an item from this . - /// - /// The item to show. - public void HideItem(DropdownMenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.Hide(); - - /// - /// Hides an item from this - /// - /// - public void ShowItem(DropdownMenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.Show(); - - /// - /// Whether any items part of this are present. - /// - public bool AnyPresent => Children.Any(c => c.IsPresent); - - protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableDropdownMenuItem(item); - - #region DrawableDropdownMenuItem - // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 - public class DrawableDropdownMenuItem : DrawableMenuItem - { - public DrawableDropdownMenuItem(MenuItem item) - : base(item) - { - } - - private bool selected; - public bool IsSelected - { - get - { - return !Item.Action.Disabled && selected; - } - set - { - if (selected == value) - return; - selected = value; - - OnSelectChange(); - } - } - - private Color4 backgroundColourSelected = Color4.SlateGray; - public Color4 BackgroundColourSelected - { - get { return backgroundColourSelected; } - set - { - backgroundColourSelected = value; - UpdateBackgroundColour(); - } - } - - private Color4 foregroundColourSelected = Color4.White; - public Color4 ForegroundColourSelected - { - get { return foregroundColourSelected; } - set - { - foregroundColourSelected = value; - UpdateForegroundColour(); - } - } - - protected virtual void OnSelectChange() - { - if (!IsLoaded) - return; - - UpdateBackgroundColour(); - UpdateForegroundColour(); - } - - protected override void UpdateBackgroundColour() - { - Background.FadeColour(IsHovered ? BackgroundColourHover : (IsSelected ? BackgroundColourSelected : BackgroundColour)); - } - - protected override void UpdateForegroundColour() - { - Foreground.FadeColour(IsHovered ? ForegroundColourHover : (IsSelected ? ForegroundColourSelected : ForegroundColour)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Background.Colour = IsSelected ? BackgroundColourSelected : BackgroundColour; - Foreground.Colour = IsSelected ? ForegroundColourSelected : ForegroundColour; - } - } - #endregion - } - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using OpenTK.Graphics; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Framework.Graphics.UserInterface +{ + /// + /// A drop-down menu to select from a group of values. + /// + /// Type of value to select. + public abstract class Dropdown : FillFlowContainer, IHasCurrentValue + { + protected internal DropdownHeader Header; + protected internal DropdownMenu Menu; + + /// + /// Creates the header part of the control. + /// + protected abstract DropdownHeader CreateHeader(); + + /// + /// A mapping from menu items to their values. + /// + private readonly Dictionary> itemMap = new Dictionary>(); + + protected IEnumerable> MenuItems => itemMap.Values; + + /// + /// Generate menu items by . + /// The part will become , + /// the part will become . + /// + public IEnumerable> Items + { + get { return MenuItems.Select(i => new KeyValuePair(i.Text, i.Value)); } + set + { + ClearItems(); + if (value == null) + return; + + foreach (var entry in value) + AddDropdownItem(entry.Key, entry.Value); + + if (Current.Value == null || !itemMap.Keys.Contains(Current.Value)) + Current.Value = itemMap.Keys.FirstOrDefault(); + else + Current.TriggerChange(); + } + } + + /// + /// Add a menu item directly. + /// + /// Text to display on the menu item. + /// Value selected by the menu item. + public void AddDropdownItem(string text, T value) + { + if (itemMap.ContainsKey(value)) + throw new ArgumentException($"The item {value} already exists in this {nameof(Dropdown)}."); + + var newItem = new DropdownMenuItem(text, value, () => + { + if (!Current.Disabled) + Current.Value = value; + + Menu.State = MenuState.Closed; + }); + + Menu.Add(newItem); + itemMap[value] = newItem; + } + + /// + /// Remove a menu item directly. + /// + /// Value of the menu item to be removed. + public bool RemoveDropdownItem(T value) + { + if (value == null) + return false; + + if (!itemMap.TryGetValue(value, out DropdownMenuItem item)) + return false; + + Menu.Remove(item); + itemMap.Remove(value); + + return true; + } + + public Bindable Current { get; } = new Bindable(); + + private DropdownMenuItem selectedItem; + + protected DropdownMenuItem SelectedItem + { + get { return selectedItem; } + set + { + selectedItem = value; + if (value != null) + Current.Value = value.Value; + } + } + + protected Dropdown() + { + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + Children = new Drawable[] + { + Header = CreateHeader(), + Menu = CreateMenu() + }; + + Menu.RelativeSizeAxes = Axes.X; + + Header.Action = Menu.Toggle; + Current.ValueChanged += selectionChanged; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Header.Label = SelectedItem?.Text.Value; + } + + private void selectionChanged(T newSelection = default(T)) + { + // refresh if SelectedItem and SelectedValue mismatched + // null is not a valid value for Dictionary, so neither here + if ((SelectedItem == null || !EqualityComparer.Default.Equals(SelectedItem.Value, newSelection)) + && newSelection != null) + { + if (!itemMap.TryGetValue(newSelection, out selectedItem)) + throw new InvalidOperationException($"Attempted to update dropdown to a value which wasn't contained as an item ({newSelection})."); + } + + Menu.SelectItem(selectedItem); + Header.Label = selectedItem.Text; + } + + /// + /// Clear all the menu items. + /// + public void ClearItems() + { + itemMap.Clear(); + Menu.Clear(); + } + + /// + /// Hide the menu item of specified value. + /// + /// The value to hide. + internal void HideItem(T val) + { + if (itemMap.TryGetValue(val, out DropdownMenuItem item)) + { + Menu.HideItem(item); + updateHeaderVisibility(); + } + } + + /// + /// Show the menu item of specified value. + /// + /// The value to show. + internal void ShowItem(T val) + { + if (itemMap.TryGetValue(val, out DropdownMenuItem item)) + { + Menu.ShowItem(item); + updateHeaderVisibility(); + } + } + + private void updateHeaderVisibility() => Header.Alpha = Menu.AnyPresent ? 1 : 0; + + protected override bool OnHover(InputState state) => true; + + /// + /// Creates the menu body. + /// + protected virtual DropdownMenu CreateMenu() => new DropdownMenu(); + + #region DropdownMenu + public class DropdownMenu : Menu + { + public DropdownMenu() + : base(Direction.Vertical) + { + } + + /// + /// Selects an item from this . + /// + /// The item to select. + public void SelectItem(DropdownMenuItem item) + { + Children.OfType().ForEach(c => c.IsSelected = c.Item == item); + } + + /// + /// Shows an item from this . + /// + /// The item to show. + public void HideItem(DropdownMenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.Hide(); + + /// + /// Hides an item from this + /// + /// + public void ShowItem(DropdownMenuItem item) => Children.FirstOrDefault(c => c.Item == item)?.Show(); + + /// + /// Whether any items part of this are present. + /// + public bool AnyPresent => Children.Any(c => c.IsPresent); + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableDropdownMenuItem(item); + + #region DrawableDropdownMenuItem + // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 + public class DrawableDropdownMenuItem : DrawableMenuItem + { + public DrawableDropdownMenuItem(MenuItem item) + : base(item) + { + } + + private bool selected; + public bool IsSelected + { + get + { + return !Item.Action.Disabled && selected; + } + set + { + if (selected == value) + return; + selected = value; + + OnSelectChange(); + } + } + + private Color4 backgroundColourSelected = Color4.SlateGray; + public Color4 BackgroundColourSelected + { + get { return backgroundColourSelected; } + set + { + backgroundColourSelected = value; + UpdateBackgroundColour(); + } + } + + private Color4 foregroundColourSelected = Color4.White; + public Color4 ForegroundColourSelected + { + get { return foregroundColourSelected; } + set + { + foregroundColourSelected = value; + UpdateForegroundColour(); + } + } + + protected virtual void OnSelectChange() + { + if (!IsLoaded) + return; + + UpdateBackgroundColour(); + UpdateForegroundColour(); + } + + protected override void UpdateBackgroundColour() + { + Background.FadeColour(IsHovered ? BackgroundColourHover : (IsSelected ? BackgroundColourSelected : BackgroundColour)); + } + + protected override void UpdateForegroundColour() + { + Foreground.FadeColour(IsHovered ? ForegroundColourHover : (IsSelected ? ForegroundColourSelected : ForegroundColour)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Background.Colour = IsSelected ? BackgroundColourSelected : BackgroundColour; + Foreground.Colour = IsSelected ? ForegroundColourSelected : ForegroundColour; + } + } + #endregion + } + #endregion + } +} diff --git a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs index 1a07a313a..ffc974acb 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownHeader.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownHeader.cs @@ -1,76 +1,76 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.UserInterface -{ - public abstract class DropdownHeader : ClickableContainer - { - protected Container Background; - protected Container Foreground; - - private Color4 backgroundColour = Color4.DarkGray; - - protected Color4 BackgroundColour - { - get { return backgroundColour; } - set - { - backgroundColour = value; - Background.Colour = value; - } - } - - protected Color4 BackgroundColourHover { get; set; } = Color4.Gray; - - protected override Container Content => Foreground; - - protected internal abstract string Label { get; set; } - - protected DropdownHeader() - { - Masking = true; - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Width = 1; - InternalChildren = new Drawable[] - { - Background = new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Colour = Color4.DarkGray, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - }, - Foreground = new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }, - }; - } - - protected override bool OnHover(InputState state) - { - Background.Colour = BackgroundColourHover; - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - Background.Colour = BackgroundColour; - base.OnHoverLost(state); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.UserInterface +{ + public abstract class DropdownHeader : ClickableContainer + { + protected Container Background; + protected Container Foreground; + + private Color4 backgroundColour = Color4.DarkGray; + + protected Color4 BackgroundColour + { + get { return backgroundColour; } + set + { + backgroundColour = value; + Background.Colour = value; + } + } + + protected Color4 BackgroundColourHover { get; set; } = Color4.Gray; + + protected override Container Content => Foreground; + + protected internal abstract string Label { get; set; } + + protected DropdownHeader() + { + Masking = true; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 1; + InternalChildren = new Drawable[] + { + Background = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkGray, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + }, + Foreground = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + }; + } + + protected override bool OnHover(InputState state) + { + Background.Colour = BackgroundColourHover; + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + Background.Colour = BackgroundColour; + base.OnHoverLost(state); + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/DropdownMenuItem.cs b/osu.Framework/Graphics/UserInterface/DropdownMenuItem.cs index ceedcb709..30e1197d9 100644 --- a/osu.Framework/Graphics/UserInterface/DropdownMenuItem.cs +++ b/osu.Framework/Graphics/UserInterface/DropdownMenuItem.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Graphics.UserInterface -{ - public class DropdownMenuItem : MenuItem - { - public readonly T Value; - - public DropdownMenuItem(string text, T value) - : base(text) - { - if (value == null) throw new ArgumentNullException(nameof(value)); - - Value = value; - } - - public DropdownMenuItem(string text, T value, Action action) - : base(text, action) - { - if (value == null) throw new ArgumentNullException(nameof(value)); - - Value = value; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Graphics.UserInterface +{ + public class DropdownMenuItem : MenuItem + { + public readonly T Value; + + public DropdownMenuItem(string text, T value) + : base(text) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + + Value = value; + } + + public DropdownMenuItem(string text, T value, Action action) + : base(text, action) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + + Value = value; + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/IHasCurrentValue.cs b/osu.Framework/Graphics/UserInterface/IHasCurrentValue.cs index 1cf882ec4..d09390db7 100644 --- a/osu.Framework/Graphics/UserInterface/IHasCurrentValue.cs +++ b/osu.Framework/Graphics/UserInterface/IHasCurrentValue.cs @@ -1,16 +1,16 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; - -namespace osu.Framework.Graphics.UserInterface -{ - /// - /// A UI element which supports a current value. - /// You can bind to 's to get updates. - /// - public interface IHasCurrentValue - { - Bindable Current { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Framework.Graphics.UserInterface +{ + /// + /// A UI element which supports a current value. + /// You can bind to 's to get updates. + /// + public interface IHasCurrentValue + { + Bindable Current { get; } + } +} diff --git a/osu.Framework/Graphics/UserInterface/Menu.cs b/osu.Framework/Graphics/UserInterface/Menu.cs index bbb5742f7..014ce9eb9 100644 --- a/osu.Framework/Graphics/UserInterface/Menu.cs +++ b/osu.Framework/Graphics/UserInterface/Menu.cs @@ -1,780 +1,780 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Caching; -using osu.Framework.Extensions.IEnumerableExtensions; -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.MathUtils; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Input; - -namespace osu.Framework.Graphics.UserInterface -{ - public class Menu : CompositeDrawable, IStateful - { - /// - /// Invoked when this 's changes. - /// - public event Action StateChanged; - - /// - /// Gets or sets the delay before opening sub-s when menu items are hovered. - /// - protected double HoverOpenDelay = 100; - - /// - /// Whether this menu is always displayed in an open state (ie. a menu bar). - /// Clicks are required to activate . - /// - protected readonly bool TopLevelMenu; - - /// - /// The that contains the content of this . - /// - protected readonly ScrollContainer> ContentContainer; - - /// - /// The that contains the items of this . - /// - protected readonly FillFlowContainer ItemsContainer; - - /// - /// The container that provides the masking effects for this . - /// - protected readonly Container MaskingContainer; - - /// - /// Gets the item representations contained by this . - /// - protected IReadOnlyList Children => ItemsContainer; - - protected readonly Direction Direction; - - private Menu parentMenu; - private Menu submenu; - - private readonly Box background; - - private Cached sizeCache = new Cached(); - - private readonly Container submenuContainer; - - /// - /// Constructs a menu. - /// - /// The direction of layout for this menu. - /// Whether the resultant menu is always displayed in an open state (ie. a menu bar). - public Menu(Direction direction, bool topLevelMenu = false) - { - Direction = direction; - TopLevelMenu = topLevelMenu; - - if (topLevelMenu) - state = MenuState.Open; - - InternalChildren = new Drawable[] - { - MaskingContainer = new Container - { - Name = "Our contents", - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black - }, - ContentContainer = new ScrollContainer>(direction) - { - RelativeSizeAxes = Axes.Both, - Masking = false, - Child = ItemsContainer = new FillFlowContainer { Direction = direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical } - } - } - }, - submenuContainer = new Container - { - Name = "Sub menu container", - AutoSizeAxes = Axes.Both - } - }; - - switch (direction) - { - case Direction.Horizontal: - ItemsContainer.AutoSizeAxes = Axes.X; - break; - case Direction.Vertical: - ItemsContainer.AutoSizeAxes = Axes.Y; - break; - } - - // The menu will provide a valid size for the items container based on our own size - ItemsContainer.RelativeSizeAxes = Axes.Both & ~ItemsContainer.AutoSizeAxes; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateState(); - } - - /// - /// Gets or sets the s contained within this . - /// - public IReadOnlyList Items - { - get { return ItemsContainer.Select(r => r.Item).ToList(); } - set - { - Clear(); - value?.ForEach(Add); - } - } - - /// - /// Gets or sets the background colour of this . - /// - public Color4 BackgroundColour - { - get { return background.Colour; } - set { background.Colour = value; } - } - - /// - /// Gets or sets whether the scroll bar of this should be visible. - /// - public bool ScrollbarVisible - { - get { return ContentContainer.ScrollbarVisible; } - set { ContentContainer.ScrollbarVisible = value; } - } - - private float maxWidth = float.MaxValue; - /// - /// Gets or sets the maximum allowable width by this . - /// - public float MaxWidth - { - get { return maxWidth; } - set - { - if (Precision.AlmostEquals(maxWidth, value)) - return; - maxWidth = value; - - sizeCache.Invalidate(); - } - } - - private float maxHeight = float.PositiveInfinity; - /// - /// Gets or sets the maximum allowable height by this . - /// - public float MaxHeight - { - get { return maxHeight; } - set - { - if (Precision.AlmostEquals(maxHeight, value)) - return; - maxHeight = value; - - sizeCache.Invalidate(); - } - } - - private MenuState state = MenuState.Closed; - /// - /// Gets or sets the current state of this . - /// - public virtual MenuState State - { - get { return state; } - set - { - if (TopLevelMenu) - { - submenu?.Close(); - return; - } - - if (state == value) - return; - state = value; - - updateState(); - StateChanged?.Invoke(State); - } - } - - private void updateState() - { - if (!IsLoaded) - return; - - submenu?.Close(); - - switch (State) - { - case MenuState.Closed: - AnimateClose(); - break; - case MenuState.Open: - AnimateOpen(); - if (!TopLevelMenu) - // We may not be present at this point, so must run on the next frame. - Schedule(delegate - { - if (State == MenuState.Open) GetContainingInputManager().ChangeFocus(this); - }); - break; - } - - sizeCache.Invalidate(); - } - - /// - /// Adds a to this . - /// - /// The to add. - public virtual void Add(MenuItem item) - { - var drawableItem = CreateDrawableMenuItem(item); - drawableItem.Clicked = menuItemClicked; - drawableItem.Hovered = menuItemHovered; - drawableItem.StateChanged += s => itemStateChanged(drawableItem, s); - - drawableItem.SetFlowDirection(Direction); - - ItemsContainer.Add(drawableItem); - } - - private void itemStateChanged(DrawableMenuItem item, MenuItemState state) - { - if (state != MenuItemState.Selected) return; - - if (item != selectedItem && selectedItem != null) - selectedItem.State = MenuItemState.NotSelected; - selectedItem = item; - } - - /// - /// Removes a from this . - /// - /// The to remove. - /// Whether was successfully removed. - public bool Remove(MenuItem item) - { - bool result = ItemsContainer.RemoveAll(d => d.Item == item) > 0; - sizeCache.Invalidate(); - - return result; - } - - /// - /// Clears all s in this . - /// - public void Clear() - { - ItemsContainer.Clear(); - updateState(); - } - - /// - /// Opens this . - /// - public void Open() => State = MenuState.Open; - - /// - /// Closes this . - /// - public void Close() => State = MenuState.Closed; - - /// - /// Toggles the state of this . - /// - public void Toggle() => State = State == MenuState.Closed ? MenuState.Open : MenuState.Closed; - - /// - /// Animates the opening of this . - /// - protected virtual void AnimateOpen() => Show(); - - /// - /// Animates the closing of this . - /// - protected virtual void AnimateClose() => Hide(); - - public override void InvalidateFromChild(Invalidation invalidation) - { - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - sizeCache.Invalidate(); - base.InvalidateFromChild(invalidation); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!sizeCache.IsValid) - { - // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute - // that size ourselves, based on the content size of our children, to give them a valid relative size - - float width = 0; - float height = 0; - - foreach (var item in Children) - { - width = Math.Max(width, item.ContentDrawWidth); - height = Math.Max(height, item.ContentDrawHeight); - } - - // When scrolling in one direction, ItemsContainer is auto-sized in that direction and relative-sized in the other - // In the case of the auto-sized direction, we want to use its size. In the case of the relative-sized direction, we want - // to use the (above) computed size. - width = Direction == Direction.Horizontal ? ItemsContainer.Width : width; - height = Direction == Direction.Vertical ? ItemsContainer.Height : height; - - width = Math.Min(MaxWidth, width); - height = Math.Min(MaxHeight, height); - - // Regardless of the above result, if we are relative-sizing, just use the stored width/height - width = (RelativeSizeAxes & Axes.X) > 0 ? Width : width; - height = (RelativeSizeAxes & Axes.Y) > 0 ? Height : height; - - if (State == MenuState.Closed && Direction == Direction.Horizontal) - width = 0; - if (State == MenuState.Closed && Direction == Direction.Vertical) - height = 0; - - UpdateSize(new Vector2(width, height)); - - sizeCache.Validate(); - } - } - - /// - /// Resizes this . - /// - /// The new size. - protected virtual void UpdateSize(Vector2 newSize) => Size = newSize; - - #region Hover/Focus logic - private void menuItemClicked(DrawableMenuItem item) - { - // We only want to close the sub-menu if we're not a sub menu - if we are a sub menu - // then clicks should instead cause the sub menus to instantly show up - if (TopLevelMenu && submenu?.State == MenuState.Open) - { - submenu.Close(); - return; - } - - // Check if there is a sub menu to display - if (item.Item.Items?.Count == 0) - { - // This item must have attempted to invoke an action - close all menus - closeAll(); - return; - } - - openDelegate?.Cancel(); - - openSubmenuFor(item); - } - - private DrawableMenuItem selectedItem; - - /// - /// The item which triggered opening us as a submenu. - /// - private MenuItem triggeringItem; - - private void openSubmenuFor(DrawableMenuItem item) - { - item.State = MenuItemState.Selected; - - if (submenu == null) - { - submenuContainer.Add(submenu = CreateSubMenu()); - submenu.parentMenu = this; - submenu.StateChanged += submenuStateChanged; - } - - submenu.triggeringItem = item.Item; - - submenu.Items = item.Item.Items; - submenu.Position = item.ToSpaceOfOtherDrawable(new Vector2( - Direction == Direction.Vertical ? item.DrawWidth : 0, - Direction == Direction.Horizontal ? item.DrawHeight : 0), this); - - if (item.Item.Items.Count > 0) - { - if (submenu.State == MenuState.Open) - Schedule(delegate { GetContainingInputManager().ChangeFocus(submenu); }); - else - submenu.Open(); - } - else - submenu.Close(); - } - - private void submenuStateChanged(MenuState state) - { - switch (state) - { - case MenuState.Closed: - selectedItem.State = MenuItemState.NotSelected; - break; - case MenuState.Open: - selectedItem.State = MenuItemState.Selected; - break; - } - } - - private ScheduledDelegate openDelegate; - private void menuItemHovered(DrawableMenuItem item) - { - // If we're not a sub-menu, then hover shouldn't display a sub-menu unless an item is clicked - if (TopLevelMenu && submenu?.State != MenuState.Open) - return; - - openDelegate?.Cancel(); - - if (TopLevelMenu || HoverOpenDelay == 0) - openSubmenuFor(item); - else - { - openDelegate = Scheduler.AddDelayed(() => - { - if (item.IsHovered) - openSubmenuFor(item); - }, HoverOpenDelay); - } - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Key == Key.Escape && !TopLevelMenu) - { - Close(); - return true; - } - - return base.OnKeyDown(state, args); - } - - protected override bool OnClick(InputState state) => true; - protected override bool OnHover(InputState state) => true; - - public override bool AcceptsFocus => !TopLevelMenu; - - public override bool RequestsFocus => !TopLevelMenu && State == MenuState.Open; - - protected override void OnFocusLost(InputState state) - { - // Case where a sub-menu was opened the focus will be transferred to that sub-menu while this menu will receive OnFocusLost - if (submenu?.State == MenuState.Open) - return; - - if (!TopLevelMenu) - // At this point we should have lost focus due to clicks outside the menu structure - closeAll(); - } - - /// - /// Closes all open s. - /// - private void closeAll() - { - Close(); - parentMenu?.closeFromChild(triggeringItem); - } - - private void closeFromChild(MenuItem source) - { - if (IsHovered || (parentMenu?.IsHovered ?? false)) return; - - if (triggeringItem?.Items?.Contains(source) ?? false) - { - Close(); - parentMenu?.closeFromChild(triggeringItem); - } - } - - #endregion - - /// - /// Creates a sub-menu for of s added to this . - /// - /// - protected virtual Menu CreateSubMenu() => new Menu(Direction.Vertical) - { - Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight - }; - - /// - /// Creates the visual representation for a . - /// - /// The that is to be visualised. - /// The visual representation. - protected virtual DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableMenuItem(item); - - #region DrawableMenuItem - // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 - public class DrawableMenuItem : CompositeDrawable, IStateful - { - /// - /// Invoked when this 's changes. - /// - public event Action StateChanged; - - /// - /// Invoked when this is clicked. This occurs regardless of whether or not was - /// invoked or not, or whether contains any sub-s. - /// - internal Action Clicked; - - /// - /// Invoked when this is hovered. This runs one update frame behind the actual hover event. - /// - internal Action Hovered; - - /// - /// The which this represents. - /// - public readonly MenuItem Item; - - /// - /// The content of this , created through . - /// - protected readonly Drawable Content; - - /// - /// The background of this . - /// - protected readonly Drawable Background; - - /// - /// The foreground of this . This contains the content of this . - /// - protected readonly Container Foreground; - - public DrawableMenuItem(MenuItem item) - { - Item = item; - - InternalChildren = new[] - { - Background = CreateBackground(), - Foreground = new Container - { - AutoSizeAxes = Axes.Both, - Child = Content = CreateContent() - }, - }; - - var textContent = Content as IHasText; - if (textContent != null) - { - textContent.Text = item.Text; - Item.Text.ValueChanged += newText => textContent.Text = newText; - } - } - - /// - /// Sets various properties of this that depend on the direction in which - /// s flow inside the containing (e.g. sizing axes). - /// - /// The direction in which s will be flowed. - public virtual void SetFlowDirection(Direction direction) - { - RelativeSizeAxes = direction == Direction.Horizontal ? Axes.Y : Axes.X; - AutoSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y; - } - - private Color4 backgroundColour = Color4.DarkSlateGray; - /// - /// Gets or sets the default background colour. - /// - public Color4 BackgroundColour - { - get { return backgroundColour; } - set - { - backgroundColour = value; - UpdateBackgroundColour(); - } - } - - private Color4 foregroundColour = Color4.White; - /// - /// Gets or sets the default foreground colour. - /// - public Color4 ForegroundColour - { - get { return foregroundColour; } - set - { - foregroundColour = value; - UpdateForegroundColour(); - } - } - - private Color4 backgroundColourHover = Color4.DarkGray; - /// - /// Gets or sets the background colour when this is hovered. - /// - public Color4 BackgroundColourHover - { - get { return backgroundColourHover; } - set - { - backgroundColourHover = value; - UpdateBackgroundColour(); - } - } - - private Color4 foregroundColourHover = Color4.White; - /// - /// Gets or sets the foreground colour when this is hovered. - /// - public Color4 ForegroundColourHover - { - get { return foregroundColourHover; } - set - { - foregroundColourHover = value; - UpdateForegroundColour(); - } - } - - private MenuItemState state; - public MenuItemState State - { - get { return state; } - set - { - state = value; - - UpdateForegroundColour(); - UpdateBackgroundColour(); - - StateChanged?.Invoke(state); - } - } - - /// - /// The draw width of the text of this . - /// - public float ContentDrawWidth => Content.DrawWidth; - - /// - /// The draw width of the text of this . - /// - public float ContentDrawHeight => Content.DrawHeight; - - /// - /// Called after the is modified or the hover state changes. - /// - protected virtual void UpdateBackgroundColour() - { - Background.FadeColour(IsHovered ? BackgroundColourHover : BackgroundColour); - } - - /// - /// Called after the is modified or the hover state changes. - /// - protected virtual void UpdateForegroundColour() - { - Foreground.FadeColour(IsHovered ? ForegroundColourHover : ForegroundColour); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Background.Colour = BackgroundColour; - Foreground.Colour = ForegroundColour; - } - - protected override bool OnHover(InputState state) - { - UpdateBackgroundColour(); - UpdateForegroundColour(); - - Schedule(() => - { - if (IsHovered) - Hovered?.Invoke(this); - }); - - return false; - } - - protected override void OnHoverLost(InputState state) - { - UpdateBackgroundColour(); - UpdateForegroundColour(); - base.OnHoverLost(state); - } - - private bool hasSubmenu => Item.Items?.Count > 0; - - protected override bool OnClick(InputState state) - { - if (Item.Action.Disabled) - return true; - - if (!hasSubmenu) - Item.Action.Value?.Invoke(); - - Clicked?.Invoke(this); - - return true; - } - - /// - /// Creates the background of this . - /// - protected virtual Drawable CreateBackground() => new Box { RelativeSizeAxes = Axes.Both }; - - /// - /// Creates the content which will be displayed in this . - /// If the returned implements , the text will be automatically - /// updated when the is updated. - /// - protected virtual Drawable CreateContent() => new SpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding(5), - TextSize = 17, - }; - } - #endregion - } - - public enum MenuState - { - Closed, - Open - } - - public enum MenuItemState - { - NotSelected, - Selected - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Caching; +using osu.Framework.Extensions.IEnumerableExtensions; +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.MathUtils; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Input; + +namespace osu.Framework.Graphics.UserInterface +{ + public class Menu : CompositeDrawable, IStateful + { + /// + /// Invoked when this 's changes. + /// + public event Action StateChanged; + + /// + /// Gets or sets the delay before opening sub-s when menu items are hovered. + /// + protected double HoverOpenDelay = 100; + + /// + /// Whether this menu is always displayed in an open state (ie. a menu bar). + /// Clicks are required to activate . + /// + protected readonly bool TopLevelMenu; + + /// + /// The that contains the content of this . + /// + protected readonly ScrollContainer> ContentContainer; + + /// + /// The that contains the items of this . + /// + protected readonly FillFlowContainer ItemsContainer; + + /// + /// The container that provides the masking effects for this . + /// + protected readonly Container MaskingContainer; + + /// + /// Gets the item representations contained by this . + /// + protected IReadOnlyList Children => ItemsContainer; + + protected readonly Direction Direction; + + private Menu parentMenu; + private Menu submenu; + + private readonly Box background; + + private Cached sizeCache = new Cached(); + + private readonly Container submenuContainer; + + /// + /// Constructs a menu. + /// + /// The direction of layout for this menu. + /// Whether the resultant menu is always displayed in an open state (ie. a menu bar). + public Menu(Direction direction, bool topLevelMenu = false) + { + Direction = direction; + TopLevelMenu = topLevelMenu; + + if (topLevelMenu) + state = MenuState.Open; + + InternalChildren = new Drawable[] + { + MaskingContainer = new Container + { + Name = "Our contents", + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + ContentContainer = new ScrollContainer>(direction) + { + RelativeSizeAxes = Axes.Both, + Masking = false, + Child = ItemsContainer = new FillFlowContainer { Direction = direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical } + } + } + }, + submenuContainer = new Container + { + Name = "Sub menu container", + AutoSizeAxes = Axes.Both + } + }; + + switch (direction) + { + case Direction.Horizontal: + ItemsContainer.AutoSizeAxes = Axes.X; + break; + case Direction.Vertical: + ItemsContainer.AutoSizeAxes = Axes.Y; + break; + } + + // The menu will provide a valid size for the items container based on our own size + ItemsContainer.RelativeSizeAxes = Axes.Both & ~ItemsContainer.AutoSizeAxes; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + /// + /// Gets or sets the s contained within this . + /// + public IReadOnlyList Items + { + get { return ItemsContainer.Select(r => r.Item).ToList(); } + set + { + Clear(); + value?.ForEach(Add); + } + } + + /// + /// Gets or sets the background colour of this . + /// + public Color4 BackgroundColour + { + get { return background.Colour; } + set { background.Colour = value; } + } + + /// + /// Gets or sets whether the scroll bar of this should be visible. + /// + public bool ScrollbarVisible + { + get { return ContentContainer.ScrollbarVisible; } + set { ContentContainer.ScrollbarVisible = value; } + } + + private float maxWidth = float.MaxValue; + /// + /// Gets or sets the maximum allowable width by this . + /// + public float MaxWidth + { + get { return maxWidth; } + set + { + if (Precision.AlmostEquals(maxWidth, value)) + return; + maxWidth = value; + + sizeCache.Invalidate(); + } + } + + private float maxHeight = float.PositiveInfinity; + /// + /// Gets or sets the maximum allowable height by this . + /// + public float MaxHeight + { + get { return maxHeight; } + set + { + if (Precision.AlmostEquals(maxHeight, value)) + return; + maxHeight = value; + + sizeCache.Invalidate(); + } + } + + private MenuState state = MenuState.Closed; + /// + /// Gets or sets the current state of this . + /// + public virtual MenuState State + { + get { return state; } + set + { + if (TopLevelMenu) + { + submenu?.Close(); + return; + } + + if (state == value) + return; + state = value; + + updateState(); + StateChanged?.Invoke(State); + } + } + + private void updateState() + { + if (!IsLoaded) + return; + + submenu?.Close(); + + switch (State) + { + case MenuState.Closed: + AnimateClose(); + break; + case MenuState.Open: + AnimateOpen(); + if (!TopLevelMenu) + // We may not be present at this point, so must run on the next frame. + Schedule(delegate + { + if (State == MenuState.Open) GetContainingInputManager().ChangeFocus(this); + }); + break; + } + + sizeCache.Invalidate(); + } + + /// + /// Adds a to this . + /// + /// The to add. + public virtual void Add(MenuItem item) + { + var drawableItem = CreateDrawableMenuItem(item); + drawableItem.Clicked = menuItemClicked; + drawableItem.Hovered = menuItemHovered; + drawableItem.StateChanged += s => itemStateChanged(drawableItem, s); + + drawableItem.SetFlowDirection(Direction); + + ItemsContainer.Add(drawableItem); + } + + private void itemStateChanged(DrawableMenuItem item, MenuItemState state) + { + if (state != MenuItemState.Selected) return; + + if (item != selectedItem && selectedItem != null) + selectedItem.State = MenuItemState.NotSelected; + selectedItem = item; + } + + /// + /// Removes a from this . + /// + /// The to remove. + /// Whether was successfully removed. + public bool Remove(MenuItem item) + { + bool result = ItemsContainer.RemoveAll(d => d.Item == item) > 0; + sizeCache.Invalidate(); + + return result; + } + + /// + /// Clears all s in this . + /// + public void Clear() + { + ItemsContainer.Clear(); + updateState(); + } + + /// + /// Opens this . + /// + public void Open() => State = MenuState.Open; + + /// + /// Closes this . + /// + public void Close() => State = MenuState.Closed; + + /// + /// Toggles the state of this . + /// + public void Toggle() => State = State == MenuState.Closed ? MenuState.Open : MenuState.Closed; + + /// + /// Animates the opening of this . + /// + protected virtual void AnimateOpen() => Show(); + + /// + /// Animates the closing of this . + /// + protected virtual void AnimateClose() => Hide(); + + public override void InvalidateFromChild(Invalidation invalidation) + { + if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) + sizeCache.Invalidate(); + base.InvalidateFromChild(invalidation); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!sizeCache.IsValid) + { + // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute + // that size ourselves, based on the content size of our children, to give them a valid relative size + + float width = 0; + float height = 0; + + foreach (var item in Children) + { + width = Math.Max(width, item.ContentDrawWidth); + height = Math.Max(height, item.ContentDrawHeight); + } + + // When scrolling in one direction, ItemsContainer is auto-sized in that direction and relative-sized in the other + // In the case of the auto-sized direction, we want to use its size. In the case of the relative-sized direction, we want + // to use the (above) computed size. + width = Direction == Direction.Horizontal ? ItemsContainer.Width : width; + height = Direction == Direction.Vertical ? ItemsContainer.Height : height; + + width = Math.Min(MaxWidth, width); + height = Math.Min(MaxHeight, height); + + // Regardless of the above result, if we are relative-sizing, just use the stored width/height + width = (RelativeSizeAxes & Axes.X) > 0 ? Width : width; + height = (RelativeSizeAxes & Axes.Y) > 0 ? Height : height; + + if (State == MenuState.Closed && Direction == Direction.Horizontal) + width = 0; + if (State == MenuState.Closed && Direction == Direction.Vertical) + height = 0; + + UpdateSize(new Vector2(width, height)); + + sizeCache.Validate(); + } + } + + /// + /// Resizes this . + /// + /// The new size. + protected virtual void UpdateSize(Vector2 newSize) => Size = newSize; + + #region Hover/Focus logic + private void menuItemClicked(DrawableMenuItem item) + { + // We only want to close the sub-menu if we're not a sub menu - if we are a sub menu + // then clicks should instead cause the sub menus to instantly show up + if (TopLevelMenu && submenu?.State == MenuState.Open) + { + submenu.Close(); + return; + } + + // Check if there is a sub menu to display + if (item.Item.Items?.Count == 0) + { + // This item must have attempted to invoke an action - close all menus + closeAll(); + return; + } + + openDelegate?.Cancel(); + + openSubmenuFor(item); + } + + private DrawableMenuItem selectedItem; + + /// + /// The item which triggered opening us as a submenu. + /// + private MenuItem triggeringItem; + + private void openSubmenuFor(DrawableMenuItem item) + { + item.State = MenuItemState.Selected; + + if (submenu == null) + { + submenuContainer.Add(submenu = CreateSubMenu()); + submenu.parentMenu = this; + submenu.StateChanged += submenuStateChanged; + } + + submenu.triggeringItem = item.Item; + + submenu.Items = item.Item.Items; + submenu.Position = item.ToSpaceOfOtherDrawable(new Vector2( + Direction == Direction.Vertical ? item.DrawWidth : 0, + Direction == Direction.Horizontal ? item.DrawHeight : 0), this); + + if (item.Item.Items.Count > 0) + { + if (submenu.State == MenuState.Open) + Schedule(delegate { GetContainingInputManager().ChangeFocus(submenu); }); + else + submenu.Open(); + } + else + submenu.Close(); + } + + private void submenuStateChanged(MenuState state) + { + switch (state) + { + case MenuState.Closed: + selectedItem.State = MenuItemState.NotSelected; + break; + case MenuState.Open: + selectedItem.State = MenuItemState.Selected; + break; + } + } + + private ScheduledDelegate openDelegate; + private void menuItemHovered(DrawableMenuItem item) + { + // If we're not a sub-menu, then hover shouldn't display a sub-menu unless an item is clicked + if (TopLevelMenu && submenu?.State != MenuState.Open) + return; + + openDelegate?.Cancel(); + + if (TopLevelMenu || HoverOpenDelay == 0) + openSubmenuFor(item); + else + { + openDelegate = Scheduler.AddDelayed(() => + { + if (item.IsHovered) + openSubmenuFor(item); + }, HoverOpenDelay); + } + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Key == Key.Escape && !TopLevelMenu) + { + Close(); + return true; + } + + return base.OnKeyDown(state, args); + } + + protected override bool OnClick(InputState state) => true; + protected override bool OnHover(InputState state) => true; + + public override bool AcceptsFocus => !TopLevelMenu; + + public override bool RequestsFocus => !TopLevelMenu && State == MenuState.Open; + + protected override void OnFocusLost(InputState state) + { + // Case where a sub-menu was opened the focus will be transferred to that sub-menu while this menu will receive OnFocusLost + if (submenu?.State == MenuState.Open) + return; + + if (!TopLevelMenu) + // At this point we should have lost focus due to clicks outside the menu structure + closeAll(); + } + + /// + /// Closes all open s. + /// + private void closeAll() + { + Close(); + parentMenu?.closeFromChild(triggeringItem); + } + + private void closeFromChild(MenuItem source) + { + if (IsHovered || (parentMenu?.IsHovered ?? false)) return; + + if (triggeringItem?.Items?.Contains(source) ?? false) + { + Close(); + parentMenu?.closeFromChild(triggeringItem); + } + } + + #endregion + + /// + /// Creates a sub-menu for of s added to this . + /// + /// + protected virtual Menu CreateSubMenu() => new Menu(Direction.Vertical) + { + Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight + }; + + /// + /// Creates the visual representation for a . + /// + /// The that is to be visualised. + /// The visual representation. + protected virtual DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableMenuItem(item); + + #region DrawableMenuItem + // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 + public class DrawableMenuItem : CompositeDrawable, IStateful + { + /// + /// Invoked when this 's changes. + /// + public event Action StateChanged; + + /// + /// Invoked when this is clicked. This occurs regardless of whether or not was + /// invoked or not, or whether contains any sub-s. + /// + internal Action Clicked; + + /// + /// Invoked when this is hovered. This runs one update frame behind the actual hover event. + /// + internal Action Hovered; + + /// + /// The which this represents. + /// + public readonly MenuItem Item; + + /// + /// The content of this , created through . + /// + protected readonly Drawable Content; + + /// + /// The background of this . + /// + protected readonly Drawable Background; + + /// + /// The foreground of this . This contains the content of this . + /// + protected readonly Container Foreground; + + public DrawableMenuItem(MenuItem item) + { + Item = item; + + InternalChildren = new[] + { + Background = CreateBackground(), + Foreground = new Container + { + AutoSizeAxes = Axes.Both, + Child = Content = CreateContent() + }, + }; + + var textContent = Content as IHasText; + if (textContent != null) + { + textContent.Text = item.Text; + Item.Text.ValueChanged += newText => textContent.Text = newText; + } + } + + /// + /// Sets various properties of this that depend on the direction in which + /// s flow inside the containing (e.g. sizing axes). + /// + /// The direction in which s will be flowed. + public virtual void SetFlowDirection(Direction direction) + { + RelativeSizeAxes = direction == Direction.Horizontal ? Axes.Y : Axes.X; + AutoSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y; + } + + private Color4 backgroundColour = Color4.DarkSlateGray; + /// + /// Gets or sets the default background colour. + /// + public Color4 BackgroundColour + { + get { return backgroundColour; } + set + { + backgroundColour = value; + UpdateBackgroundColour(); + } + } + + private Color4 foregroundColour = Color4.White; + /// + /// Gets or sets the default foreground colour. + /// + public Color4 ForegroundColour + { + get { return foregroundColour; } + set + { + foregroundColour = value; + UpdateForegroundColour(); + } + } + + private Color4 backgroundColourHover = Color4.DarkGray; + /// + /// Gets or sets the background colour when this is hovered. + /// + public Color4 BackgroundColourHover + { + get { return backgroundColourHover; } + set + { + backgroundColourHover = value; + UpdateBackgroundColour(); + } + } + + private Color4 foregroundColourHover = Color4.White; + /// + /// Gets or sets the foreground colour when this is hovered. + /// + public Color4 ForegroundColourHover + { + get { return foregroundColourHover; } + set + { + foregroundColourHover = value; + UpdateForegroundColour(); + } + } + + private MenuItemState state; + public MenuItemState State + { + get { return state; } + set + { + state = value; + + UpdateForegroundColour(); + UpdateBackgroundColour(); + + StateChanged?.Invoke(state); + } + } + + /// + /// The draw width of the text of this . + /// + public float ContentDrawWidth => Content.DrawWidth; + + /// + /// The draw width of the text of this . + /// + public float ContentDrawHeight => Content.DrawHeight; + + /// + /// Called after the is modified or the hover state changes. + /// + protected virtual void UpdateBackgroundColour() + { + Background.FadeColour(IsHovered ? BackgroundColourHover : BackgroundColour); + } + + /// + /// Called after the is modified or the hover state changes. + /// + protected virtual void UpdateForegroundColour() + { + Foreground.FadeColour(IsHovered ? ForegroundColourHover : ForegroundColour); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Background.Colour = BackgroundColour; + Foreground.Colour = ForegroundColour; + } + + protected override bool OnHover(InputState state) + { + UpdateBackgroundColour(); + UpdateForegroundColour(); + + Schedule(() => + { + if (IsHovered) + Hovered?.Invoke(this); + }); + + return false; + } + + protected override void OnHoverLost(InputState state) + { + UpdateBackgroundColour(); + UpdateForegroundColour(); + base.OnHoverLost(state); + } + + private bool hasSubmenu => Item.Items?.Count > 0; + + protected override bool OnClick(InputState state) + { + if (Item.Action.Disabled) + return true; + + if (!hasSubmenu) + Item.Action.Value?.Invoke(); + + Clicked?.Invoke(this); + + return true; + } + + /// + /// Creates the background of this . + /// + protected virtual Drawable CreateBackground() => new Box { RelativeSizeAxes = Axes.Both }; + + /// + /// Creates the content which will be displayed in this . + /// If the returned implements , the text will be automatically + /// updated when the is updated. + /// + protected virtual Drawable CreateContent() => new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding(5), + TextSize = 17, + }; + } + #endregion + } + + public enum MenuState + { + Closed, + Open + } + + public enum MenuItemState + { + NotSelected, + Selected + } +} diff --git a/osu.Framework/Graphics/UserInterface/MenuItem.cs b/osu.Framework/Graphics/UserInterface/MenuItem.cs index ac88ac3e0..67e19235b 100644 --- a/osu.Framework/Graphics/UserInterface/MenuItem.cs +++ b/osu.Framework/Graphics/UserInterface/MenuItem.cs @@ -1,47 +1,47 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Configuration; - -namespace osu.Framework.Graphics.UserInterface -{ - public class MenuItem - { - /// - /// The text which this displays. - /// - public readonly Bindable Text = new Bindable(); - - /// - /// The that is performed when this is clicked. - /// - public readonly Bindable Action = new Bindable(); - - /// - /// A list of items which are to be displayed in a sub-menu originating from this . - /// - public IReadOnlyList Items = Array.Empty(); - - /// - /// Creates a new . - /// - /// The text to display. - public MenuItem(string text) - { - Text.Value = text; - } - - /// - /// Creates a new . - /// - /// The text to display. - /// The to perform when clicked. - public MenuItem(string text, Action action) - : this(text) - { - Action.Value = action; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Configuration; + +namespace osu.Framework.Graphics.UserInterface +{ + public class MenuItem + { + /// + /// The text which this displays. + /// + public readonly Bindable Text = new Bindable(); + + /// + /// The that is performed when this is clicked. + /// + public readonly Bindable Action = new Bindable(); + + /// + /// A list of items which are to be displayed in a sub-menu originating from this . + /// + public IReadOnlyList Items = Array.Empty(); + + /// + /// Creates a new . + /// + /// The text to display. + public MenuItem(string text) + { + Text.Value = text; + } + + /// + /// Creates a new . + /// + /// The text to display. + /// The to perform when clicked. + public MenuItem(string text, Action action) + : this(text) + { + Action.Value = action; + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs b/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs index 8198a831d..ba91d3dc8 100644 --- a/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs +++ b/osu.Framework/Graphics/UserInterface/PasswordTextBox.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Graphics.UserInterface -{ - public class PasswordTextBox : TextBox - { - protected virtual char MaskCharacter => '*'; - - public override bool AllowClipboardExport => false; - - protected override Drawable AddCharacterToFlow(char c) => base.AddCharacterToFlow(MaskCharacter); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Graphics.UserInterface +{ + public class PasswordTextBox : TextBox + { + protected virtual char MaskCharacter => '*'; + + public override bool AllowClipboardExport => false; + + protected override Drawable AddCharacterToFlow(char c) => base.AddCharacterToFlow(MaskCharacter); + } +} diff --git a/osu.Framework/Graphics/UserInterface/SliderBar.cs b/osu.Framework/Graphics/UserInterface/SliderBar.cs index 8a6a89fbd..4f1530fcf 100644 --- a/osu.Framework/Graphics/UserInterface/SliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/SliderBar.cs @@ -1,150 +1,150 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using OpenTK.Input; -using OpenTK; -using System.Diagnostics; - -namespace osu.Framework.Graphics.UserInterface -{ - public abstract class SliderBar : Container, IHasCurrentValue - where T : struct, IComparable, IConvertible - { - /// - /// Range padding reduces the range of movement a slider bar is allowed to have - /// while still receiving input in the padded region. This behavior is necessary - /// for finite-sized nubs and can not be achieved (currently) by existing - /// scene graph padding / margin functionality. - /// - public float RangePadding; - - public float UsableWidth => DrawWidth - 2 * RangePadding; - - /// - /// A custom step value for each key press which actuates a change on this control. - /// - public float KeyboardStep; - - protected readonly BindableNumber CurrentNumber; - - public Bindable Current => CurrentNumber; - - protected SliderBar() - { - if (typeof(T) == typeof(int)) - CurrentNumber = new BindableInt() as BindableNumber; - else if (typeof(T) == typeof(long)) - CurrentNumber = new BindableLong() as BindableNumber; - else if (typeof(T) == typeof(double)) - CurrentNumber = new BindableDouble() as BindableNumber; - else if (typeof(T) == typeof(float)) - CurrentNumber = new BindableFloat() as BindableNumber; - - if (CurrentNumber == null) - throw new NotSupportedException($"We don't support the generic type of {nameof(BindableNumber)}."); - - CurrentNumber.ValueChanged += v => UpdateValue(NormalizedValue); - } - - protected float NormalizedValue - { - get - { - if (Current == null) - return 0; - - if (!CurrentNumber.HasDefinedRange) - throw new InvalidOperationException($"A {nameof(SliderBar)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber.MinValue)}" - + $" and {nameof(BindableNumber.MaxValue)} to produce a valid {nameof(NormalizedValue)}."); - - var min = Convert.ToSingle(CurrentNumber.MinValue); - var max = Convert.ToSingle(CurrentNumber.MaxValue); - - if (max - min == 0) - return 1; - - var val = Convert.ToSingle(CurrentNumber.Value); - return (val - min) / (max - min); - } - } - - /// - /// Triggered when the value has changed. Used to update the displayed value. - /// - /// The normalized value. - protected abstract void UpdateValue(float value); - - protected override void LoadComplete() - { - base.LoadComplete(); - UpdateValue(NormalizedValue); - } - - protected override bool OnClick(InputState state) - { - handleMouseInput(state); - return true; - } - - protected override bool OnDrag(InputState state) - { - handleMouseInput(state); - return true; - } - - protected override bool OnDragStart(InputState state) - { - Trace.Assert(state.Mouse.PositionMouseDown.HasValue, - $@"Can not start a {nameof(SliderBar)} drag without knowing the mouse down position."); - - // ReSharper disable once PossibleInvalidOperationException - Vector2 posDiff = state.Mouse.PositionMouseDown.Value - state.Mouse.Position; - - return Math.Abs(posDiff.X) > Math.Abs(posDiff.Y); - } - - protected override bool OnDragEnd(InputState state) => true; - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (!IsHovered || CurrentNumber.Disabled) - return false; - - var step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(CurrentNumber.MaxValue) - Convert.ToSingle(CurrentNumber.MinValue)) / 20; - if (CurrentNumber.IsInteger) step = (float)Math.Ceiling(step); - - switch (args.Key) - { - case Key.Right: - CurrentNumber.Add(step); - OnUserChange(); - return true; - case Key.Left: - CurrentNumber.Add(-step); - OnUserChange(); - return true; - default: - return false; - } - } - - private void handleMouseInput(InputState state) - { - var xPosition = ToLocalSpace(state?.Mouse.NativeState.Position ?? Vector2.Zero).X - RangePadding; - - if (!CurrentNumber.Disabled) - CurrentNumber.SetProportional(xPosition / UsableWidth, state != null && state.Keyboard.ShiftPressed ? KeyboardStep : 0); - - OnUserChange(); - } - - /// - /// Triggered when the value is changed based on end-user input to this control. - /// - protected virtual void OnUserChange() { } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using OpenTK.Input; +using OpenTK; +using System.Diagnostics; + +namespace osu.Framework.Graphics.UserInterface +{ + public abstract class SliderBar : Container, IHasCurrentValue + where T : struct, IComparable, IConvertible + { + /// + /// Range padding reduces the range of movement a slider bar is allowed to have + /// while still receiving input in the padded region. This behavior is necessary + /// for finite-sized nubs and can not be achieved (currently) by existing + /// scene graph padding / margin functionality. + /// + public float RangePadding; + + public float UsableWidth => DrawWidth - 2 * RangePadding; + + /// + /// A custom step value for each key press which actuates a change on this control. + /// + public float KeyboardStep; + + protected readonly BindableNumber CurrentNumber; + + public Bindable Current => CurrentNumber; + + protected SliderBar() + { + if (typeof(T) == typeof(int)) + CurrentNumber = new BindableInt() as BindableNumber; + else if (typeof(T) == typeof(long)) + CurrentNumber = new BindableLong() as BindableNumber; + else if (typeof(T) == typeof(double)) + CurrentNumber = new BindableDouble() as BindableNumber; + else if (typeof(T) == typeof(float)) + CurrentNumber = new BindableFloat() as BindableNumber; + + if (CurrentNumber == null) + throw new NotSupportedException($"We don't support the generic type of {nameof(BindableNumber)}."); + + CurrentNumber.ValueChanged += v => UpdateValue(NormalizedValue); + } + + protected float NormalizedValue + { + get + { + if (Current == null) + return 0; + + if (!CurrentNumber.HasDefinedRange) + throw new InvalidOperationException($"A {nameof(SliderBar)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber.MinValue)}" + + $" and {nameof(BindableNumber.MaxValue)} to produce a valid {nameof(NormalizedValue)}."); + + var min = Convert.ToSingle(CurrentNumber.MinValue); + var max = Convert.ToSingle(CurrentNumber.MaxValue); + + if (max - min == 0) + return 1; + + var val = Convert.ToSingle(CurrentNumber.Value); + return (val - min) / (max - min); + } + } + + /// + /// Triggered when the value has changed. Used to update the displayed value. + /// + /// The normalized value. + protected abstract void UpdateValue(float value); + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateValue(NormalizedValue); + } + + protected override bool OnClick(InputState state) + { + handleMouseInput(state); + return true; + } + + protected override bool OnDrag(InputState state) + { + handleMouseInput(state); + return true; + } + + protected override bool OnDragStart(InputState state) + { + Trace.Assert(state.Mouse.PositionMouseDown.HasValue, + $@"Can not start a {nameof(SliderBar)} drag without knowing the mouse down position."); + + // ReSharper disable once PossibleInvalidOperationException + Vector2 posDiff = state.Mouse.PositionMouseDown.Value - state.Mouse.Position; + + return Math.Abs(posDiff.X) > Math.Abs(posDiff.Y); + } + + protected override bool OnDragEnd(InputState state) => true; + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!IsHovered || CurrentNumber.Disabled) + return false; + + var step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(CurrentNumber.MaxValue) - Convert.ToSingle(CurrentNumber.MinValue)) / 20; + if (CurrentNumber.IsInteger) step = (float)Math.Ceiling(step); + + switch (args.Key) + { + case Key.Right: + CurrentNumber.Add(step); + OnUserChange(); + return true; + case Key.Left: + CurrentNumber.Add(-step); + OnUserChange(); + return true; + default: + return false; + } + } + + private void handleMouseInput(InputState state) + { + var xPosition = ToLocalSpace(state?.Mouse.NativeState.Position ?? Vector2.Zero).X - RangePadding; + + if (!CurrentNumber.Disabled) + CurrentNumber.SetProportional(xPosition / UsableWidth, state != null && state.Keyboard.ShiftPressed ? KeyboardStep : 0); + + OnUserChange(); + } + + /// + /// Triggered when the value is changed based on end-user input to this control. + /// + protected virtual void OnUserChange() { } + } +} diff --git a/osu.Framework/Graphics/UserInterface/TabControl.cs b/osu.Framework/Graphics/UserInterface/TabControl.cs index 933a6220c..e5945cb9b 100644 --- a/osu.Framework/Graphics/UserInterface/TabControl.cs +++ b/osu.Framework/Graphics/UserInterface/TabControl.cs @@ -1,298 +1,298 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.Extensions; -using osu.Framework.Graphics.Containers; -using OpenTK; - -namespace osu.Framework.Graphics.UserInterface -{ - /// - /// A single-row control to display a list of selectable tabs along with an optional right-aligned dropdown - /// containing overflow items (tabs which cannot be displayed in the allocated width). Includes - /// support for pinning items, causing them to be displayed before all other items at the - /// start of the list. - /// - /// The type of item to be represented by tabs. - public abstract class TabControl : Container, IHasCurrentValue - { - public Bindable Current { get; } = new Bindable(); - - /// - /// A list of items currently in the tab control in the order they are dispalyed. - /// - public IEnumerable Items => TabContainer.TabItems.Select(t => t.Value).Concat(Dropdown.Items.Select(kvp => kvp.Value)).Distinct(); - - /// - /// When true, tabs selected from the overflow dropdown will be moved to the front of the list (after pinned items). - /// - public bool AutoSort { set; get; } - - protected Dropdown Dropdown; - - protected readonly TabFillFlowContainer TabContainer; - - protected IReadOnlyDictionary> TabMap => tabMap; - - protected TabItem SelectedTab; - - /// - /// Creates an optional overflow dropdown. - /// When implementing this dropdown make sure: - /// - It is made to be anchored to the right-hand side of its parent. - /// - The dropdown's header does *not* have a relative x axis. - /// - protected abstract Dropdown CreateDropdown(); - - /// - /// Creates a tab item. - /// - protected abstract TabItem CreateTabItem(T value); - - /// - /// Decremented each time a tab needs to be inserted at the start of the list. - /// - private int depthCounter; - - /// - /// A mapping of tabs to their items. - /// - private readonly Dictionary> tabMap; - - protected TabControl() - { - Dropdown = CreateDropdown(); - if (Dropdown != null) - { - Dropdown.RelativeSizeAxes = Axes.X; - Dropdown.Anchor = Anchor.TopRight; - Dropdown.Origin = Anchor.TopRight; - Dropdown.Current.BindTo(Current); - - Add(Dropdown); - - Trace.Assert((Dropdown.Header.Anchor & Anchor.x2) > 0, $@"The {nameof(Dropdown)} implementation should use a right-based anchor inside a TabControl."); - Trace.Assert((Dropdown.Header.RelativeSizeAxes & Axes.X) == 0, $@"The {nameof(Dropdown)} implementation's header should have a specific size."); - - // create tab items for already existing items in dropdown (if any). - tabMap = Dropdown.Items.ToDictionary(item => item.Value, item => addTab(item.Value, false)); - } - else - tabMap = new Dictionary>(); - - Add(TabContainer = CreateTabFlow()); - TabContainer.TabVisibilityChanged = updateDropdown; - TabContainer.ChildrenEnumerable = tabMap.Values; - - Current.ValueChanged += newSelection => - { - if (IsLoaded) - SelectTab(tabMap[Current]); - else - //will be handled in LoadComplete - SelectedTab = tabMap[Current]; - }; - } - - protected override void Update() - { - base.Update(); - - if (Dropdown != null) - { - Dropdown.Header.Height = DrawHeight; - TabContainer.Padding = new MarginPadding { Right = Dropdown.Header.Width }; - } - } - - // Default to first selection in list - protected override void LoadComplete() - { - if (SelectedTab != null) - SelectTab(SelectedTab); - else if (TabContainer.Children.Any()) - SelectTab(TabContainer.Children.First()); - } - - /// - /// Pin an item to the start of the list. - /// - /// The item to pin. - public void PinItem(T item) - { - if (!tabMap.TryGetValue(item, out TabItem tab)) - return; - tab.Pinned = true; - } - - /// - /// Unpin an item and return it to the start of unpinned items. - /// - /// The item to unpin. - public void UnpinItem(T item) - { - if (!tabMap.TryGetValue(item, out TabItem tab)) - return; - tab.Pinned = false; - } - - /// - /// Add a new item to the control. - /// - /// The item to add. - public void AddItem(T item) => addTab(item); - - /// - /// Removes an item from the control. - /// - /// The item to remove. - public void RemoveItem(T item) => removeTab(item); - - private TabItem addTab(T value, bool addToDropdown = true) - { - // Do not allow duplicate adding - if (tabMap.ContainsKey(value)) - throw new InvalidOperationException($"Item {value} has already been added to this {nameof(TabControl)}"); - - var tab = CreateTabItem(value); - AddTabItem(tab, addToDropdown); - - return tab; - } - - private void removeTab(T value, bool removeFromDropdown = true) - { - if (!tabMap.ContainsKey(value)) - throw new InvalidOperationException($"Item {value} doesn't exist in this {nameof(TabControl)}."); - - RemoveTabItem(tabMap[value], removeFromDropdown); - } - - /// - /// Adds an arbitrary to the control. - /// - /// The tab to add. - /// Whether the tab should be added to the Dropdown if supported by the implementation. - protected virtual void AddTabItem(TabItem tab, bool addToDropdown = true) - { - tab.PinnedChanged += performTabSort; - - tab.ActivationRequested += SelectTab; - - tabMap[tab.Value] = tab; - if (addToDropdown) - Dropdown?.AddDropdownItem((tab.Value as Enum)?.GetDescription() ?? tab.Value.ToString(), tab.Value); - TabContainer.Add(tab); - } - - /// - /// Removes a from this . - /// - /// The tab to remove. - /// Whether the tab should be removed from the Dropdown if supported by the implementation. - protected virtual void RemoveTabItem(TabItem tab, bool removeFromDropdown = true) - { - if (!tab.IsRemovable) return; - - if (tab == SelectedTab) - SelectedTab = null; - - tabMap.Remove(tab.Value); - - if (removeFromDropdown) - Dropdown?.RemoveDropdownItem(tab.Value); - - TabContainer.Remove(tab); - } - - /// - /// Callback on the change of visibility of a tab. - /// Used to update the item's status in the overflow dropdown if required. - /// - private void updateDropdown(TabItem tab, bool isVisible) - { - if (isVisible) - Dropdown?.HideItem(tab.Value); - else - Dropdown?.ShowItem(tab.Value); - } - - protected virtual void SelectTab(TabItem tab) - { - // Only reorder if not pinned and not showing - if (AutoSort && !tab.IsPresent && !tab.Pinned) - performTabSort(tab); - - // Deactivate previously selected tab - if (SelectedTab != null && SelectedTab != tab) SelectedTab.Active.Value = false; - - SelectedTab = tab; - SelectedTab.Active.Value = true; - - Current.Value = SelectedTab.Value; - } - - private void performTabSort(TabItem tab) - { - TabContainer.SetLayoutPosition(tab, getTabDepth(tab)); - - // IsPresent of TabItems is based on Y position. - // We reset it here to allow tabs to get a correct initial position. - tab.Y = 0; - } - - private float getTabDepth(TabItem tab) => tab.Pinned ? float.MinValue : --depthCounter; - - protected virtual TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.Both, - Depth = -1, - Masking = true - }; - - public class TabFillFlowContainer : FillFlowContainer> - { - /// - /// Gets called whenever the visibility of a tab in this container changes. Gets invoked with the whose visibility changed and the new visibility state (true = visible, false = hidden). - /// - public Action, bool> TabVisibilityChanged; - - /// - /// The list of tabs currently displayed by this container. - /// - public IEnumerable> TabItems => FlowingChildren.OfType>(); - - protected override IEnumerable ComputeLayoutPositions() - { - foreach (var child in Children) - child.Y = 0; - - var result = base.ComputeLayoutPositions().ToArray(); - int i = 0; - foreach (var child in FlowingChildren.OfType>()) - { - updateChildIfNeeded(child, result[i].Y == 0); - ++i; - } - return result; - } - - private readonly Dictionary, bool> tabVisibility = new Dictionary, bool>(); - - private void updateChildIfNeeded(TabItem child, bool isVisible) - { - if (!tabVisibility.ContainsKey(child) || tabVisibility[child] != isVisible) - { - TabVisibilityChanged?.Invoke(child, isVisible); - tabVisibility[child] = isVisible; - } - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Framework.Graphics.UserInterface +{ + /// + /// A single-row control to display a list of selectable tabs along with an optional right-aligned dropdown + /// containing overflow items (tabs which cannot be displayed in the allocated width). Includes + /// support for pinning items, causing them to be displayed before all other items at the + /// start of the list. + /// + /// The type of item to be represented by tabs. + public abstract class TabControl : Container, IHasCurrentValue + { + public Bindable Current { get; } = new Bindable(); + + /// + /// A list of items currently in the tab control in the order they are dispalyed. + /// + public IEnumerable Items => TabContainer.TabItems.Select(t => t.Value).Concat(Dropdown.Items.Select(kvp => kvp.Value)).Distinct(); + + /// + /// When true, tabs selected from the overflow dropdown will be moved to the front of the list (after pinned items). + /// + public bool AutoSort { set; get; } + + protected Dropdown Dropdown; + + protected readonly TabFillFlowContainer TabContainer; + + protected IReadOnlyDictionary> TabMap => tabMap; + + protected TabItem SelectedTab; + + /// + /// Creates an optional overflow dropdown. + /// When implementing this dropdown make sure: + /// - It is made to be anchored to the right-hand side of its parent. + /// - The dropdown's header does *not* have a relative x axis. + /// + protected abstract Dropdown CreateDropdown(); + + /// + /// Creates a tab item. + /// + protected abstract TabItem CreateTabItem(T value); + + /// + /// Decremented each time a tab needs to be inserted at the start of the list. + /// + private int depthCounter; + + /// + /// A mapping of tabs to their items. + /// + private readonly Dictionary> tabMap; + + protected TabControl() + { + Dropdown = CreateDropdown(); + if (Dropdown != null) + { + Dropdown.RelativeSizeAxes = Axes.X; + Dropdown.Anchor = Anchor.TopRight; + Dropdown.Origin = Anchor.TopRight; + Dropdown.Current.BindTo(Current); + + Add(Dropdown); + + Trace.Assert((Dropdown.Header.Anchor & Anchor.x2) > 0, $@"The {nameof(Dropdown)} implementation should use a right-based anchor inside a TabControl."); + Trace.Assert((Dropdown.Header.RelativeSizeAxes & Axes.X) == 0, $@"The {nameof(Dropdown)} implementation's header should have a specific size."); + + // create tab items for already existing items in dropdown (if any). + tabMap = Dropdown.Items.ToDictionary(item => item.Value, item => addTab(item.Value, false)); + } + else + tabMap = new Dictionary>(); + + Add(TabContainer = CreateTabFlow()); + TabContainer.TabVisibilityChanged = updateDropdown; + TabContainer.ChildrenEnumerable = tabMap.Values; + + Current.ValueChanged += newSelection => + { + if (IsLoaded) + SelectTab(tabMap[Current]); + else + //will be handled in LoadComplete + SelectedTab = tabMap[Current]; + }; + } + + protected override void Update() + { + base.Update(); + + if (Dropdown != null) + { + Dropdown.Header.Height = DrawHeight; + TabContainer.Padding = new MarginPadding { Right = Dropdown.Header.Width }; + } + } + + // Default to first selection in list + protected override void LoadComplete() + { + if (SelectedTab != null) + SelectTab(SelectedTab); + else if (TabContainer.Children.Any()) + SelectTab(TabContainer.Children.First()); + } + + /// + /// Pin an item to the start of the list. + /// + /// The item to pin. + public void PinItem(T item) + { + if (!tabMap.TryGetValue(item, out TabItem tab)) + return; + tab.Pinned = true; + } + + /// + /// Unpin an item and return it to the start of unpinned items. + /// + /// The item to unpin. + public void UnpinItem(T item) + { + if (!tabMap.TryGetValue(item, out TabItem tab)) + return; + tab.Pinned = false; + } + + /// + /// Add a new item to the control. + /// + /// The item to add. + public void AddItem(T item) => addTab(item); + + /// + /// Removes an item from the control. + /// + /// The item to remove. + public void RemoveItem(T item) => removeTab(item); + + private TabItem addTab(T value, bool addToDropdown = true) + { + // Do not allow duplicate adding + if (tabMap.ContainsKey(value)) + throw new InvalidOperationException($"Item {value} has already been added to this {nameof(TabControl)}"); + + var tab = CreateTabItem(value); + AddTabItem(tab, addToDropdown); + + return tab; + } + + private void removeTab(T value, bool removeFromDropdown = true) + { + if (!tabMap.ContainsKey(value)) + throw new InvalidOperationException($"Item {value} doesn't exist in this {nameof(TabControl)}."); + + RemoveTabItem(tabMap[value], removeFromDropdown); + } + + /// + /// Adds an arbitrary to the control. + /// + /// The tab to add. + /// Whether the tab should be added to the Dropdown if supported by the implementation. + protected virtual void AddTabItem(TabItem tab, bool addToDropdown = true) + { + tab.PinnedChanged += performTabSort; + + tab.ActivationRequested += SelectTab; + + tabMap[tab.Value] = tab; + if (addToDropdown) + Dropdown?.AddDropdownItem((tab.Value as Enum)?.GetDescription() ?? tab.Value.ToString(), tab.Value); + TabContainer.Add(tab); + } + + /// + /// Removes a from this . + /// + /// The tab to remove. + /// Whether the tab should be removed from the Dropdown if supported by the implementation. + protected virtual void RemoveTabItem(TabItem tab, bool removeFromDropdown = true) + { + if (!tab.IsRemovable) return; + + if (tab == SelectedTab) + SelectedTab = null; + + tabMap.Remove(tab.Value); + + if (removeFromDropdown) + Dropdown?.RemoveDropdownItem(tab.Value); + + TabContainer.Remove(tab); + } + + /// + /// Callback on the change of visibility of a tab. + /// Used to update the item's status in the overflow dropdown if required. + /// + private void updateDropdown(TabItem tab, bool isVisible) + { + if (isVisible) + Dropdown?.HideItem(tab.Value); + else + Dropdown?.ShowItem(tab.Value); + } + + protected virtual void SelectTab(TabItem tab) + { + // Only reorder if not pinned and not showing + if (AutoSort && !tab.IsPresent && !tab.Pinned) + performTabSort(tab); + + // Deactivate previously selected tab + if (SelectedTab != null && SelectedTab != tab) SelectedTab.Active.Value = false; + + SelectedTab = tab; + SelectedTab.Active.Value = true; + + Current.Value = SelectedTab.Value; + } + + private void performTabSort(TabItem tab) + { + TabContainer.SetLayoutPosition(tab, getTabDepth(tab)); + + // IsPresent of TabItems is based on Y position. + // We reset it here to allow tabs to get a correct initial position. + tab.Y = 0; + } + + private float getTabDepth(TabItem tab) => tab.Pinned ? float.MinValue : --depthCounter; + + protected virtual TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.Both, + Depth = -1, + Masking = true + }; + + public class TabFillFlowContainer : FillFlowContainer> + { + /// + /// Gets called whenever the visibility of a tab in this container changes. Gets invoked with the whose visibility changed and the new visibility state (true = visible, false = hidden). + /// + public Action, bool> TabVisibilityChanged; + + /// + /// The list of tabs currently displayed by this container. + /// + public IEnumerable> TabItems => FlowingChildren.OfType>(); + + protected override IEnumerable ComputeLayoutPositions() + { + foreach (var child in Children) + child.Y = 0; + + var result = base.ComputeLayoutPositions().ToArray(); + int i = 0; + foreach (var child in FlowingChildren.OfType>()) + { + updateChildIfNeeded(child, result[i].Y == 0); + ++i; + } + return result; + } + + private readonly Dictionary, bool> tabVisibility = new Dictionary, bool>(); + + private void updateChildIfNeeded(TabItem child, bool isVisible) + { + if (!tabVisibility.ContainsKey(child) || tabVisibility[child] != isVisible) + { + TabVisibilityChanged?.Invoke(child, isVisible); + tabVisibility[child] = isVisible; + } + } + } + } +} diff --git a/osu.Framework/Graphics/UserInterface/TabItem.cs b/osu.Framework/Graphics/UserInterface/TabItem.cs index 8deab30c4..d92d45ed3 100644 --- a/osu.Framework/Graphics/UserInterface/TabItem.cs +++ b/osu.Framework/Graphics/UserInterface/TabItem.cs @@ -1,74 +1,74 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.UserInterface -{ - public abstract class TabItem : ClickableContainer - { - /// - /// If false, ths cannot be removed from its . - /// - public abstract bool IsRemovable { get; } - } - - public abstract class TabItem : TabItem - { - internal Action> ActivationRequested; - - internal Action> PinnedChanged; - - public override bool IsPresent => base.IsPresent && Y == 0; - - public override bool IsRemovable => false; - - public readonly T Value; - - protected TabItem(T value) - { - Value = value; - - Active.ValueChanged += active_ValueChanged; - } - - private void active_ValueChanged(bool newValue) - { - if (newValue) - OnActivated(); - else - OnDeactivated(); - } - - private bool pinned; - - public bool Pinned - { - get { return pinned; } - set - { - if (pinned == value) return; - - pinned = value; - PinnedChanged?.Invoke(this); - } - } - - protected abstract void OnActivated(); - protected abstract void OnDeactivated(); - - public readonly BindableBool Active = new BindableBool(); - - protected override bool OnClick(InputState state) - { - base.OnClick(state); - ActivationRequested?.Invoke(this); - return true; - } - - public override string ToString() => $"{base.ToString()} value: {Value}"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.UserInterface +{ + public abstract class TabItem : ClickableContainer + { + /// + /// If false, ths cannot be removed from its . + /// + public abstract bool IsRemovable { get; } + } + + public abstract class TabItem : TabItem + { + internal Action> ActivationRequested; + + internal Action> PinnedChanged; + + public override bool IsPresent => base.IsPresent && Y == 0; + + public override bool IsRemovable => false; + + public readonly T Value; + + protected TabItem(T value) + { + Value = value; + + Active.ValueChanged += active_ValueChanged; + } + + private void active_ValueChanged(bool newValue) + { + if (newValue) + OnActivated(); + else + OnDeactivated(); + } + + private bool pinned; + + public bool Pinned + { + get { return pinned; } + set + { + if (pinned == value) return; + + pinned = value; + PinnedChanged?.Invoke(this); + } + } + + protected abstract void OnActivated(); + protected abstract void OnDeactivated(); + + public readonly BindableBool Active = new BindableBool(); + + protected override bool OnClick(InputState state) + { + base.OnClick(state); + ActivationRequested?.Invoke(this); + return true; + } + + public override string ToString() => $"{base.ToString()} value: {Value}"; + } +} diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index b6cc0b935..00e36a70e 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -1,881 +1,881 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Caching; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Framework.MathUtils; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Colour; -using osu.Framework.Platform; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Framework.Timing; - -namespace osu.Framework.Graphics.UserInterface -{ - public class TextBox : TabbableContainer, IHasCurrentValue - { - protected FillFlowContainer TextFlow; - protected Box Background; - protected Drawable Caret; - protected Container TextContainer; - - public override bool HandleKeyboardInput => HasFocus; - - /// - /// Padding to be used within the TextContainer. Requires special handling due to the sideways scrolling of text content. - /// - protected virtual float LeftRightPadding => 5; - - private const float caret_move_time = 60; - - public int? LengthLimit; - - public virtual bool AllowClipboardExport => true; - - //represents the left/right selection coordinates of the word double clicked on when dragging - private int[] doubleClickWord; - - private AudioManager audio; - - /// - /// Whether this TextBox should accept left and right arrow keys for navigation. - /// - public virtual bool HandleLeftRightArrows => true; - - protected virtual Color4 BackgroundCommit => new Color4(249, 90, 255, 200); - protected virtual Color4 BackgroundFocused => new Color4(100, 100, 100, 255); - protected virtual Color4 BackgroundUnfocused => new Color4(100, 100, 100, 120); - - public bool ReadOnly; - - public bool ReleaseFocusOnCommit = true; - - public override bool CanBeTabbedTo => !ReadOnly; - - private ITextInputSource textInput; - private Clipboard clipboard; - - public delegate void OnCommitHandler(TextBox sender, bool newText); - - public OnCommitHandler OnCommit; - - private readonly Scheduler textUpdateScheduler = new Scheduler(); - - public TextBox() - { - Masking = true; - CornerRadius = 3; - - Children = new Drawable[] - { - Background = new Box - { - Colour = BackgroundUnfocused, - RelativeSizeAxes = Axes.Both, - }, - TextContainer = new TextBoxPlatformBindingHandler(this) - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Position = new Vector2(LeftRightPadding, 0), - Children = new[] - { - Placeholder = CreatePlaceholder(), - Caret = new DrawableCaret(), - TextFlow = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }; - - Current.ValueChanged += newValue => { Text = newValue; }; - } - - [BackgroundDependencyLoader] - private void load(GameHost host, AudioManager audio) - { - this.audio = audio; - - textInput = host.GetTextInput(); - clipboard = host.GetClipboard(); - - if (textInput != null) - { - textInput.OnNewImeComposition += delegate(string s) - { - textUpdateScheduler.Add(() => onImeComposition(s)); - cursorAndLayout.Invalidate(); - }; - textInput.OnNewImeResult += delegate - { - textUpdateScheduler.Add(onImeResult); - cursorAndLayout.Invalidate(); - }; - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - textUpdateScheduler.SetCurrentThread(MainThread); - } - - internal override void UpdateClock(IFrameBasedClock clock) - { - base.UpdateClock(clock); - textUpdateScheduler.UpdateClock(Clock); - } - - private void resetSelection() - { - selectionStart = selectionEnd; - cursorAndLayout.Invalidate(); - } - - protected override void Dispose(bool isDisposing) - { - OnCommit = null; - - unbindInput(); - - base.Dispose(isDisposing); - } - - private float textContainerPosX; - - private string textAtLastLayout = string.Empty; - - private void updateCursorAndLayout() - { - const float cursor_width = 3; - - Placeholder.TextSize = CalculatedTextSize; - - textUpdateScheduler.Update(); - - float caretWidth = cursor_width; - - Vector2 cursorPos = Vector2.Zero; - if (text.Length > 0) - cursorPos.X = getPositionAt(selectionLeft) - cursor_width / 2; - - float cursorPosEnd = getPositionAt(selectionEnd); - - if (selectionLength > 0) - caretWidth = getPositionAt(selectionRight) - cursorPos.X; - - float cursorRelativePositionAxesInBox = (cursorPosEnd - textContainerPosX) / DrawWidth; - - //we only want to reposition the view when the cursor reaches near the extremities. - if (cursorRelativePositionAxesInBox < 0.1 || cursorRelativePositionAxesInBox > 0.9) - { - textContainerPosX = cursorPosEnd - DrawWidth / 2 + LeftRightPadding * 2; - } - - textContainerPosX = MathHelper.Clamp(textContainerPosX, 0, Math.Max(0, TextFlow.DrawWidth - DrawWidth + LeftRightPadding * 2)); - - TextContainer.MoveToX(LeftRightPadding - textContainerPosX, 300, Easing.OutExpo); - - if (HasFocus) - { - Caret.ClearTransforms(); - Caret.MoveTo(cursorPos, 60, Easing.Out); - Caret.ResizeWidthTo(caretWidth, caret_move_time, Easing.Out); - - if (selectionLength > 0) - Caret - .FadeTo(0.5f, 200, Easing.Out) - .FadeColour(new Color4(249, 90, 255, 255), 200, Easing.Out); - else - Caret - .FadeColour(Color4.White, 200, Easing.Out) - .Loop(c => c.FadeTo(0.7f).FadeTo(0.4f, 500, Easing.InOutSine)); - } - - if (textAtLastLayout != text) - Current.Value = text; - if (textAtLastLayout.Length == 0 || text.Length == 0) - Placeholder.FadeTo(text.Length == 0 ? 1 : 0, 200); - - textAtLastLayout = text; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - //have to run this after children flow - if (!cursorAndLayout.IsValid) - { - updateCursorAndLayout(); - cursorAndLayout.Validate(); - } - } - - private float getPositionAt(int index) - { - if (index > 0) - { - if (index < text.Length) - return TextFlow.Children[index].DrawPosition.X + TextFlow.DrawPosition.X; - var d = TextFlow.Children[index - 1]; - return d.DrawPosition.X + d.DrawSize.X + TextFlow.Spacing.X + TextFlow.DrawPosition.X; - } - - return 0; - } - - private int getCharacterClosestTo(Vector2 pos) - { - pos = Parent.ToSpaceOfOtherDrawable(pos, TextFlow); - - int i = 0; - foreach (Drawable d in TextFlow.Children) - { - if (d.DrawPosition.X + d.DrawSize.X / 2 > pos.X) - break; - i++; - } - - return i; - } - - private int selectionStart; - private int selectionEnd; - - private int selectionLength => Math.Abs(selectionEnd - selectionStart); - - private int selectionLeft => Math.Min(selectionStart, selectionEnd); - private int selectionRight => Math.Max(selectionStart, selectionEnd); - - private Cached cursorAndLayout = new Cached(); - - private bool handleAction(PlatformAction action) - { - int? amount = null; - - if (!HandleLeftRightArrows && - action.ActionMethod == PlatformActionMethod.Move && - (action.ActionType == PlatformActionType.CharNext || action.ActionType == PlatformActionType.CharPrevious)) - return false; - - switch (action.ActionType) - { - // Clipboard - case PlatformActionType.Cut: - case PlatformActionType.Copy: - if (string.IsNullOrEmpty(SelectedText) || !AllowClipboardExport) return true; - - clipboard?.SetText(SelectedText); - if (action.ActionType == PlatformActionType.Cut) - removeCharacterOrSelection(); - return true; - - case PlatformActionType.Paste: - //the text may get pasted into the hidden textbox, so we don't need any direct clipboard interaction here. - string pending = textInput?.GetPendingText(); - - if (string.IsNullOrEmpty(pending)) - pending = clipboard?.GetText(); - - insertString(pending); - return true; - - case PlatformActionType.SelectAll: - selectionStart = 0; - selectionEnd = text.Length; - cursorAndLayout.Invalidate(); - return true; - - // Cursor Manipulation - case PlatformActionType.CharNext: - amount = 1; - break; - - case PlatformActionType.CharPrevious: - amount = -1; - break; - - case PlatformActionType.LineEnd: - amount = text.Length; - break; - - case PlatformActionType.LineStart: - amount = -text.Length; - break; - - case PlatformActionType.WordNext: - int searchNext = MathHelper.Clamp(selectionEnd, 0, Text.Length - 1); - while (searchNext < Text.Length && text[searchNext] == ' ') - searchNext++; - int nextSpace = text.IndexOf(' ', searchNext); - amount = (nextSpace >= 0 ? nextSpace : text.Length) - selectionEnd; - break; - - case PlatformActionType.WordPrevious: - int searchPrev = MathHelper.Clamp(selectionEnd - 2, 0, Text.Length - 1); - while (searchPrev > 0 && text[searchPrev] == ' ') - searchPrev--; - int lastSpace = text.LastIndexOf(' ', searchPrev); - amount = lastSpace > 0 ? -(selectionEnd - lastSpace - 1) : -selectionEnd; - break; - } - - if (amount.HasValue) - { - switch (action.ActionMethod) - { - case PlatformActionMethod.Move: - resetSelection(); - moveSelection(amount.Value, false); - break; - - case PlatformActionMethod.Select: - moveSelection(amount.Value, true); - break; - - case PlatformActionMethod.Delete: - if (selectionLength == 0) - selectionEnd = MathHelper.Clamp(selectionStart + amount.Value, 0, text.Length); - if (selectionLength > 0) - removeCharacterOrSelection(); - break; - } - - return true; - } - - return false; - } - - private void moveSelection(int offset, bool expand) - { - if (textInput?.ImeActive == true) return; - - int oldStart = selectionStart; - int oldEnd = selectionEnd; - - if (expand) - selectionEnd = MathHelper.Clamp(selectionEnd + offset, 0, text.Length); - else - { - if (selectionLength > 0 && Math.Abs(offset) <= 1) - { - //we don't want to move the location when "removing" an existing selection, just set the new location. - if (offset > 0) - selectionEnd = selectionStart = selectionRight; - else - selectionEnd = selectionStart = selectionLeft; - } - else - selectionEnd = selectionStart = MathHelper.Clamp((offset > 0 ? selectionRight : selectionLeft) + offset, 0, text.Length); - } - - if (oldStart != selectionStart || oldEnd != selectionEnd) - { - audio.Sample.Get(@"Keyboard/key-movement")?.Play(); - cursorAndLayout.Invalidate(); - } - } - - private bool removeCharacterOrSelection(bool sound = true) - { - if (Current.Disabled) - return false; - - if (text.Length == 0) return false; - if (selectionLength == 0 && selectionLeft == 0) return false; - - int count = MathHelper.Clamp(selectionLength, 1, text.Length); - int start = MathHelper.Clamp(selectionLength > 0 ? selectionLeft : selectionLeft - 1, 0, text.Length - count); - - if (count == 0) return false; - - if (sound) - audio.Sample.Get(@"Keyboard/key-delete")?.Play(); - - foreach (var d in TextFlow.Children.Skip(start).Take(count).ToArray()) //ToArray since we are removing items from the children in this block. - { - TextFlow.Remove(d); - - TextContainer.Add(d); - d.FadeOut(200); - d.MoveToY(d.DrawSize.Y, 200, Easing.InExpo); - d.Expire(); - } - - text = text.Remove(start, count); - - if (selectionLength > 0) - selectionStart = selectionEnd = selectionLeft; - else - selectionStart = selectionEnd = selectionLeft - 1; - - cursorAndLayout.Invalidate(); - return true; - } - - protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), TextSize = CalculatedTextSize }; - - protected virtual Drawable AddCharacterToFlow(char c) - { - // Remove all characters to the right and store them in a local list, - // such that their depth can be updated. - List charsRight = new List(); - foreach (Drawable d in TextFlow.Children.Skip(selectionLeft)) - charsRight.Add(d); - TextFlow.RemoveRange(charsRight); - - // Update their depth to make room for the to-be inserted character. - int i = -selectionLeft; - foreach (Drawable d in charsRight) - d.Depth = --i; - - // Add the character - Drawable ch = GetDrawableCharacter(c); - ch.Depth = -selectionLeft; - - TextFlow.Add(ch); - - ch.FadeColour(Color4.Transparent) - .FadeColour(ColourInfo.GradientHorizontal(Color4.White, Color4.Transparent), caret_move_time / 2).Then() - .FadeColour(Color4.White, caret_move_time / 2); - - // Add back all the previously removed characters - TextFlow.AddRange(charsRight); - - return ch; - } - - protected float CalculatedTextSize => TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom); - - /// - /// Insert an arbitrary string into the text at the current position. - /// - /// - private void insertString(string addText) - { - if (string.IsNullOrEmpty(addText)) return; - - foreach (char c in addText) - addCharacter(c); - } - - private Drawable addCharacter(char c) - { - if (Current.Disabled) - return null; - - if (char.IsControl(c)) return null; - - if (selectionLength > 0) - removeCharacterOrSelection(); - - if (text.Length + 1 > LengthLimit) - { - if (Background.Alpha > 0) - Background.FlashColour(Color4.Red, 200); - else - TextFlow.FlashColour(Color4.Red, 200); - return null; - } - - Drawable ch = AddCharacterToFlow(c); - - text = text.Insert(selectionLeft, c.ToString()); - selectionStart = selectionEnd = selectionLeft + 1; - - cursorAndLayout.Invalidate(); - - return ch; - } - - protected virtual SpriteText CreatePlaceholder() => new SpriteText - { - Colour = Color4.Gray, - }; - - protected SpriteText Placeholder; - - public string PlaceholderText - { - get { return Placeholder.Text; } - set { Placeholder.Text = value; } - } - - public Bindable Current { get; } = new Bindable(); - - private string text = string.Empty; - - public virtual string Text - { - get { return text; } - set - { - if (Current.Disabled) - return; - - if (value == text) - return; - - value = value ?? string.Empty; - - Placeholder.FadeTo(value.Length == 0 ? 1 : 0); - - if (!IsLoaded) - Current.Value = text = value; - - textUpdateScheduler.Add(delegate - { - int startBefore = selectionStart; - selectionStart = selectionEnd = 0; - TextFlow?.Clear(); - text = string.Empty; - - foreach (char c in value) - addCharacter(c); - - selectionStart = MathHelper.Clamp(startBefore, 0, text.Length); - }); - - cursorAndLayout.Invalidate(); - } - } - - public string SelectedText => selectionLength > 0 ? Text.Substring(selectionLeft, selectionLength) : string.Empty; - - protected bool HandlePendingText(InputState state) - { - string str = textInput?.GetPendingText(); - if (string.IsNullOrEmpty(str) || ReadOnly) - return false; - - if (state.Keyboard.ShiftPressed) - audio.Sample.Get(@"Keyboard/key-caps")?.Play(); - else - audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play(); - insertString(str); - return true; - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (textInput?.ImeActive == true) return true; - - if (args.Key <= Key.F35) - return false; - - if (HandlePendingText(state)) return true; - - if (ReadOnly) return true; - - if (state.Keyboard.AltPressed || state.Keyboard.ControlPressed || state.Keyboard.SuperPressed) - return false; - - switch (args.Key) - { - case Key.Escape: - GetContainingInputManager().ChangeFocus(null); - return true; - - case Key.Tab: - return base.OnKeyDown(state, args); - - case Key.KeypadEnter: - case Key.Enter: - if (ReleaseFocusOnCommit) - GetContainingInputManager().ChangeFocus(null); - - Background.Colour = ReleaseFocusOnCommit ? BackgroundUnfocused : BackgroundFocused; - Background.ClearTransforms(); - Background.FlashColour(BackgroundCommit, 400); - - audio.Sample.Get(@"Keyboard/key-confirm")?.Play(); - OnCommit?.Invoke(this, true); - return true; - } - - return false; - } - - protected override bool OnDrag(InputState state) - { - //if (textInput?.ImeActive == true) return true; - - if (doubleClickWord != null) - { - //select words at a time - if (getCharacterClosestTo(state.Mouse.Position) > doubleClickWord[1]) - { - selectionStart = doubleClickWord[0]; - selectionEnd = findSeparatorIndex(text, getCharacterClosestTo(state.Mouse.Position) - 1, 1); - selectionEnd = selectionEnd >= 0 ? selectionEnd : text.Length; - } - else if (getCharacterClosestTo(state.Mouse.Position) < doubleClickWord[0]) - { - selectionStart = doubleClickWord[1]; - selectionEnd = findSeparatorIndex(text, getCharacterClosestTo(state.Mouse.Position), -1); - selectionEnd = selectionEnd >= 0 ? selectionEnd + 1 : 0; - } - else - { - //in the middle - selectionStart = doubleClickWord[0]; - selectionEnd = doubleClickWord[1]; - } - - cursorAndLayout.Invalidate(); - } - else - { - if (text.Length == 0) return true; - - selectionEnd = getCharacterClosestTo(state.Mouse.Position); - if (selectionLength > 0) - GetContainingInputManager().ChangeFocus(this); - - cursorAndLayout.Invalidate(); - } - - return true; - } - - protected override bool OnDragStart(InputState state) - { - if (HasFocus) return true; - - if (!state.Mouse.PositionMouseDown.HasValue) - throw new ArgumentNullException(nameof(state.Mouse.PositionMouseDown)); - - Vector2 posDiff = state.Mouse.PositionMouseDown.Value - state.Mouse.Position; - - return Math.Abs(posDiff.X) > Math.Abs(posDiff.Y); - } - - protected override bool OnDoubleClick(InputState state) - { - if (textInput?.ImeActive == true) return true; - - if (text.Length == 0) return true; - - if (AllowClipboardExport) - { - int hover = Math.Min(text.Length - 1, getCharacterClosestTo(state.Mouse.Position)); - - int lastSeparator = findSeparatorIndex(text, hover, -1); - int nextSeparator = findSeparatorIndex(text, hover, 1); - - selectionStart = lastSeparator >= 0 ? lastSeparator + 1 : 0; - selectionEnd = nextSeparator >= 0 ? nextSeparator : text.Length; - } - else - { - selectionStart = 0; - selectionEnd = text.Length; - } - - //in order to keep the home word selected - doubleClickWord = new[] { selectionStart, selectionEnd }; - - cursorAndLayout.Invalidate(); - return true; - } - - private static int findSeparatorIndex(string input, int searchPos, int direction) - { - bool isLetterOrDigit = char.IsLetterOrDigit(input[searchPos]); - - for (int i = searchPos; i >= 0 && i < input.Length; i += direction) - { - if (char.IsLetterOrDigit(input[i]) != isLetterOrDigit) - return i; - } - - return -1; - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - if (textInput?.ImeActive == true) return true; - - selectionStart = selectionEnd = getCharacterClosestTo(state.Mouse.Position); - - cursorAndLayout.Invalidate(); - - return false; - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - doubleClickWord = null; - return true; - } - - protected override void OnFocusLost(InputState state) - { - unbindInput(); - - Caret.ClearTransforms(); - Caret.FadeOut(200); - - - Background.ClearTransforms(); - Background.FadeColour(BackgroundUnfocused, 200, Easing.OutExpo); - - cursorAndLayout.Invalidate(); - } - - public override bool AcceptsFocus => true; - - protected override bool OnClick(InputState state) => !ReadOnly; - - protected override void OnFocus(InputState state) - { - bindInput(); - - Background.ClearTransforms(); - Background.FadeColour(BackgroundFocused, 200, Easing.Out); - - cursorAndLayout.Invalidate(); - } - - #region Native TextBox handling (winform specific) - - private void unbindInput() - { - textInput?.Deactivate(this); - } - - private void bindInput() - { - textInput?.Activate(this); - } - - private void onImeResult() - { - //we only succeeded if there is pending data in the textbox - if (imeDrawables.Count > 0) - { - foreach (Drawable d in imeDrawables) - { - d.Colour = Color4.White; - d.FadeTo(1, 200, Easing.Out); - } - } - - imeDrawables.Clear(); - } - - private readonly List imeDrawables = new List(); - - private void onImeComposition(string s) - { - //search for unchanged characters.. - int matchCount = 0; - bool matching = true; - bool didDelete = false; - - int searchStart = text.Length - imeDrawables.Count; - - //we want to keep processing to the end of the longest string (the current displayed or the new composition). - int maxLength = Math.Max(imeDrawables.Count, s.Length); - - for (int i = 0; i < maxLength; i++) - { - if (matching && searchStart + i < text.Length && i < s.Length && text[searchStart + i] == s[i]) - { - matchCount = i + 1; - continue; - } - - matching = false; - - if (matchCount < imeDrawables.Count) - { - //if we are no longer matching, we want to remove all further characters. - removeCharacterOrSelection(false); - imeDrawables.RemoveAt(matchCount); - didDelete = true; - } - } - - if (matchCount == s.Length) - { - //in the case of backspacing (or a NOP), we can exit early here. - if (didDelete) - audio.Sample.Get(@"Keyboard/key-delete")?.Play(); - return; - } - - //add any new or changed characters - for (int i = matchCount; i < s.Length; i++) - { - Drawable dr = addCharacter(s[i]); - if (dr != null) - { - dr.Colour = Color4.Aqua; - dr.Alpha = 0.6f; - imeDrawables.Add(dr); - } - } - - audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play(); - } - - #endregion - - private class DrawableCaret : CompositeDrawable - { - public DrawableCaret() - { - RelativeSizeAxes = Axes.Y; - Size = new Vector2(1, 0.9f); - Alpha = 0; - Colour = Color4.Transparent; - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - - Masking = true; - CornerRadius = 1; - - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }; - } - } - - private class TextBoxPlatformBindingHandler : Container, IKeyBindingHandler - { - private readonly TextBox textBox; - - public TextBoxPlatformBindingHandler(TextBox textBox) - { - this.textBox = textBox; - } - - public bool OnPressed(PlatformAction action) => textBox.HasFocus && textBox.handleAction(action); - - public bool OnReleased(PlatformAction action) => false; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Caching; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Colour; +using osu.Framework.Platform; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Timing; + +namespace osu.Framework.Graphics.UserInterface +{ + public class TextBox : TabbableContainer, IHasCurrentValue + { + protected FillFlowContainer TextFlow; + protected Box Background; + protected Drawable Caret; + protected Container TextContainer; + + public override bool HandleKeyboardInput => HasFocus; + + /// + /// Padding to be used within the TextContainer. Requires special handling due to the sideways scrolling of text content. + /// + protected virtual float LeftRightPadding => 5; + + private const float caret_move_time = 60; + + public int? LengthLimit; + + public virtual bool AllowClipboardExport => true; + + //represents the left/right selection coordinates of the word double clicked on when dragging + private int[] doubleClickWord; + + private AudioManager audio; + + /// + /// Whether this TextBox should accept left and right arrow keys for navigation. + /// + public virtual bool HandleLeftRightArrows => true; + + protected virtual Color4 BackgroundCommit => new Color4(249, 90, 255, 200); + protected virtual Color4 BackgroundFocused => new Color4(100, 100, 100, 255); + protected virtual Color4 BackgroundUnfocused => new Color4(100, 100, 100, 120); + + public bool ReadOnly; + + public bool ReleaseFocusOnCommit = true; + + public override bool CanBeTabbedTo => !ReadOnly; + + private ITextInputSource textInput; + private Clipboard clipboard; + + public delegate void OnCommitHandler(TextBox sender, bool newText); + + public OnCommitHandler OnCommit; + + private readonly Scheduler textUpdateScheduler = new Scheduler(); + + public TextBox() + { + Masking = true; + CornerRadius = 3; + + Children = new Drawable[] + { + Background = new Box + { + Colour = BackgroundUnfocused, + RelativeSizeAxes = Axes.Both, + }, + TextContainer = new TextBoxPlatformBindingHandler(this) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Position = new Vector2(LeftRightPadding, 0), + Children = new[] + { + Placeholder = CreatePlaceholder(), + Caret = new DrawableCaret(), + TextFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }; + + Current.ValueChanged += newValue => { Text = newValue; }; + } + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + this.audio = audio; + + textInput = host.GetTextInput(); + clipboard = host.GetClipboard(); + + if (textInput != null) + { + textInput.OnNewImeComposition += delegate(string s) + { + textUpdateScheduler.Add(() => onImeComposition(s)); + cursorAndLayout.Invalidate(); + }; + textInput.OnNewImeResult += delegate + { + textUpdateScheduler.Add(onImeResult); + cursorAndLayout.Invalidate(); + }; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + textUpdateScheduler.SetCurrentThread(MainThread); + } + + internal override void UpdateClock(IFrameBasedClock clock) + { + base.UpdateClock(clock); + textUpdateScheduler.UpdateClock(Clock); + } + + private void resetSelection() + { + selectionStart = selectionEnd; + cursorAndLayout.Invalidate(); + } + + protected override void Dispose(bool isDisposing) + { + OnCommit = null; + + unbindInput(); + + base.Dispose(isDisposing); + } + + private float textContainerPosX; + + private string textAtLastLayout = string.Empty; + + private void updateCursorAndLayout() + { + const float cursor_width = 3; + + Placeholder.TextSize = CalculatedTextSize; + + textUpdateScheduler.Update(); + + float caretWidth = cursor_width; + + Vector2 cursorPos = Vector2.Zero; + if (text.Length > 0) + cursorPos.X = getPositionAt(selectionLeft) - cursor_width / 2; + + float cursorPosEnd = getPositionAt(selectionEnd); + + if (selectionLength > 0) + caretWidth = getPositionAt(selectionRight) - cursorPos.X; + + float cursorRelativePositionAxesInBox = (cursorPosEnd - textContainerPosX) / DrawWidth; + + //we only want to reposition the view when the cursor reaches near the extremities. + if (cursorRelativePositionAxesInBox < 0.1 || cursorRelativePositionAxesInBox > 0.9) + { + textContainerPosX = cursorPosEnd - DrawWidth / 2 + LeftRightPadding * 2; + } + + textContainerPosX = MathHelper.Clamp(textContainerPosX, 0, Math.Max(0, TextFlow.DrawWidth - DrawWidth + LeftRightPadding * 2)); + + TextContainer.MoveToX(LeftRightPadding - textContainerPosX, 300, Easing.OutExpo); + + if (HasFocus) + { + Caret.ClearTransforms(); + Caret.MoveTo(cursorPos, 60, Easing.Out); + Caret.ResizeWidthTo(caretWidth, caret_move_time, Easing.Out); + + if (selectionLength > 0) + Caret + .FadeTo(0.5f, 200, Easing.Out) + .FadeColour(new Color4(249, 90, 255, 255), 200, Easing.Out); + else + Caret + .FadeColour(Color4.White, 200, Easing.Out) + .Loop(c => c.FadeTo(0.7f).FadeTo(0.4f, 500, Easing.InOutSine)); + } + + if (textAtLastLayout != text) + Current.Value = text; + if (textAtLastLayout.Length == 0 || text.Length == 0) + Placeholder.FadeTo(text.Length == 0 ? 1 : 0, 200); + + textAtLastLayout = text; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + //have to run this after children flow + if (!cursorAndLayout.IsValid) + { + updateCursorAndLayout(); + cursorAndLayout.Validate(); + } + } + + private float getPositionAt(int index) + { + if (index > 0) + { + if (index < text.Length) + return TextFlow.Children[index].DrawPosition.X + TextFlow.DrawPosition.X; + var d = TextFlow.Children[index - 1]; + return d.DrawPosition.X + d.DrawSize.X + TextFlow.Spacing.X + TextFlow.DrawPosition.X; + } + + return 0; + } + + private int getCharacterClosestTo(Vector2 pos) + { + pos = Parent.ToSpaceOfOtherDrawable(pos, TextFlow); + + int i = 0; + foreach (Drawable d in TextFlow.Children) + { + if (d.DrawPosition.X + d.DrawSize.X / 2 > pos.X) + break; + i++; + } + + return i; + } + + private int selectionStart; + private int selectionEnd; + + private int selectionLength => Math.Abs(selectionEnd - selectionStart); + + private int selectionLeft => Math.Min(selectionStart, selectionEnd); + private int selectionRight => Math.Max(selectionStart, selectionEnd); + + private Cached cursorAndLayout = new Cached(); + + private bool handleAction(PlatformAction action) + { + int? amount = null; + + if (!HandleLeftRightArrows && + action.ActionMethod == PlatformActionMethod.Move && + (action.ActionType == PlatformActionType.CharNext || action.ActionType == PlatformActionType.CharPrevious)) + return false; + + switch (action.ActionType) + { + // Clipboard + case PlatformActionType.Cut: + case PlatformActionType.Copy: + if (string.IsNullOrEmpty(SelectedText) || !AllowClipboardExport) return true; + + clipboard?.SetText(SelectedText); + if (action.ActionType == PlatformActionType.Cut) + removeCharacterOrSelection(); + return true; + + case PlatformActionType.Paste: + //the text may get pasted into the hidden textbox, so we don't need any direct clipboard interaction here. + string pending = textInput?.GetPendingText(); + + if (string.IsNullOrEmpty(pending)) + pending = clipboard?.GetText(); + + insertString(pending); + return true; + + case PlatformActionType.SelectAll: + selectionStart = 0; + selectionEnd = text.Length; + cursorAndLayout.Invalidate(); + return true; + + // Cursor Manipulation + case PlatformActionType.CharNext: + amount = 1; + break; + + case PlatformActionType.CharPrevious: + amount = -1; + break; + + case PlatformActionType.LineEnd: + amount = text.Length; + break; + + case PlatformActionType.LineStart: + amount = -text.Length; + break; + + case PlatformActionType.WordNext: + int searchNext = MathHelper.Clamp(selectionEnd, 0, Text.Length - 1); + while (searchNext < Text.Length && text[searchNext] == ' ') + searchNext++; + int nextSpace = text.IndexOf(' ', searchNext); + amount = (nextSpace >= 0 ? nextSpace : text.Length) - selectionEnd; + break; + + case PlatformActionType.WordPrevious: + int searchPrev = MathHelper.Clamp(selectionEnd - 2, 0, Text.Length - 1); + while (searchPrev > 0 && text[searchPrev] == ' ') + searchPrev--; + int lastSpace = text.LastIndexOf(' ', searchPrev); + amount = lastSpace > 0 ? -(selectionEnd - lastSpace - 1) : -selectionEnd; + break; + } + + if (amount.HasValue) + { + switch (action.ActionMethod) + { + case PlatformActionMethod.Move: + resetSelection(); + moveSelection(amount.Value, false); + break; + + case PlatformActionMethod.Select: + moveSelection(amount.Value, true); + break; + + case PlatformActionMethod.Delete: + if (selectionLength == 0) + selectionEnd = MathHelper.Clamp(selectionStart + amount.Value, 0, text.Length); + if (selectionLength > 0) + removeCharacterOrSelection(); + break; + } + + return true; + } + + return false; + } + + private void moveSelection(int offset, bool expand) + { + if (textInput?.ImeActive == true) return; + + int oldStart = selectionStart; + int oldEnd = selectionEnd; + + if (expand) + selectionEnd = MathHelper.Clamp(selectionEnd + offset, 0, text.Length); + else + { + if (selectionLength > 0 && Math.Abs(offset) <= 1) + { + //we don't want to move the location when "removing" an existing selection, just set the new location. + if (offset > 0) + selectionEnd = selectionStart = selectionRight; + else + selectionEnd = selectionStart = selectionLeft; + } + else + selectionEnd = selectionStart = MathHelper.Clamp((offset > 0 ? selectionRight : selectionLeft) + offset, 0, text.Length); + } + + if (oldStart != selectionStart || oldEnd != selectionEnd) + { + audio.Sample.Get(@"Keyboard/key-movement")?.Play(); + cursorAndLayout.Invalidate(); + } + } + + private bool removeCharacterOrSelection(bool sound = true) + { + if (Current.Disabled) + return false; + + if (text.Length == 0) return false; + if (selectionLength == 0 && selectionLeft == 0) return false; + + int count = MathHelper.Clamp(selectionLength, 1, text.Length); + int start = MathHelper.Clamp(selectionLength > 0 ? selectionLeft : selectionLeft - 1, 0, text.Length - count); + + if (count == 0) return false; + + if (sound) + audio.Sample.Get(@"Keyboard/key-delete")?.Play(); + + foreach (var d in TextFlow.Children.Skip(start).Take(count).ToArray()) //ToArray since we are removing items from the children in this block. + { + TextFlow.Remove(d); + + TextContainer.Add(d); + d.FadeOut(200); + d.MoveToY(d.DrawSize.Y, 200, Easing.InExpo); + d.Expire(); + } + + text = text.Remove(start, count); + + if (selectionLength > 0) + selectionStart = selectionEnd = selectionLeft; + else + selectionStart = selectionEnd = selectionLeft - 1; + + cursorAndLayout.Invalidate(); + return true; + } + + protected virtual Drawable GetDrawableCharacter(char c) => new SpriteText { Text = c.ToString(), TextSize = CalculatedTextSize }; + + protected virtual Drawable AddCharacterToFlow(char c) + { + // Remove all characters to the right and store them in a local list, + // such that their depth can be updated. + List charsRight = new List(); + foreach (Drawable d in TextFlow.Children.Skip(selectionLeft)) + charsRight.Add(d); + TextFlow.RemoveRange(charsRight); + + // Update their depth to make room for the to-be inserted character. + int i = -selectionLeft; + foreach (Drawable d in charsRight) + d.Depth = --i; + + // Add the character + Drawable ch = GetDrawableCharacter(c); + ch.Depth = -selectionLeft; + + TextFlow.Add(ch); + + ch.FadeColour(Color4.Transparent) + .FadeColour(ColourInfo.GradientHorizontal(Color4.White, Color4.Transparent), caret_move_time / 2).Then() + .FadeColour(Color4.White, caret_move_time / 2); + + // Add back all the previously removed characters + TextFlow.AddRange(charsRight); + + return ch; + } + + protected float CalculatedTextSize => TextFlow.DrawSize.Y - (TextFlow.Padding.Top + TextFlow.Padding.Bottom); + + /// + /// Insert an arbitrary string into the text at the current position. + /// + /// + private void insertString(string addText) + { + if (string.IsNullOrEmpty(addText)) return; + + foreach (char c in addText) + addCharacter(c); + } + + private Drawable addCharacter(char c) + { + if (Current.Disabled) + return null; + + if (char.IsControl(c)) return null; + + if (selectionLength > 0) + removeCharacterOrSelection(); + + if (text.Length + 1 > LengthLimit) + { + if (Background.Alpha > 0) + Background.FlashColour(Color4.Red, 200); + else + TextFlow.FlashColour(Color4.Red, 200); + return null; + } + + Drawable ch = AddCharacterToFlow(c); + + text = text.Insert(selectionLeft, c.ToString()); + selectionStart = selectionEnd = selectionLeft + 1; + + cursorAndLayout.Invalidate(); + + return ch; + } + + protected virtual SpriteText CreatePlaceholder() => new SpriteText + { + Colour = Color4.Gray, + }; + + protected SpriteText Placeholder; + + public string PlaceholderText + { + get { return Placeholder.Text; } + set { Placeholder.Text = value; } + } + + public Bindable Current { get; } = new Bindable(); + + private string text = string.Empty; + + public virtual string Text + { + get { return text; } + set + { + if (Current.Disabled) + return; + + if (value == text) + return; + + value = value ?? string.Empty; + + Placeholder.FadeTo(value.Length == 0 ? 1 : 0); + + if (!IsLoaded) + Current.Value = text = value; + + textUpdateScheduler.Add(delegate + { + int startBefore = selectionStart; + selectionStart = selectionEnd = 0; + TextFlow?.Clear(); + text = string.Empty; + + foreach (char c in value) + addCharacter(c); + + selectionStart = MathHelper.Clamp(startBefore, 0, text.Length); + }); + + cursorAndLayout.Invalidate(); + } + } + + public string SelectedText => selectionLength > 0 ? Text.Substring(selectionLeft, selectionLength) : string.Empty; + + protected bool HandlePendingText(InputState state) + { + string str = textInput?.GetPendingText(); + if (string.IsNullOrEmpty(str) || ReadOnly) + return false; + + if (state.Keyboard.ShiftPressed) + audio.Sample.Get(@"Keyboard/key-caps")?.Play(); + else + audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play(); + insertString(str); + return true; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (textInput?.ImeActive == true) return true; + + if (args.Key <= Key.F35) + return false; + + if (HandlePendingText(state)) return true; + + if (ReadOnly) return true; + + if (state.Keyboard.AltPressed || state.Keyboard.ControlPressed || state.Keyboard.SuperPressed) + return false; + + switch (args.Key) + { + case Key.Escape: + GetContainingInputManager().ChangeFocus(null); + return true; + + case Key.Tab: + return base.OnKeyDown(state, args); + + case Key.KeypadEnter: + case Key.Enter: + if (ReleaseFocusOnCommit) + GetContainingInputManager().ChangeFocus(null); + + Background.Colour = ReleaseFocusOnCommit ? BackgroundUnfocused : BackgroundFocused; + Background.ClearTransforms(); + Background.FlashColour(BackgroundCommit, 400); + + audio.Sample.Get(@"Keyboard/key-confirm")?.Play(); + OnCommit?.Invoke(this, true); + return true; + } + + return false; + } + + protected override bool OnDrag(InputState state) + { + //if (textInput?.ImeActive == true) return true; + + if (doubleClickWord != null) + { + //select words at a time + if (getCharacterClosestTo(state.Mouse.Position) > doubleClickWord[1]) + { + selectionStart = doubleClickWord[0]; + selectionEnd = findSeparatorIndex(text, getCharacterClosestTo(state.Mouse.Position) - 1, 1); + selectionEnd = selectionEnd >= 0 ? selectionEnd : text.Length; + } + else if (getCharacterClosestTo(state.Mouse.Position) < doubleClickWord[0]) + { + selectionStart = doubleClickWord[1]; + selectionEnd = findSeparatorIndex(text, getCharacterClosestTo(state.Mouse.Position), -1); + selectionEnd = selectionEnd >= 0 ? selectionEnd + 1 : 0; + } + else + { + //in the middle + selectionStart = doubleClickWord[0]; + selectionEnd = doubleClickWord[1]; + } + + cursorAndLayout.Invalidate(); + } + else + { + if (text.Length == 0) return true; + + selectionEnd = getCharacterClosestTo(state.Mouse.Position); + if (selectionLength > 0) + GetContainingInputManager().ChangeFocus(this); + + cursorAndLayout.Invalidate(); + } + + return true; + } + + protected override bool OnDragStart(InputState state) + { + if (HasFocus) return true; + + if (!state.Mouse.PositionMouseDown.HasValue) + throw new ArgumentNullException(nameof(state.Mouse.PositionMouseDown)); + + Vector2 posDiff = state.Mouse.PositionMouseDown.Value - state.Mouse.Position; + + return Math.Abs(posDiff.X) > Math.Abs(posDiff.Y); + } + + protected override bool OnDoubleClick(InputState state) + { + if (textInput?.ImeActive == true) return true; + + if (text.Length == 0) return true; + + if (AllowClipboardExport) + { + int hover = Math.Min(text.Length - 1, getCharacterClosestTo(state.Mouse.Position)); + + int lastSeparator = findSeparatorIndex(text, hover, -1); + int nextSeparator = findSeparatorIndex(text, hover, 1); + + selectionStart = lastSeparator >= 0 ? lastSeparator + 1 : 0; + selectionEnd = nextSeparator >= 0 ? nextSeparator : text.Length; + } + else + { + selectionStart = 0; + selectionEnd = text.Length; + } + + //in order to keep the home word selected + doubleClickWord = new[] { selectionStart, selectionEnd }; + + cursorAndLayout.Invalidate(); + return true; + } + + private static int findSeparatorIndex(string input, int searchPos, int direction) + { + bool isLetterOrDigit = char.IsLetterOrDigit(input[searchPos]); + + for (int i = searchPos; i >= 0 && i < input.Length; i += direction) + { + if (char.IsLetterOrDigit(input[i]) != isLetterOrDigit) + return i; + } + + return -1; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (textInput?.ImeActive == true) return true; + + selectionStart = selectionEnd = getCharacterClosestTo(state.Mouse.Position); + + cursorAndLayout.Invalidate(); + + return false; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + doubleClickWord = null; + return true; + } + + protected override void OnFocusLost(InputState state) + { + unbindInput(); + + Caret.ClearTransforms(); + Caret.FadeOut(200); + + + Background.ClearTransforms(); + Background.FadeColour(BackgroundUnfocused, 200, Easing.OutExpo); + + cursorAndLayout.Invalidate(); + } + + public override bool AcceptsFocus => true; + + protected override bool OnClick(InputState state) => !ReadOnly; + + protected override void OnFocus(InputState state) + { + bindInput(); + + Background.ClearTransforms(); + Background.FadeColour(BackgroundFocused, 200, Easing.Out); + + cursorAndLayout.Invalidate(); + } + + #region Native TextBox handling (winform specific) + + private void unbindInput() + { + textInput?.Deactivate(this); + } + + private void bindInput() + { + textInput?.Activate(this); + } + + private void onImeResult() + { + //we only succeeded if there is pending data in the textbox + if (imeDrawables.Count > 0) + { + foreach (Drawable d in imeDrawables) + { + d.Colour = Color4.White; + d.FadeTo(1, 200, Easing.Out); + } + } + + imeDrawables.Clear(); + } + + private readonly List imeDrawables = new List(); + + private void onImeComposition(string s) + { + //search for unchanged characters.. + int matchCount = 0; + bool matching = true; + bool didDelete = false; + + int searchStart = text.Length - imeDrawables.Count; + + //we want to keep processing to the end of the longest string (the current displayed or the new composition). + int maxLength = Math.Max(imeDrawables.Count, s.Length); + + for (int i = 0; i < maxLength; i++) + { + if (matching && searchStart + i < text.Length && i < s.Length && text[searchStart + i] == s[i]) + { + matchCount = i + 1; + continue; + } + + matching = false; + + if (matchCount < imeDrawables.Count) + { + //if we are no longer matching, we want to remove all further characters. + removeCharacterOrSelection(false); + imeDrawables.RemoveAt(matchCount); + didDelete = true; + } + } + + if (matchCount == s.Length) + { + //in the case of backspacing (or a NOP), we can exit early here. + if (didDelete) + audio.Sample.Get(@"Keyboard/key-delete")?.Play(); + return; + } + + //add any new or changed characters + for (int i = matchCount; i < s.Length; i++) + { + Drawable dr = addCharacter(s[i]); + if (dr != null) + { + dr.Colour = Color4.Aqua; + dr.Alpha = 0.6f; + imeDrawables.Add(dr); + } + } + + audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play(); + } + + #endregion + + private class DrawableCaret : CompositeDrawable + { + public DrawableCaret() + { + RelativeSizeAxes = Axes.Y; + Size = new Vector2(1, 0.9f); + Alpha = 0; + Colour = Color4.Transparent; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Masking = true; + CornerRadius = 1; + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }; + } + } + + private class TextBoxPlatformBindingHandler : Container, IKeyBindingHandler + { + private readonly TextBox textBox; + + public TextBoxPlatformBindingHandler(TextBox textBox) + { + this.textBox = textBox; + } + + public bool OnPressed(PlatformAction action) => textBox.HasFocus && textBox.handleAction(action); + + public bool OnReleased(PlatformAction action) => false; + } + } +} diff --git a/osu.Framework/Graphics/Vector2Extensions.cs b/osu.Framework/Graphics/Vector2Extensions.cs index ad697f9af..57e412294 100644 --- a/osu.Framework/Graphics/Vector2Extensions.cs +++ b/osu.Framework/Graphics/Vector2Extensions.cs @@ -1,77 +1,77 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; - -namespace osu.Framework.Graphics -{ - public static class Vector2Extensions - { - /// Transform a Position by the given Matrix - /// The position to transform - /// The desired transformation - /// The transformed position - public static Vector2 Transform(Vector2 pos, Matrix3 mat) - { - Transform(ref pos, ref mat, out Vector2 result); - return result; - } - - /// Transform a Position by the given Matrix - /// The position to transform - /// The desired transformation - /// The transformed vector - public static void Transform(ref Vector2 pos, ref Matrix3 mat, out Vector2 result) - { - result.X = mat.Row0.X * pos.X + mat.Row1.X * pos.Y + mat.Row2.X; - result.Y = mat.Row0.Y * pos.X + mat.Row1.Y * pos.Y + mat.Row2.Y; - } - - /// - /// Compute the euclidean distance between two vectors. - /// - /// The first vector - /// The second vector - /// The distance - public static float Distance(Vector2 vec1, Vector2 vec2) - { - Distance(ref vec1, ref vec2, out float result); - return result; - } - - /// - /// Compute the euclidean distance between two vectors. - /// - /// The first vector - /// The second vector - /// The distance - public static void Distance(ref Vector2 vec1, ref Vector2 vec2, out float result) - { - result = (float)Math.Sqrt((vec2.X - vec1.X) * (vec2.X - vec1.X) + (vec2.Y - vec1.Y) * (vec2.Y - vec1.Y)); - } - - /// - /// Compute the squared euclidean distance between two vectors. - /// - /// The first vector - /// The second vector - /// The squared distance - public static float DistanceSquared(Vector2 vec1, Vector2 vec2) - { - DistanceSquared(ref vec1, ref vec2, out float result); - return result; - } - - /// - /// Compute the squared euclidean distance between two vectors. - /// - /// The first vector - /// The second vector - /// The squared distance - public static void DistanceSquared(ref Vector2 vec1, ref Vector2 vec2, out float result) - { - result = (vec2.X - vec1.X) * (vec2.X - vec1.X) + (vec2.Y - vec1.Y) * (vec2.Y - vec1.Y); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; + +namespace osu.Framework.Graphics +{ + public static class Vector2Extensions + { + /// Transform a Position by the given Matrix + /// The position to transform + /// The desired transformation + /// The transformed position + public static Vector2 Transform(Vector2 pos, Matrix3 mat) + { + Transform(ref pos, ref mat, out Vector2 result); + return result; + } + + /// Transform a Position by the given Matrix + /// The position to transform + /// The desired transformation + /// The transformed vector + public static void Transform(ref Vector2 pos, ref Matrix3 mat, out Vector2 result) + { + result.X = mat.Row0.X * pos.X + mat.Row1.X * pos.Y + mat.Row2.X; + result.Y = mat.Row0.Y * pos.X + mat.Row1.Y * pos.Y + mat.Row2.Y; + } + + /// + /// Compute the euclidean distance between two vectors. + /// + /// The first vector + /// The second vector + /// The distance + public static float Distance(Vector2 vec1, Vector2 vec2) + { + Distance(ref vec1, ref vec2, out float result); + return result; + } + + /// + /// Compute the euclidean distance between two vectors. + /// + /// The first vector + /// The second vector + /// The distance + public static void Distance(ref Vector2 vec1, ref Vector2 vec2, out float result) + { + result = (float)Math.Sqrt((vec2.X - vec1.X) * (vec2.X - vec1.X) + (vec2.Y - vec1.Y) * (vec2.Y - vec1.Y)); + } + + /// + /// Compute the squared euclidean distance between two vectors. + /// + /// The first vector + /// The second vector + /// The squared distance + public static float DistanceSquared(Vector2 vec1, Vector2 vec2) + { + DistanceSquared(ref vec1, ref vec2, out float result); + return result; + } + + /// + /// Compute the squared euclidean distance between two vectors. + /// + /// The first vector + /// The second vector + /// The squared distance + public static void DistanceSquared(ref Vector2 vec1, ref Vector2 vec2, out float result) + { + result = (vec2.X - vec1.X) * (vec2.X - vec1.X) + (vec2.Y - vec1.Y) * (vec2.Y - vec1.Y); + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs b/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs index 152adb9f4..5a39b9a5a 100644 --- a/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs +++ b/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs @@ -1,266 +1,266 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Linq; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Input; - -namespace osu.Framework.Graphics.Visualisation -{ - public class DrawVisualiser : OverlayContainer - { - private readonly TreeContainer treeContainer; - private VisualisedDrawable highlightedTarget; - - private readonly PropertyDisplay propertyDisplay; - - private readonly InfoOverlay overlay; - - private InputManager inputManager; - - public DrawVisualiser() - { - RelativeSizeAxes = Axes.Both; - Children = new Drawable[] - { - overlay = new InfoOverlay(), - treeContainer = new TreeContainer - { - ChooseTarget = chooseTarget, - GoUpOneParent = delegate - { - Drawable lastHighlight = highlightedTarget?.Target; - - var parent = Target?.Parent; - if (parent?.Parent != null) - Target = Target?.Parent; - - // Rehighlight the last highlight - if (lastHighlight != null) - { - VisualisedDrawable visualised = targetDrawable.FindVisualisedDrawable(lastHighlight); - if (visualised != null) - { - propertyDisplay.State = Visibility.Visible; - setHighlight(visualised); - } - } - }, - ToggleProperties = delegate - { - if (targetDrawable == null) - return; - - propertyDisplay.ToggleVisibility(); - - if (propertyDisplay.State == Visibility.Visible) - setHighlight(targetDrawable); - }, - }, - new CursorContainer() - }; - - propertyDisplay = treeContainer.PropertyDisplay; - - propertyDisplay.StateChanged += visibility => - { - switch (visibility) - { - case Visibility.Hidden: - // Dehighlight everything automatically if property display is closed - setHighlight(null); - break; - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager(); - } - - protected override bool BlockPassThroughMouse => false; - - protected override void PopIn() - { - this.FadeIn(100); - if (Target == null) - chooseTarget(); - else - createRootVisualisedDrawable(); - } - - protected override void PopOut() - { - this.FadeOut(100); - - // Don't keep resources for visualizing the target - // allocated; unbind callback events. - removeRootVisualisedDrawable(); - } - - private bool targetSearching; - - private void chooseTarget() - { - Target = null; - targetSearching = true; - } - - private Drawable findTargetIn(Drawable d, InputState state) - { - if (d is DrawVisualiser) return null; - if (d is CursorContainer) return null; - if (d is PropertyDisplay) return null; - - if (!d.IsPresent) return null; - - bool containsCursor = d.ScreenSpaceDrawQuad.Contains(state.Mouse.NativeState.Position); - // This is an optimization: We don't need to consider drawables which we don't hover, and which do not - // forward input further to children (via d.ReceiveMouseInputAt). If they do forward input to children, then there - // is a good chance they have children poking out of their bounds, which we need to catch. - if (!containsCursor && !d.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) - return null; - - var dAsContainer = d as CompositeDrawable; - - Drawable containedTarget = null; - - if (dAsContainer != null) - { - if (!dAsContainer.InternalChildren.Any()) - return null; - - foreach (var c in dAsContainer.AliveInternalChildren) - { - var contained = findTargetIn(c, state); - if (contained != null) - { - if (containedTarget == null || - containedTarget.DrawWidth * containedTarget.DrawHeight > contained.DrawWidth * contained.DrawHeight) - { - containedTarget = contained; - } - } - } - } - - return containedTarget ?? (containsCursor ? d : null); - } - - private VisualisedDrawable targetDrawable; - - private void removeRootVisualisedDrawable(bool hideProperties = true) - { - if (hideProperties) - propertyDisplay.State = Visibility.Hidden; - - if (targetDrawable != null) - { - if (targetDrawable.Parent != null) - { - // targetDrawable may have gotten purged from the TreeContainer - treeContainer.Remove(targetDrawable); - targetDrawable.Dispose(); - } - targetDrawable = null; - } - } - - private void createRootVisualisedDrawable() - { - removeRootVisualisedDrawable(target == null); - - if (target == null) - return; - - targetDrawable = new VisualisedDrawable(target, treeContainer) - { - RequestTarget = d => Target = d, - HighlightTarget = d => - { - propertyDisplay.State = Visibility.Visible; - - // Either highlight or dehighlight the target, depending on whether - // it is currently highlighted - setHighlight(d); - - } - }; - - treeContainer.Add(targetDrawable); - } - - private Drawable target; - - public Drawable Target - { - get { return target; } - set - { - target = value; - createRootVisualisedDrawable(); - } - } - - private void setHighlight(VisualisedDrawable newHighlight) - { - if (highlightedTarget != null) - { - // Dehighlight the lastly highlighted target - highlightedTarget.IsHighlighted = false; - highlightedTarget = null; - } - - if (newHighlight == null) - { - propertyDisplay.UpdateFrom(null); - return; - } - - // Only update when property display is visible - if (propertyDisplay.State == Visibility.Visible) - { - highlightedTarget = newHighlight; - newHighlight.IsHighlighted = true; - - propertyDisplay.UpdateFrom(newHighlight.Target); - } - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - return targetSearching; - } - - private Drawable findTarget(InputState state) - { - return findTargetIn(Parent?.Parent, state); - } - - protected override bool OnClick(InputState state) - { - if (targetSearching) - { - Target = findTarget(state)?.Parent; - - if (Target != null) - { - targetSearching = false; - overlay.Target = null; - return true; - } - } - - return base.OnClick(state); - } - - protected override bool OnMouseMove(InputState state) - { - overlay.Target = targetSearching ? findTarget(state) : inputManager.HoveredDrawables.OfType().FirstOrDefault()?.Target; - return base.OnMouseMove(state); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Linq; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; + +namespace osu.Framework.Graphics.Visualisation +{ + public class DrawVisualiser : OverlayContainer + { + private readonly TreeContainer treeContainer; + private VisualisedDrawable highlightedTarget; + + private readonly PropertyDisplay propertyDisplay; + + private readonly InfoOverlay overlay; + + private InputManager inputManager; + + public DrawVisualiser() + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + overlay = new InfoOverlay(), + treeContainer = new TreeContainer + { + ChooseTarget = chooseTarget, + GoUpOneParent = delegate + { + Drawable lastHighlight = highlightedTarget?.Target; + + var parent = Target?.Parent; + if (parent?.Parent != null) + Target = Target?.Parent; + + // Rehighlight the last highlight + if (lastHighlight != null) + { + VisualisedDrawable visualised = targetDrawable.FindVisualisedDrawable(lastHighlight); + if (visualised != null) + { + propertyDisplay.State = Visibility.Visible; + setHighlight(visualised); + } + } + }, + ToggleProperties = delegate + { + if (targetDrawable == null) + return; + + propertyDisplay.ToggleVisibility(); + + if (propertyDisplay.State == Visibility.Visible) + setHighlight(targetDrawable); + }, + }, + new CursorContainer() + }; + + propertyDisplay = treeContainer.PropertyDisplay; + + propertyDisplay.StateChanged += visibility => + { + switch (visibility) + { + case Visibility.Hidden: + // Dehighlight everything automatically if property display is closed + setHighlight(null); + break; + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + protected override bool BlockPassThroughMouse => false; + + protected override void PopIn() + { + this.FadeIn(100); + if (Target == null) + chooseTarget(); + else + createRootVisualisedDrawable(); + } + + protected override void PopOut() + { + this.FadeOut(100); + + // Don't keep resources for visualizing the target + // allocated; unbind callback events. + removeRootVisualisedDrawable(); + } + + private bool targetSearching; + + private void chooseTarget() + { + Target = null; + targetSearching = true; + } + + private Drawable findTargetIn(Drawable d, InputState state) + { + if (d is DrawVisualiser) return null; + if (d is CursorContainer) return null; + if (d is PropertyDisplay) return null; + + if (!d.IsPresent) return null; + + bool containsCursor = d.ScreenSpaceDrawQuad.Contains(state.Mouse.NativeState.Position); + // This is an optimization: We don't need to consider drawables which we don't hover, and which do not + // forward input further to children (via d.ReceiveMouseInputAt). If they do forward input to children, then there + // is a good chance they have children poking out of their bounds, which we need to catch. + if (!containsCursor && !d.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) + return null; + + var dAsContainer = d as CompositeDrawable; + + Drawable containedTarget = null; + + if (dAsContainer != null) + { + if (!dAsContainer.InternalChildren.Any()) + return null; + + foreach (var c in dAsContainer.AliveInternalChildren) + { + var contained = findTargetIn(c, state); + if (contained != null) + { + if (containedTarget == null || + containedTarget.DrawWidth * containedTarget.DrawHeight > contained.DrawWidth * contained.DrawHeight) + { + containedTarget = contained; + } + } + } + } + + return containedTarget ?? (containsCursor ? d : null); + } + + private VisualisedDrawable targetDrawable; + + private void removeRootVisualisedDrawable(bool hideProperties = true) + { + if (hideProperties) + propertyDisplay.State = Visibility.Hidden; + + if (targetDrawable != null) + { + if (targetDrawable.Parent != null) + { + // targetDrawable may have gotten purged from the TreeContainer + treeContainer.Remove(targetDrawable); + targetDrawable.Dispose(); + } + targetDrawable = null; + } + } + + private void createRootVisualisedDrawable() + { + removeRootVisualisedDrawable(target == null); + + if (target == null) + return; + + targetDrawable = new VisualisedDrawable(target, treeContainer) + { + RequestTarget = d => Target = d, + HighlightTarget = d => + { + propertyDisplay.State = Visibility.Visible; + + // Either highlight or dehighlight the target, depending on whether + // it is currently highlighted + setHighlight(d); + + } + }; + + treeContainer.Add(targetDrawable); + } + + private Drawable target; + + public Drawable Target + { + get { return target; } + set + { + target = value; + createRootVisualisedDrawable(); + } + } + + private void setHighlight(VisualisedDrawable newHighlight) + { + if (highlightedTarget != null) + { + // Dehighlight the lastly highlighted target + highlightedTarget.IsHighlighted = false; + highlightedTarget = null; + } + + if (newHighlight == null) + { + propertyDisplay.UpdateFrom(null); + return; + } + + // Only update when property display is visible + if (propertyDisplay.State == Visibility.Visible) + { + highlightedTarget = newHighlight; + newHighlight.IsHighlighted = true; + + propertyDisplay.UpdateFrom(newHighlight.Target); + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + return targetSearching; + } + + private Drawable findTarget(InputState state) + { + return findTargetIn(Parent?.Parent, state); + } + + protected override bool OnClick(InputState state) + { + if (targetSearching) + { + Target = findTarget(state)?.Parent; + + if (Target != null) + { + targetSearching = false; + overlay.Target = null; + return true; + } + } + + return base.OnClick(state); + } + + protected override bool OnMouseMove(InputState state) + { + overlay.Target = targetSearching ? findTarget(state) : inputManager.HoveredDrawables.OfType().FirstOrDefault()?.Target; + return base.OnMouseMove(state); + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/FlashyBox.cs b/osu.Framework/Graphics/Visualisation/FlashyBox.cs index b093cb1d5..f153525d9 100644 --- a/osu.Framework/Graphics/Visualisation/FlashyBox.cs +++ b/osu.Framework/Graphics/Visualisation/FlashyBox.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using System; - -namespace osu.Framework.Graphics.Visualisation -{ - internal class FlashyBox : Box - { - private Drawable target; - private readonly Func getScreenSpaceQuad; - - public FlashyBox(Func getScreenSpaceQuad) - { - this.getScreenSpaceQuad = getScreenSpaceQuad; - } - - public Drawable Target - { - set { target = value; } - } - - public override Quad ScreenSpaceDrawQuad => target == null ? new Quad() : getScreenSpaceQuad(target); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using System; + +namespace osu.Framework.Graphics.Visualisation +{ + internal class FlashyBox : Box + { + private Drawable target; + private readonly Func getScreenSpaceQuad; + + public FlashyBox(Func getScreenSpaceQuad) + { + this.getScreenSpaceQuad = getScreenSpaceQuad; + } + + public Drawable Target + { + set { target = value; } + } + + public override Quad ScreenSpaceDrawQuad => target == null ? new Quad() : getScreenSpaceQuad(target); + } +} diff --git a/osu.Framework/Graphics/Visualisation/InfoOverlay.cs b/osu.Framework/Graphics/Visualisation/InfoOverlay.cs index bb6ab4b52..56b332756 100644 --- a/osu.Framework/Graphics/Visualisation/InfoOverlay.cs +++ b/osu.Framework/Graphics/Visualisation/InfoOverlay.cs @@ -1,93 +1,93 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Graphics.Visualisation -{ - internal class InfoOverlay : Container - { - private Drawable target; - - public Drawable Target - { - get { return target; } - - set - { - if (target == value) return; - target = value; - - foreach (FlashyBox c in Children) - c.Target = target; - - Alpha = target != null ? 1.0f : 0.0f; - - Pulse(); - } - } - - private static Quad quadAroundPosition(Vector2 pos, float sideLength) - { - Vector2 size = new Vector2(sideLength); - return new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y); - } - - private readonly FlashyBox layout; - private readonly FlashyBox shape; - private readonly FlashyBox childShape; - - public InfoOverlay() - { - RelativeSizeAxes = Axes.Both; - - Children = new[] - { - layout = new FlashyBox(d => d.ToScreenSpace(d.LayoutRectangle)) - { - Colour = Color4.Green, - Alpha = 0.5f, - }, - shape = new FlashyBox(d => d.ScreenSpaceDrawQuad) - { - Colour = Color4.Blue, - Alpha = 0.5f, - }, - childShape = new FlashyBox(delegate(Drawable d) - { - var c = d as CompositeDrawable; - if (c == null) - return d.ScreenSpaceDrawQuad; - - RectangleF rect = new RectangleF(c.ChildOffset, c.ChildSize); - return d.ToScreenSpace(rect); - }) - { - Colour = Color4.Red, - Alpha = 0.5f, - }, - // We're adding this guy twice to get a border in a somewhat hacky way. - new FlashyBox(d => quadAroundPosition(d.ToScreenSpace(d.OriginPosition), 5)) { Colour = Color4.Blue, }, - new FlashyBox(d => quadAroundPosition(d.ToScreenSpace(d.OriginPosition), 3)) { Colour = Color4.Yellow, }, - }; - } - - public void Pulse() - { - layout.FlashColour(Color4.White, 250); - shape.FlashColour(Color4.White, 250); - childShape.FlashColour(Color4.White, 250); - } - - protected override void Update() - { - base.Update(); - - foreach (FlashyBox c in Children) - c.Invalidate(Invalidation.DrawNode); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Graphics.Visualisation +{ + internal class InfoOverlay : Container + { + private Drawable target; + + public Drawable Target + { + get { return target; } + + set + { + if (target == value) return; + target = value; + + foreach (FlashyBox c in Children) + c.Target = target; + + Alpha = target != null ? 1.0f : 0.0f; + + Pulse(); + } + } + + private static Quad quadAroundPosition(Vector2 pos, float sideLength) + { + Vector2 size = new Vector2(sideLength); + return new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y); + } + + private readonly FlashyBox layout; + private readonly FlashyBox shape; + private readonly FlashyBox childShape; + + public InfoOverlay() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + layout = new FlashyBox(d => d.ToScreenSpace(d.LayoutRectangle)) + { + Colour = Color4.Green, + Alpha = 0.5f, + }, + shape = new FlashyBox(d => d.ScreenSpaceDrawQuad) + { + Colour = Color4.Blue, + Alpha = 0.5f, + }, + childShape = new FlashyBox(delegate(Drawable d) + { + var c = d as CompositeDrawable; + if (c == null) + return d.ScreenSpaceDrawQuad; + + RectangleF rect = new RectangleF(c.ChildOffset, c.ChildSize); + return d.ToScreenSpace(rect); + }) + { + Colour = Color4.Red, + Alpha = 0.5f, + }, + // We're adding this guy twice to get a border in a somewhat hacky way. + new FlashyBox(d => quadAroundPosition(d.ToScreenSpace(d.OriginPosition), 5)) { Colour = Color4.Blue, }, + new FlashyBox(d => quadAroundPosition(d.ToScreenSpace(d.OriginPosition), 3)) { Colour = Color4.Yellow, }, + }; + } + + public void Pulse() + { + layout.FlashColour(Color4.White, 250); + shape.FlashColour(Color4.White, 250); + childShape.FlashColour(Color4.White, 250); + } + + protected override void Update() + { + base.Update(); + + foreach (FlashyBox c in Children) + c.Invalidate(Invalidation.DrawNode); + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/LogOverlay.cs b/osu.Framework/Graphics/Visualisation/LogOverlay.cs index a84a8eae6..837a58d8a 100644 --- a/osu.Framework/Graphics/Visualisation/LogOverlay.cs +++ b/osu.Framework/Graphics/Visualisation/LogOverlay.cs @@ -1,232 +1,232 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Input; -using osu.Framework.Timing; -using OpenTK.Input; -using osu.Framework.Graphics.Shapes; - -namespace osu.Framework.Graphics.Visualisation -{ - internal class LogOverlay : OverlayContainer - { - private readonly FillFlowContainer flow; - - protected override bool BlockPassThroughMouse => false; - - private Bindable enabled; - - private StopwatchClock clock; - - private readonly Box box; - - private const float background_alpha = 0.6f; - - public LogOverlay() - { - //todo: use Input as font - - Width = 700; - AutoSizeAxes = Axes.Y; - - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - - Margin = new MarginPadding(1); - - Masking = true; - - Children = new Drawable[] - { - box = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = background_alpha, - }, - flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }; - } - - protected override void LoadComplete() - { - // custom clock is used to adjust log display speed (to freeze log display with a key). - Clock = new FramedClock(clock = new StopwatchClock(true)); - - base.LoadComplete(); - - addEntry(new LogEntry - { - Level = LogLevel.Important, - Message = "The debug log overlay is currently being displayed. You can toggle with Ctrl+F10 at any point.", - Target = LoggingTarget.Information, - }); - } - - private void addEntry(LogEntry entry) - { -#if !DEBUG - if (entry.Level <= LogLevel.Verbose) - return; -#endif - - Schedule(() => - { - const int display_length = 4000; - - LoadComponentAsync(new DrawableLogEntry(entry), drawEntry => - { - flow.Add(drawEntry); - - drawEntry.FadeInFromZero(800, Easing.OutQuint).Delay(display_length).FadeOut(800, Easing.InQuint); - drawEntry.Expire(); - }); - }); - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (!args.Repeat) - setHoldState(args.Key == Key.ControlLeft || args.Key == Key.ControlRight); - - return base.OnKeyDown(state, args); - } - - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) - { - if (!state.Keyboard.ControlPressed) - setHoldState(false); - return base.OnKeyUp(state, args); - } - - private void setHoldState(bool controlPressed) - { - box.Alpha = controlPressed ? 1 : background_alpha; - clock.Rate = controlPressed ? 0 : 1; - } - - [BackgroundDependencyLoader] - private void load(FrameworkConfigManager config) - { - enabled = config.GetBindable(FrameworkSetting.ShowLogOverlay); - enabled.ValueChanged += val => State = val ? Visibility.Visible : Visibility.Hidden; - enabled.TriggerChange(); - } - - protected override void PopIn() - { - Logger.NewEntry += addEntry; - enabled.Value = true; - this.FadeIn(100); - } - - protected override void PopOut() - { - Logger.NewEntry -= addEntry; - setHoldState(false); - enabled.Value = false; - this.FadeOut(100); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - Logger.NewEntry -= addEntry; - } - } - - internal class DrawableLogEntry : Container - { - private const float target_box_width = 65; - - private const float font_size = 14; - - public override bool HandleKeyboardInput => false; - public override bool HandleMouseInput => false; - - public DrawableLogEntry(LogEntry entry) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Color4 col = getColourForEntry(entry); - - Children = new Drawable[] - { - new Container - { - //log target coloured box - Margin = new MarginPadding(3), - Size = new Vector2(target_box_width, font_size), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - CornerRadius = 5, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = col, - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = true, - ShadowColour = Color4.Black, - Margin = new MarginPadding { Left = 5, Right = 5 }, - TextSize = font_size, - Text = entry.Target?.ToString() ?? entry.LoggerName, - } - } - }, - new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Left = target_box_width + 10 }, - Child = new SpriteText - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - TextSize = font_size, - Text = entry.Message - } - } - }; - } - - private Color4 getColourForEntry(LogEntry entry) - { - switch (entry.Target) - { - case LoggingTarget.Runtime: - return Color4.YellowGreen; - case LoggingTarget.Network: - return Color4.BlueViolet; - case LoggingTarget.Performance: - return Color4.HotPink; - case LoggingTarget.Debug: - return Color4.DarkBlue; - case LoggingTarget.Information: - return Color4.CadetBlue; - default: - return Color4.Cyan; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Input; +using osu.Framework.Timing; +using OpenTK.Input; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Graphics.Visualisation +{ + internal class LogOverlay : OverlayContainer + { + private readonly FillFlowContainer flow; + + protected override bool BlockPassThroughMouse => false; + + private Bindable enabled; + + private StopwatchClock clock; + + private readonly Box box; + + private const float background_alpha = 0.6f; + + public LogOverlay() + { + //todo: use Input as font + + Width = 700; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + Margin = new MarginPadding(1); + + Masking = true; + + Children = new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = background_alpha, + }, + flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }; + } + + protected override void LoadComplete() + { + // custom clock is used to adjust log display speed (to freeze log display with a key). + Clock = new FramedClock(clock = new StopwatchClock(true)); + + base.LoadComplete(); + + addEntry(new LogEntry + { + Level = LogLevel.Important, + Message = "The debug log overlay is currently being displayed. You can toggle with Ctrl+F10 at any point.", + Target = LoggingTarget.Information, + }); + } + + private void addEntry(LogEntry entry) + { +#if !DEBUG + if (entry.Level <= LogLevel.Verbose) + return; +#endif + + Schedule(() => + { + const int display_length = 4000; + + LoadComponentAsync(new DrawableLogEntry(entry), drawEntry => + { + flow.Add(drawEntry); + + drawEntry.FadeInFromZero(800, Easing.OutQuint).Delay(display_length).FadeOut(800, Easing.InQuint); + drawEntry.Expire(); + }); + }); + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!args.Repeat) + setHoldState(args.Key == Key.ControlLeft || args.Key == Key.ControlRight); + + return base.OnKeyDown(state, args); + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + if (!state.Keyboard.ControlPressed) + setHoldState(false); + return base.OnKeyUp(state, args); + } + + private void setHoldState(bool controlPressed) + { + box.Alpha = controlPressed ? 1 : background_alpha; + clock.Rate = controlPressed ? 0 : 1; + } + + [BackgroundDependencyLoader] + private void load(FrameworkConfigManager config) + { + enabled = config.GetBindable(FrameworkSetting.ShowLogOverlay); + enabled.ValueChanged += val => State = val ? Visibility.Visible : Visibility.Hidden; + enabled.TriggerChange(); + } + + protected override void PopIn() + { + Logger.NewEntry += addEntry; + enabled.Value = true; + this.FadeIn(100); + } + + protected override void PopOut() + { + Logger.NewEntry -= addEntry; + setHoldState(false); + enabled.Value = false; + this.FadeOut(100); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Logger.NewEntry -= addEntry; + } + } + + internal class DrawableLogEntry : Container + { + private const float target_box_width = 65; + + private const float font_size = 14; + + public override bool HandleKeyboardInput => false; + public override bool HandleMouseInput => false; + + public DrawableLogEntry(LogEntry entry) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Color4 col = getColourForEntry(entry); + + Children = new Drawable[] + { + new Container + { + //log target coloured box + Margin = new MarginPadding(3), + Size = new Vector2(target_box_width, font_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = col, + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = true, + ShadowColour = Color4.Black, + Margin = new MarginPadding { Left = 5, Right = 5 }, + TextSize = font_size, + Text = entry.Target?.ToString() ?? entry.LoggerName, + } + } + }, + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = target_box_width + 10 }, + Child = new SpriteText + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + TextSize = font_size, + Text = entry.Message + } + } + }; + } + + private Color4 getColourForEntry(LogEntry entry) + { + switch (entry.Target) + { + case LoggingTarget.Runtime: + return Color4.YellowGreen; + case LoggingTarget.Network: + return Color4.BlueViolet; + case LoggingTarget.Performance: + return Color4.HotPink; + case LoggingTarget.Debug: + return Color4.DarkBlue; + case LoggingTarget.Information: + return Color4.CadetBlue; + default: + return Color4.Cyan; + } + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/PropertyDisplay.cs b/osu.Framework/Graphics/Visualisation/PropertyDisplay.cs index d8292d01a..de9b0734e 100644 --- a/osu.Framework/Graphics/Visualisation/PropertyDisplay.cs +++ b/osu.Framework/Graphics/Visualisation/PropertyDisplay.cs @@ -1,188 +1,188 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Extensions.TypeExtensions; - -namespace osu.Framework.Graphics.Visualisation -{ - internal class PropertyDisplay : VisibilityContainer - { - private readonly FillFlowContainer flow; - - private const float width = 600; - - protected override Container Content => flow; - - public PropertyDisplay() - { - Width = width; - RelativeSizeAxes = Axes.Y; - - AddInternal(new ScrollContainer - { - Padding = new MarginPadding(10), - RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical - } - }); - } - - public void UpdateFrom(Drawable source) - { - Clear(); - - if (source == null) - return; - - Type type = source.GetType(); - - var properties = (IEnumerable)type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - // Only properties which we can read - .Where(p => p.CanRead); - - var fields = (IEnumerable)type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - // Exclude the backing fields of properties - .Where(f => f.GetCustomAttribute() == null); - - // Upper, then lower-case - var allMembers = properties.Concat(fields).OrderBy(m => (int)m.Name[0]).ThenBy(m => m.Name); - - AddRange(allMembers.Select(member => new PropertyItem(member, source))); - } - - protected override void PopIn() - { - this.ResizeWidthTo(width, 500, Easing.OutQuint); - } - - protected override void PopOut() - { - this.ResizeWidthTo(0, 500, Easing.OutQuint); - } - - private class PropertyItem : Container - { - private readonly SpriteText valueText; - private readonly Box changeMarker; - private readonly Func getValue; - - public PropertyItem(MemberInfo info, IDrawable d) - { - Type type; - switch (info.MemberType) - { - case MemberTypes.Property: - PropertyInfo propertyInfo = (PropertyInfo)info; - type = propertyInfo.PropertyType; - getValue = () => propertyInfo.GetValue(d); - break; - - case MemberTypes.Field: - FieldInfo fieldInfo = (FieldInfo)info; - type = fieldInfo.FieldType; - getValue = () => fieldInfo.GetValue(d); - break; - - default: - throw new NotImplementedException(@"Not a value member."); - } - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - AddRangeInternal(new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Right = 6 - }, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f), - Children = new[] - { - new SpriteText - { - Text = info.Name, - Colour = Color4.LightBlue, - }, - new SpriteText - { - Text = $@"[{type.Name}]:", - Colour = Color4.MediumPurple, - }, - valueText = new SpriteText - { - Colour = Color4.White, - }, - } - } - }, - changeMarker = new Box - { - Size = new Vector2(4, 18), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Colour = Color4.Red - } - }); - - // Update the value once - updateValue(); - } - - protected override void Update() - { - base.Update(); - updateValue(); - } - - private object lastValue; - - private void updateValue() - { - object value; - try - { - value = getValue() ?? ""; - } - catch (Exception e) - { - value = $@"<{((e as TargetInvocationException)?.InnerException ?? e).GetType().ReadableName()} occured during evaluation>"; - } - - if (!value.Equals(lastValue)) - { - changeMarker.ClearTransforms(); - changeMarker.Alpha = 0.8f; - changeMarker.FadeOut(200); - } - - lastValue = value; - valueText.Text = value.ToString(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Framework.Graphics.Visualisation +{ + internal class PropertyDisplay : VisibilityContainer + { + private readonly FillFlowContainer flow; + + private const float width = 600; + + protected override Container Content => flow; + + public PropertyDisplay() + { + Width = width; + RelativeSizeAxes = Axes.Y; + + AddInternal(new ScrollContainer + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + } + }); + } + + public void UpdateFrom(Drawable source) + { + Clear(); + + if (source == null) + return; + + Type type = source.GetType(); + + var properties = (IEnumerable)type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + // Only properties which we can read + .Where(p => p.CanRead); + + var fields = (IEnumerable)type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + // Exclude the backing fields of properties + .Where(f => f.GetCustomAttribute() == null); + + // Upper, then lower-case + var allMembers = properties.Concat(fields).OrderBy(m => (int)m.Name[0]).ThenBy(m => m.Name); + + AddRange(allMembers.Select(member => new PropertyItem(member, source))); + } + + protected override void PopIn() + { + this.ResizeWidthTo(width, 500, Easing.OutQuint); + } + + protected override void PopOut() + { + this.ResizeWidthTo(0, 500, Easing.OutQuint); + } + + private class PropertyItem : Container + { + private readonly SpriteText valueText; + private readonly Box changeMarker; + private readonly Func getValue; + + public PropertyItem(MemberInfo info, IDrawable d) + { + Type type; + switch (info.MemberType) + { + case MemberTypes.Property: + PropertyInfo propertyInfo = (PropertyInfo)info; + type = propertyInfo.PropertyType; + getValue = () => propertyInfo.GetValue(d); + break; + + case MemberTypes.Field: + FieldInfo fieldInfo = (FieldInfo)info; + type = fieldInfo.FieldType; + getValue = () => fieldInfo.GetValue(d); + break; + + default: + throw new NotImplementedException(@"Not a value member."); + } + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddRangeInternal(new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Right = 6 + }, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f), + Children = new[] + { + new SpriteText + { + Text = info.Name, + Colour = Color4.LightBlue, + }, + new SpriteText + { + Text = $@"[{type.Name}]:", + Colour = Color4.MediumPurple, + }, + valueText = new SpriteText + { + Colour = Color4.White, + }, + } + } + }, + changeMarker = new Box + { + Size = new Vector2(4, 18), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Colour = Color4.Red + } + }); + + // Update the value once + updateValue(); + } + + protected override void Update() + { + base.Update(); + updateValue(); + } + + private object lastValue; + + private void updateValue() + { + object value; + try + { + value = getValue() ?? ""; + } + catch (Exception e) + { + value = $@"<{((e as TargetInvocationException)?.InnerException ?? e).GetType().ReadableName()} occured during evaluation>"; + } + + if (!value.Equals(lastValue)) + { + changeMarker.ClearTransforms(); + changeMarker.Alpha = 0.8f; + changeMarker.FadeOut(200); + } + + lastValue = value; + valueText.Text = value.ToString(); + } + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/TreeContainer.cs b/osu.Framework/Graphics/Visualisation/TreeContainer.cs index 81085283b..e9b8ad33e 100644 --- a/osu.Framework/Graphics/Visualisation/TreeContainer.cs +++ b/osu.Framework/Graphics/Visualisation/TreeContainer.cs @@ -1,230 +1,230 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Framework.Graphics.Visualisation -{ - internal enum TreeContainerStatus - { - Onscreen, - Offscreen - } - - internal class TreeContainer : Container, IStateful - { - private readonly ScrollContainer scroll; - - private readonly SpriteText waitingText; - - public Action ChooseTarget; - public Action GoUpOneParent; - public Action ToggleProperties; - - protected override Container Content => scroll; - - private readonly Container titleBar; - - private const float width = 400; - private const float height = 600; - - internal PropertyDisplay PropertyDisplay { get; private set; } - - private TreeContainerStatus state; - - public event Action StateChanged; - - public TreeContainerStatus State - { - get { return state; } - - set - { - if (state == value) - return; - state = value; - - switch (state) - { - case TreeContainerStatus.Offscreen: - this.Delay(500).FadeTo(0.7f, 300); - break; - case TreeContainerStatus.Onscreen: - this.FadeIn(300, Easing.OutQuint); - break; - } - - StateChanged?.Invoke(State); - } - } - - public TreeContainer() - { - Masking = true; - CornerRadius = 5; - Position = new Vector2(100, 100); - - AutoSizeAxes = Axes.X; - Height = height; - - Color4 buttonBackground = new Color4(50, 50, 50, 255); - Color4 buttonBackgroundHighlighted = new Color4(80, 80, 80, 255); - const float button_width = width / 3 - 1; - - Button propertyButton; - - AddRangeInternal(new Drawable[] - { - new Box - { - Colour = new Color4(15, 15, 15, 255), - RelativeSizeAxes = Axes.Both, - Depth = 0 - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - titleBar = new Container - { - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, 25), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.BlueViolet, - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "draw visualiser (Ctrl+F1 to toggle)", - Alpha = 0.8f, - }, - } - }, - new Container //toolbar - { - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, 40), - Children = new Drawable[] - { - new Box - { - Colour = new Color4(20, 20, 20, 255), - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(1), - Children = new Drawable[] - { - new Button - { - BackgroundColour = buttonBackground, - Size = new Vector2(button_width, 1), - RelativeSizeAxes = Axes.Y, - Text = @"choose target", - Action = delegate { ChooseTarget?.Invoke(); } - }, - new Button - { - BackgroundColour = buttonBackground, - Size = new Vector2(button_width, 1), - RelativeSizeAxes = Axes.Y, - Text = @"up one parent", - Action = delegate { GoUpOneParent?.Invoke(); }, - }, - propertyButton = new Button - { - BackgroundColour = buttonBackground, - Size = new Vector2(button_width, 1), - RelativeSizeAxes = Axes.Y, - Text = @"view properties", - Action = delegate { ToggleProperties?.Invoke(); }, - }, - }, - }, - }, - }, - }, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Top = 65 }, - Children = new Drawable[] - { - scroll = new ScrollContainer - { - Padding = new MarginPadding(10), - RelativeSizeAxes = Axes.Y, - Width = width - }, - PropertyDisplay = new PropertyDisplay() - } - }, - waitingText = new SpriteText - { - Text = @"Waiting for target selection...", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }); - - PropertyDisplay.StateChanged += v => propertyButton.BackgroundColour = v == Visibility.Visible ? buttonBackgroundHighlighted : buttonBackground; - } - - protected override void Update() - { - waitingText.Alpha = scroll.Children.Any() ? 0 : 1; - base.Update(); - } - - protected override bool OnHover(InputState state) - { - State = TreeContainerStatus.Onscreen; - return true; - } - - protected override void OnHoverLost(InputState state) - { - State = TreeContainerStatus.Offscreen; - base.OnHoverLost(state); - } - - protected override bool OnDragStart(InputState state) => titleBar.ReceiveMouseInputAt(state.Mouse.NativeState.Position); - - protected override bool OnDrag(InputState state) - { - Position += state.Mouse.Delta; - return base.OnDrag(state); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; - - protected override bool OnClick(InputState state) => true; - - protected override void LoadComplete() - { - base.LoadComplete(); - State = TreeContainerStatus.Offscreen; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Graphics.Visualisation +{ + internal enum TreeContainerStatus + { + Onscreen, + Offscreen + } + + internal class TreeContainer : Container, IStateful + { + private readonly ScrollContainer scroll; + + private readonly SpriteText waitingText; + + public Action ChooseTarget; + public Action GoUpOneParent; + public Action ToggleProperties; + + protected override Container Content => scroll; + + private readonly Container titleBar; + + private const float width = 400; + private const float height = 600; + + internal PropertyDisplay PropertyDisplay { get; private set; } + + private TreeContainerStatus state; + + public event Action StateChanged; + + public TreeContainerStatus State + { + get { return state; } + + set + { + if (state == value) + return; + state = value; + + switch (state) + { + case TreeContainerStatus.Offscreen: + this.Delay(500).FadeTo(0.7f, 300); + break; + case TreeContainerStatus.Onscreen: + this.FadeIn(300, Easing.OutQuint); + break; + } + + StateChanged?.Invoke(State); + } + } + + public TreeContainer() + { + Masking = true; + CornerRadius = 5; + Position = new Vector2(100, 100); + + AutoSizeAxes = Axes.X; + Height = height; + + Color4 buttonBackground = new Color4(50, 50, 50, 255); + Color4 buttonBackgroundHighlighted = new Color4(80, 80, 80, 255); + const float button_width = width / 3 - 1; + + Button propertyButton; + + AddRangeInternal(new Drawable[] + { + new Box + { + Colour = new Color4(15, 15, 15, 255), + RelativeSizeAxes = Axes.Both, + Depth = 0 + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + titleBar = new Container + { + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 25), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.BlueViolet, + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "draw visualiser (Ctrl+F1 to toggle)", + Alpha = 0.8f, + }, + } + }, + new Container //toolbar + { + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40), + Children = new Drawable[] + { + new Box + { + Colour = new Color4(20, 20, 20, 255), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(1), + Children = new Drawable[] + { + new Button + { + BackgroundColour = buttonBackground, + Size = new Vector2(button_width, 1), + RelativeSizeAxes = Axes.Y, + Text = @"choose target", + Action = delegate { ChooseTarget?.Invoke(); } + }, + new Button + { + BackgroundColour = buttonBackground, + Size = new Vector2(button_width, 1), + RelativeSizeAxes = Axes.Y, + Text = @"up one parent", + Action = delegate { GoUpOneParent?.Invoke(); }, + }, + propertyButton = new Button + { + BackgroundColour = buttonBackground, + Size = new Vector2(button_width, 1), + RelativeSizeAxes = Axes.Y, + Text = @"view properties", + Action = delegate { ToggleProperties?.Invoke(); }, + }, + }, + }, + }, + }, + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Top = 65 }, + Children = new Drawable[] + { + scroll = new ScrollContainer + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Y, + Width = width + }, + PropertyDisplay = new PropertyDisplay() + } + }, + waitingText = new SpriteText + { + Text = @"Waiting for target selection...", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + PropertyDisplay.StateChanged += v => propertyButton.BackgroundColour = v == Visibility.Visible ? buttonBackgroundHighlighted : buttonBackground; + } + + protected override void Update() + { + waitingText.Alpha = scroll.Children.Any() ? 0 : 1; + base.Update(); + } + + protected override bool OnHover(InputState state) + { + State = TreeContainerStatus.Onscreen; + return true; + } + + protected override void OnHoverLost(InputState state) + { + State = TreeContainerStatus.Offscreen; + base.OnHoverLost(state); + } + + protected override bool OnDragStart(InputState state) => titleBar.ReceiveMouseInputAt(state.Mouse.NativeState.Position); + + protected override bool OnDrag(InputState state) + { + Position += state.Mouse.Delta; + return base.OnDrag(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; + + protected override bool OnClick(InputState state) => true; + + protected override void LoadComplete() + { + base.LoadComplete(); + State = TreeContainerStatus.Offscreen; + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs b/osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs index 3f8b23496..73c1bbbed 100644 --- a/osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs +++ b/osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs @@ -1,325 +1,325 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using OpenTK.Input; -using osu.Framework.Graphics.Shapes; -using System.Collections.Generic; -using osu.Framework.Extensions.IEnumerableExtensions; - -namespace osu.Framework.Graphics.Visualisation -{ - internal class VisualisedDrawable : Container - { - public Drawable Target { get; } - - private bool isHighlighted; - - public bool IsHighlighted - { - get - { - return isHighlighted; - } - set - { - isHighlighted = value; - - if (value) - { - highlightBackground.FadeIn(); - Expand(); - } - else - { - highlightBackground.FadeOut(); - } - } - } - - private readonly Box background; - private readonly Box highlightBackground; - private readonly SpriteText text; - private readonly Drawable previewBox; - private readonly Drawable activityInvalidate; - private readonly Drawable activityAutosize; - private readonly Drawable activityLayout; - - public Action RequestTarget; - public Action HighlightTarget; - - private const int line_height = 12; - - private readonly FillFlowContainer flow; - private readonly TreeContainer tree; - - public VisualisedDrawable(Drawable d, TreeContainer tree) - { - this.tree = tree; - - Target = d; - - attachEvents(); - - var sprite = Target as Sprite; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - AddRange(new[] - { - activityInvalidate = new Box - { - Colour = Color4.Yellow, - Size = new Vector2(2, line_height), - Position = new Vector2(6, 0), - Alpha = 0 - }, - activityLayout = new Box - { - Colour = Color4.Orange, - Size = new Vector2(2, line_height), - Position = new Vector2(3, 0), - Alpha = 0 - }, - activityAutosize = new Box - { - Colour = Color4.Red, - Size = new Vector2(2, line_height), - Position = new Vector2(0, 0), - Alpha = 0 - }, - previewBox = sprite?.Texture == null - ? previewBox = new Box { Colour = Color4.White } - : new Sprite - { - Texture = sprite.Texture, - Scale = new Vector2(sprite.Texture.DisplayWidth / sprite.Texture.DisplayHeight, 1), - }, - new Container - { - AutoSizeAxes = Axes.Both, - Position = new Vector2(24, -3), - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1, 0.8f), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.Transparent, - }, - highlightBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1, 0.8f), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.Khaki.Opacity(0.4f), - Alpha = 0 - }, - text = new SpriteText() - } - }, - flow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Position = new Vector2(10, 14) - }, - }); - - previewBox.Position = new Vector2(9, 0); - previewBox.Size = new Vector2(line_height, line_height); - - var compositeTarget = Target as CompositeDrawable; - compositeTarget?.AliveInternalChildren.ForEach(addChild); - - updateSpecifics(); - } - - private void attachEvents() - { - Target.OnInvalidate += onInvalidate; - - var da = Target as Container; - if (da != null) - { - da.OnAutoSize += onAutoSize; - da.ChildBecameAlive += addChild; - da.ChildDied += removeChild; - } - - var df = Target as FlowContainer; - if (df != null) df.OnLayout += onLayout; - } - - private void detachEvents() - { - Target.OnInvalidate -= onInvalidate; - - var da = Target as Container; - if (da != null) - { - da.OnAutoSize -= onAutoSize; - da.ChildBecameAlive -= addChild; - da.ChildDied -= removeChild; - } - - var df = Target as FlowContainer; - if (df != null) df.OnLayout -= onLayout; - } - - private readonly Dictionary visCache = new Dictionary(); - private void addChild(Drawable drawable) - { - // Make sure to never add the DrawVisualiser (recursive scenario) - if (drawable is DrawVisualiser) return; - - // Don't add individual characters of SpriteText - if (Target is SpriteText) return; - - if (!visCache.TryGetValue(drawable, out VisualisedDrawable vis)) - { - vis = visCache[drawable] = new VisualisedDrawable(drawable, tree) - { - RequestTarget = d => RequestTarget?.Invoke(d), - HighlightTarget = d => HighlightTarget?.Invoke(d) - }; - } - - flow.Add(vis); - } - - private void removeChild(Drawable drawable) - { - if (!visCache.ContainsKey(drawable)) - return; - flow.Remove(visCache[drawable]); - } - - public VisualisedDrawable FindVisualisedDrawable(Drawable drawable) - { - if (drawable == Target) - return this; - - foreach (var child in flow) - { - var vis = child.FindVisualisedDrawable(drawable); - if (vis != null) - return vis; - } - - return null; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - detachEvents(); - } - - protected override bool OnHover(InputState state) - { - background.Colour = Color4.PaleVioletRed.Opacity(0.7f); - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - background.Colour = Color4.Transparent; - base.OnHoverLost(state); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - if (args.Button == MouseButton.Right) - { - HighlightTarget?.Invoke(this); - return true; - } - return false; - } - - protected override bool OnClick(InputState state) - { - if (isExpanded) - Collapse(); - else Expand(); - - return true; - } - - protected override bool OnDoubleClick(InputState state) - { - RequestTarget?.Invoke(Target); - return true; - } - - private bool isExpanded = true; - - public void Expand() - { - flow.FadeIn(); - updateSpecifics(); - - isExpanded = true; - } - - public void Collapse() - { - flow.FadeOut(); - updateSpecifics(); - - isExpanded = false; - } - - private void onAutoSize() - { - Scheduler.Add(() => activityAutosize.FadeOutFromOne(1)); - } - - private void onLayout() - { - Scheduler.Add(() => activityLayout.FadeOutFromOne(1)); - } - - private void onInvalidate(Drawable d) - { - Scheduler.Add(() => activityInvalidate.FadeOutFromOne(1)); - } - - private void updateSpecifics() - { - Vector2 posInTree = ToSpaceOfOtherDrawable(Vector2.Zero, tree); - if (posInTree.Y < -previewBox.DrawHeight || posInTree.Y > tree.Height) - { - text.Text = string.Empty; - return; - } - - previewBox.Alpha = Math.Max(0.2f, Target.Alpha); - previewBox.Colour = Target.Colour; - - int childCount = (Target as CompositeDrawable)?.InternalChildren.Count ?? 0; - - text.Text = Target + (!isExpanded && childCount > 0 ? $@" ({childCount} children)" : string.Empty); - text.Colour = !isExpanded && childCount > 0 ? Color4.LightBlue : Color4.White; - - Alpha = Target.IsPresent ? 1 : 0.3f; - } - - protected override void Update() - { - updateSpecifics(); - base.Update(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using OpenTK.Input; +using osu.Framework.Graphics.Shapes; +using System.Collections.Generic; +using osu.Framework.Extensions.IEnumerableExtensions; + +namespace osu.Framework.Graphics.Visualisation +{ + internal class VisualisedDrawable : Container + { + public Drawable Target { get; } + + private bool isHighlighted; + + public bool IsHighlighted + { + get + { + return isHighlighted; + } + set + { + isHighlighted = value; + + if (value) + { + highlightBackground.FadeIn(); + Expand(); + } + else + { + highlightBackground.FadeOut(); + } + } + } + + private readonly Box background; + private readonly Box highlightBackground; + private readonly SpriteText text; + private readonly Drawable previewBox; + private readonly Drawable activityInvalidate; + private readonly Drawable activityAutosize; + private readonly Drawable activityLayout; + + public Action RequestTarget; + public Action HighlightTarget; + + private const int line_height = 12; + + private readonly FillFlowContainer flow; + private readonly TreeContainer tree; + + public VisualisedDrawable(Drawable d, TreeContainer tree) + { + this.tree = tree; + + Target = d; + + attachEvents(); + + var sprite = Target as Sprite; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + AddRange(new[] + { + activityInvalidate = new Box + { + Colour = Color4.Yellow, + Size = new Vector2(2, line_height), + Position = new Vector2(6, 0), + Alpha = 0 + }, + activityLayout = new Box + { + Colour = Color4.Orange, + Size = new Vector2(2, line_height), + Position = new Vector2(3, 0), + Alpha = 0 + }, + activityAutosize = new Box + { + Colour = Color4.Red, + Size = new Vector2(2, line_height), + Position = new Vector2(0, 0), + Alpha = 0 + }, + previewBox = sprite?.Texture == null + ? previewBox = new Box { Colour = Color4.White } + : new Sprite + { + Texture = sprite.Texture, + Scale = new Vector2(sprite.Texture.DisplayWidth / sprite.Texture.DisplayHeight, 1), + }, + new Container + { + AutoSizeAxes = Axes.Both, + Position = new Vector2(24, -3), + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.8f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.Transparent, + }, + highlightBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.8f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.Khaki.Opacity(0.4f), + Alpha = 0 + }, + text = new SpriteText() + } + }, + flow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Position = new Vector2(10, 14) + }, + }); + + previewBox.Position = new Vector2(9, 0); + previewBox.Size = new Vector2(line_height, line_height); + + var compositeTarget = Target as CompositeDrawable; + compositeTarget?.AliveInternalChildren.ForEach(addChild); + + updateSpecifics(); + } + + private void attachEvents() + { + Target.OnInvalidate += onInvalidate; + + var da = Target as Container; + if (da != null) + { + da.OnAutoSize += onAutoSize; + da.ChildBecameAlive += addChild; + da.ChildDied += removeChild; + } + + var df = Target as FlowContainer; + if (df != null) df.OnLayout += onLayout; + } + + private void detachEvents() + { + Target.OnInvalidate -= onInvalidate; + + var da = Target as Container; + if (da != null) + { + da.OnAutoSize -= onAutoSize; + da.ChildBecameAlive -= addChild; + da.ChildDied -= removeChild; + } + + var df = Target as FlowContainer; + if (df != null) df.OnLayout -= onLayout; + } + + private readonly Dictionary visCache = new Dictionary(); + private void addChild(Drawable drawable) + { + // Make sure to never add the DrawVisualiser (recursive scenario) + if (drawable is DrawVisualiser) return; + + // Don't add individual characters of SpriteText + if (Target is SpriteText) return; + + if (!visCache.TryGetValue(drawable, out VisualisedDrawable vis)) + { + vis = visCache[drawable] = new VisualisedDrawable(drawable, tree) + { + RequestTarget = d => RequestTarget?.Invoke(d), + HighlightTarget = d => HighlightTarget?.Invoke(d) + }; + } + + flow.Add(vis); + } + + private void removeChild(Drawable drawable) + { + if (!visCache.ContainsKey(drawable)) + return; + flow.Remove(visCache[drawable]); + } + + public VisualisedDrawable FindVisualisedDrawable(Drawable drawable) + { + if (drawable == Target) + return this; + + foreach (var child in flow) + { + var vis = child.FindVisualisedDrawable(drawable); + if (vis != null) + return vis; + } + + return null; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + detachEvents(); + } + + protected override bool OnHover(InputState state) + { + background.Colour = Color4.PaleVioletRed.Opacity(0.7f); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + background.Colour = Color4.Transparent; + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (args.Button == MouseButton.Right) + { + HighlightTarget?.Invoke(this); + return true; + } + return false; + } + + protected override bool OnClick(InputState state) + { + if (isExpanded) + Collapse(); + else Expand(); + + return true; + } + + protected override bool OnDoubleClick(InputState state) + { + RequestTarget?.Invoke(Target); + return true; + } + + private bool isExpanded = true; + + public void Expand() + { + flow.FadeIn(); + updateSpecifics(); + + isExpanded = true; + } + + public void Collapse() + { + flow.FadeOut(); + updateSpecifics(); + + isExpanded = false; + } + + private void onAutoSize() + { + Scheduler.Add(() => activityAutosize.FadeOutFromOne(1)); + } + + private void onLayout() + { + Scheduler.Add(() => activityLayout.FadeOutFromOne(1)); + } + + private void onInvalidate(Drawable d) + { + Scheduler.Add(() => activityInvalidate.FadeOutFromOne(1)); + } + + private void updateSpecifics() + { + Vector2 posInTree = ToSpaceOfOtherDrawable(Vector2.Zero, tree); + if (posInTree.Y < -previewBox.DrawHeight || posInTree.Y > tree.Height) + { + text.Text = string.Empty; + return; + } + + previewBox.Alpha = Math.Max(0.2f, Target.Alpha); + previewBox.Colour = Target.Colour; + + int childCount = (Target as CompositeDrawable)?.InternalChildren.Count ?? 0; + + text.Text = Target + (!isExpanded && childCount > 0 ? $@" ({childCount} children)" : string.Empty); + text.Colour = !isExpanded && childCount > 0 ? Color4.LightBlue : Color4.White; + + Alpha = Target.IsPresent ? 1 : 0.3f; + } + + protected override void Update() + { + updateSpecifics(); + base.Update(); + } + } +} diff --git a/osu.Framework/Host.cs b/osu.Framework/Host.cs index 9fd00fcff..4e99c48c7 100644 --- a/osu.Framework/Host.cs +++ b/osu.Framework/Host.cs @@ -1,29 +1,29 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Platform; -using osu.Framework.Platform.Linux; -using osu.Framework.Platform.MacOS; -using osu.Framework.Platform.Windows; -using System; - -namespace osu.Framework -{ - public static class Host - { - public static DesktopGameHost GetSuitableHost(string gameName, bool bindIPC = false) - { - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.MacOsx: - return new MacOSGameHost(gameName, bindIPC); - case RuntimeInfo.Platform.Linux: - return new LinuxGameHost(gameName, bindIPC); - case RuntimeInfo.Platform.Windows: - return new WindowsGameHost(gameName, bindIPC); - default: - throw new InvalidOperationException($"Could not find a suitable host for the selected operating system ({Enum.GetName(typeof(RuntimeInfo.Platform), RuntimeInfo.OS)})."); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Platform; +using osu.Framework.Platform.Linux; +using osu.Framework.Platform.MacOS; +using osu.Framework.Platform.Windows; +using System; + +namespace osu.Framework +{ + public static class Host + { + public static DesktopGameHost GetSuitableHost(string gameName, bool bindIPC = false) + { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.MacOsx: + return new MacOSGameHost(gameName, bindIPC); + case RuntimeInfo.Platform.Linux: + return new LinuxGameHost(gameName, bindIPC); + case RuntimeInfo.Platform.Windows: + return new WindowsGameHost(gameName, bindIPC); + default: + throw new InvalidOperationException($"Could not find a suitable host for the selected operating system ({Enum.GetName(typeof(RuntimeInfo.Platform), RuntimeInfo.OS)})."); + } + } + } +} diff --git a/osu.Framework/IO/AsyncBufferStream.cs b/osu.Framework/IO/AsyncBufferStream.cs index d0e25cff4..c1f2e21e9 100644 --- a/osu.Framework/IO/AsyncBufferStream.cs +++ b/osu.Framework/IO/AsyncBufferStream.cs @@ -1,235 +1,235 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using OpenTK; - -namespace osu.Framework.IO -{ - internal class AsyncBufferStream : Stream - { - private const int block_size = 32768; - - #region Concurrent access - - private readonly byte[] data; - - private readonly bool[] blockLoadedStatus; - - private volatile bool isClosed; - private volatile bool isLoaded; - - private volatile int position; - private volatile int amountBytesToRead; - - #endregion - - private readonly int blocksToReadAhead; - - private readonly Stream underlyingStream; - - private CancellationTokenSource cancellationToken; - - /// - /// A stream that buffers the underlying stream to contiguous memory, reading until the whole file is eventually memory-backed. - /// - /// The underlying stream to read from. - /// The amount of blocks to read ahead of the read position. - /// Another AsyncBufferStream which is backing the same underlying stream. Allows shared usage of memory-backing. - public AsyncBufferStream(Stream stream, int blocksToReadAhead, AsyncBufferStream shared = null) - { - underlyingStream = stream ?? throw new ArgumentNullException(nameof(stream)); - this.blocksToReadAhead = blocksToReadAhead; - - if (underlyingStream.CanSeek) - underlyingStream.Seek(0, SeekOrigin.Begin); - - if (shared?.Length != stream.Length) - { - data = new byte[underlyingStream.Length]; - blockLoadedStatus = new bool[data.Length / block_size + 1]; - } - else - { - data = shared.data; - blockLoadedStatus = shared.blockLoadedStatus; - isLoaded = shared.isLoaded; - } - - cancellationToken = new CancellationTokenSource(); - Task.Factory.StartNew(loadRequiredBlocks, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - - ~AsyncBufferStream() - { - Dispose(false); - } - - private void loadRequiredBlocks() - { - if (isLoaded) - return; - - int last = -1; - while (!isLoaded && !isClosed) - { - cancellationToken.Token.ThrowIfCancellationRequested(); - - int curr = nextBlockToLoad; - if (curr < 0) - { - Thread.Sleep(1); - continue; - } - - int readStart = curr * block_size; - - if (last + 1 != curr) - { - //follow along with a seek. - Debug.Assert(underlyingStream.CanSeek); - underlyingStream.Seek(readStart, SeekOrigin.Begin); - } - - Trace.Assert(underlyingStream.Position == readStart); - - int readSize = Math.Min(data.Length - readStart, block_size); - int read = underlyingStream.Read(data, readStart, readSize); - - Trace.Assert(read == readSize); - - blockLoadedStatus[curr] = true; - last = curr; - - isLoaded |= blockLoadedStatus.All(loaded => loaded); - } - - if (!isClosed) underlyingStream?.Close(); - } - - private int nextBlockToLoad - { - get - { - if (isClosed) return -1; - - int start = underlyingStream.CanSeek ? position / block_size : 0; - - int end = blockLoadedStatus.Length; - if (blocksToReadAhead > -1) - end = Math.Min(end, (position + amountBytesToRead) / block_size + blocksToReadAhead + 1); - - for (int i = start; i < end; i++) - if (!blockLoadedStatus[i]) return i; - - return -1; - } - } - - private volatile bool isDisposed; - - protected override void Dispose(bool disposing) - { - isDisposed = true; - - cancellationToken?.Cancel(); - cancellationToken?.Dispose(); - cancellationToken = null; - - if (!isClosed) Close(); - base.Dispose(disposing); - } - - public override void Close() - { - isClosed = true; - - underlyingStream?.Close(); - - base.Close(); - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => data.Length; - - public override long Position - { - get { return position; } - - set { position = MathHelper.Clamp((int)value, 0, data.Length); } - } - - public override void Flush() - { - throw new NotSupportedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - Trace.Assert(count <= buffer.Length - offset); - - amountBytesToRead = Math.Min(count, data.Length - position); - - int startBlock = position / block_size; - int endBlock = (position + amountBytesToRead) / block_size; - - //ensure all required buffers are loaded - for (int i = startBlock; i <= endBlock; i++) - { - while (!blockLoadedStatus[i]) - { - Thread.Sleep(1); - if (isDisposed) - return 0; - } - } - - Array.Copy(data, position, buffer, offset, amountBytesToRead); - - int bytesRead = amountBytesToRead; - - amountBytesToRead = 0; - position += bytesRead; - - return bytesRead; - } - - public override long Seek(long offset, SeekOrigin origin) - { - switch (origin) - { - case SeekOrigin.Begin: - Position = offset; - break; - case SeekOrigin.Current: - Position += offset; - break; - case SeekOrigin.End: - Position = data.Length + offset; - break; - } - - return position; - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenTK; + +namespace osu.Framework.IO +{ + internal class AsyncBufferStream : Stream + { + private const int block_size = 32768; + + #region Concurrent access + + private readonly byte[] data; + + private readonly bool[] blockLoadedStatus; + + private volatile bool isClosed; + private volatile bool isLoaded; + + private volatile int position; + private volatile int amountBytesToRead; + + #endregion + + private readonly int blocksToReadAhead; + + private readonly Stream underlyingStream; + + private CancellationTokenSource cancellationToken; + + /// + /// A stream that buffers the underlying stream to contiguous memory, reading until the whole file is eventually memory-backed. + /// + /// The underlying stream to read from. + /// The amount of blocks to read ahead of the read position. + /// Another AsyncBufferStream which is backing the same underlying stream. Allows shared usage of memory-backing. + public AsyncBufferStream(Stream stream, int blocksToReadAhead, AsyncBufferStream shared = null) + { + underlyingStream = stream ?? throw new ArgumentNullException(nameof(stream)); + this.blocksToReadAhead = blocksToReadAhead; + + if (underlyingStream.CanSeek) + underlyingStream.Seek(0, SeekOrigin.Begin); + + if (shared?.Length != stream.Length) + { + data = new byte[underlyingStream.Length]; + blockLoadedStatus = new bool[data.Length / block_size + 1]; + } + else + { + data = shared.data; + blockLoadedStatus = shared.blockLoadedStatus; + isLoaded = shared.isLoaded; + } + + cancellationToken = new CancellationTokenSource(); + Task.Factory.StartNew(loadRequiredBlocks, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + ~AsyncBufferStream() + { + Dispose(false); + } + + private void loadRequiredBlocks() + { + if (isLoaded) + return; + + int last = -1; + while (!isLoaded && !isClosed) + { + cancellationToken.Token.ThrowIfCancellationRequested(); + + int curr = nextBlockToLoad; + if (curr < 0) + { + Thread.Sleep(1); + continue; + } + + int readStart = curr * block_size; + + if (last + 1 != curr) + { + //follow along with a seek. + Debug.Assert(underlyingStream.CanSeek); + underlyingStream.Seek(readStart, SeekOrigin.Begin); + } + + Trace.Assert(underlyingStream.Position == readStart); + + int readSize = Math.Min(data.Length - readStart, block_size); + int read = underlyingStream.Read(data, readStart, readSize); + + Trace.Assert(read == readSize); + + blockLoadedStatus[curr] = true; + last = curr; + + isLoaded |= blockLoadedStatus.All(loaded => loaded); + } + + if (!isClosed) underlyingStream?.Close(); + } + + private int nextBlockToLoad + { + get + { + if (isClosed) return -1; + + int start = underlyingStream.CanSeek ? position / block_size : 0; + + int end = blockLoadedStatus.Length; + if (blocksToReadAhead > -1) + end = Math.Min(end, (position + amountBytesToRead) / block_size + blocksToReadAhead + 1); + + for (int i = start; i < end; i++) + if (!blockLoadedStatus[i]) return i; + + return -1; + } + } + + private volatile bool isDisposed; + + protected override void Dispose(bool disposing) + { + isDisposed = true; + + cancellationToken?.Cancel(); + cancellationToken?.Dispose(); + cancellationToken = null; + + if (!isClosed) Close(); + base.Dispose(disposing); + } + + public override void Close() + { + isClosed = true; + + underlyingStream?.Close(); + + base.Close(); + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => data.Length; + + public override long Position + { + get { return position; } + + set { position = MathHelper.Clamp((int)value, 0, data.Length); } + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + Trace.Assert(count <= buffer.Length - offset); + + amountBytesToRead = Math.Min(count, data.Length - position); + + int startBlock = position / block_size; + int endBlock = (position + amountBytesToRead) / block_size; + + //ensure all required buffers are loaded + for (int i = startBlock; i <= endBlock; i++) + { + while (!blockLoadedStatus[i]) + { + Thread.Sleep(1); + if (isDisposed) + return 0; + } + } + + Array.Copy(data, position, buffer, offset, amountBytesToRead); + + int bytesRead = amountBytesToRead; + + amountBytesToRead = 0; + position += bytesRead; + + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = data.Length + offset; + break; + } + + return position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/osu.Framework/IO/File/FileSafety.cs b/osu.Framework/IO/File/FileSafety.cs index d54c09c80..7d4eb7417 100644 --- a/osu.Framework/IO/File/FileSafety.cs +++ b/osu.Framework/IO/File/FileSafety.cs @@ -1,448 +1,448 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace osu.Framework.IO.File -{ - public static class FileSafety - { - public const int MAX_PATH_LENGTH = 248; - - public const string CLEANUP_DIRECTORY = @"_cleanup"; - - public static string FilenameStrip(string entry) - { - foreach (char c in Path.GetInvalidFileNameChars()) - entry = entry.Replace(c.ToString(), string.Empty); - return entry; - } - - public static string PathStrip(string entry) - { - foreach (char c in Path.GetInvalidFileNameChars()) - entry = entry.Replace(c.ToString(), string.Empty); - entry = entry.Replace(".", string.Empty); - return entry; - } - - // Adds an ACL entry on the specified directory for the specified account. - // public static void AddDirectorySecurity(string filename, string account, FileSystemRights rights, InheritanceFlags inheritance, PropagationFlags propagation, AccessControlType controlType) - // { - // // Create a new DirectoryInfo object. - // DirectoryInfo dInfo = new DirectoryInfo(filename); - // // Get a DirectorySecurity object that represents the - // // current security settings. - // DirectorySecurity dSecurity = dInfo.GetAccessControl(); - // // Add the FileSystemAccessRule to the security settings. - // dSecurity.AddAccessRule(new FileSystemAccessRule(account, rights, inheritance, propagation, controlType)); - // // Set the new access settings. - // dInfo.SetAccessControl(dSecurity); - // } - - public static void RemoveReadOnlyRecursive(string s) - { - foreach (string f in Directory.GetFiles(s)) - { - FileInfo myFile = new FileInfo(f); - if ((myFile.Attributes & FileAttributes.ReadOnly) > 0) - myFile.Attributes &= ~FileAttributes.ReadOnly; - } - - foreach (string d in Directory.GetDirectories(s)) - RemoveReadOnlyRecursive(d); - } - - public static bool FileMove(string src, string dest, bool overwrite = true) - { - src = PathSanitise(src); - dest = PathSanitise(dest); - if (src == dest) - return true; //no move necessary - - try - { - if (overwrite) - FileDelete(dest); - System.IO.File.Move(src, dest); - } - catch - { - try - { - System.IO.File.Copy(src, dest); - return FileDelete(src); - } - catch - { - return false; - } - } - return true; - } - - /// - /// Converts all slashes and backslashes to OS-specific directory separator characters. Useful for sanitising user input. - /// - public static string PathSanitise(string path) - { - return path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar); - } - - /// - /// Converts all OS-specific directory separator characters to '/'. Useful for outputting to a config file or similar. - /// - public static string PathStandardise(string path) - { - return path.Replace(Path.DirectorySeparatorChar, '/'); - } - - [Flags] - internal enum MoveFileFlags - { - None = 0, - ReplaceExisting = 1, - CopyAllowed = 2, - DelayUntilReboot = 4, - WriteThrough = 8, - CreateHardlink = 16, - FailIfNotTrackable = 32, - } - - internal static class NativeMethods - { - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool MoveFileEx( - string lpExistingFileName, - string lpNewFileName, - MoveFileFlags dwFlags); - } - - public static bool FileDeleteOnReboot(string filename) - { - filename = PathSanitise(filename); - - try - { - System.IO.File.Delete(filename); - return true; - } - catch - { - } - - string deathLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - System.IO.File.Move(filename, deathLocation); - } - catch - { - deathLocation = filename; - } - - return NativeMethods.MoveFileEx(deathLocation, null, MoveFileFlags.DelayUntilReboot); - } - - /// - /// Performs a file delete. If a delete operation fails (as can regularly happen on windows), the file is moved to a temporary directory for future cleanup (see ). - /// - /// The relative or full path of the file to delete. - /// True if deletion was successful by any means, or the file didn't exist. - public static bool FileDelete(string filename) - { - filename = PathSanitise(filename); - - if (!System.IO.File.Exists(filename)) return true; - - try - { - System.IO.File.Delete(filename); - return true; - } - catch - { - } - - try - { - //try alternative method: move to a cleanup folder and delete later. - if (!Directory.Exists(CLEANUP_DIRECTORY)) - { - DirectoryInfo di = Directory.CreateDirectory(CLEANUP_DIRECTORY); - di.Attributes |= FileAttributes.Hidden; - } - - System.IO.File.Move(filename, Path.Combine(CLEANUP_DIRECTORY, Guid.NewGuid().ToString())); - return true; - } - catch - { - } - - return false; - } - - /// - /// Delete any files in the cleanup directory (see ). - /// - public static void DeleteCleanupDirectory() - { - if (Directory.Exists(CLEANUP_DIRECTORY)) - Directory.Delete(CLEANUP_DIRECTORY, true); - } - - public static string AsciiOnly(string input) - { - if (input == null) return null; - - StringBuilder asc = new StringBuilder(input.Length); - //keep only ascii chars - foreach (char c in input) - if (c <= 126) - asc.Append(c); - return asc.ToString().Trim(); - } - - public static void RecursiveMove(string oldDirectory, string newDirectory) - { - oldDirectory = PathSanitise(oldDirectory); - newDirectory = PathSanitise(newDirectory); - - if (oldDirectory == newDirectory) - return; - - foreach (string dir in Directory.GetDirectories(oldDirectory)) - { - string newSubDirectory = dir; - newSubDirectory = Path.Combine(newDirectory, newSubDirectory.Remove(0, 1 + newSubDirectory.LastIndexOf(Path.DirectorySeparatorChar))); - - try - { - DirectoryInfo newDirectoryInfo = Directory.CreateDirectory(newSubDirectory); - - if ((new DirectoryInfo(dir).Attributes & FileAttributes.Hidden) > 0) - newDirectoryInfo.Attributes |= FileAttributes.Hidden; - } - catch - { - } - - RecursiveMove(dir, newSubDirectory); - } - - bool didExist = Directory.Exists(newDirectory); - if (!didExist) - { - DirectoryInfo newDirectoryInfo = Directory.CreateDirectory(newDirectory); - try - { - if ((new DirectoryInfo(oldDirectory).Attributes & FileAttributes.Hidden) > 0) - newDirectoryInfo.Attributes |= FileAttributes.Hidden; - } - catch - { - } - } - - foreach (string file in Directory.GetFiles(oldDirectory)) - { - string newFile = Path.Combine(newDirectory, Path.GetFileName(file)); - - bool didMove = FileMove(file, newFile, didExist); - if (!didMove) - { - try - { - System.IO.File.Copy(file, newFile); - } - catch - { - } - System.IO.File.Delete(file); - } - } - - Directory.Delete(oldDirectory, true); - } - - public static string GetExtension(string filename) => Path.GetExtension(filename)?.Trim('.').ToLower(); - - // public static FileType GetFileType(string filename) - // { - // try - // { - // string ext = GetExtension(filename); - - // switch (ext) - // { - // case "osu": - // return FileType.Beatmap; - // case "rar": - // return FileType.BeatmapPack; - // case "osz": - // return FileType.BeatmapPackage; - // case "osz2": - // return FileType.BeatmapPackage2; - // case "db": - // return FileType.Database; - // case "zip": - // return FileType.Zip; - //#if P2P - // case "osumagnet": - // return FileType.OsuMagnet; - //#endif - // case "osc": - // return FileType.OsuM; //osu!stream - // case "ogg": - // case "mp3": - // return FileType.AudioTrack; - // case "osr": - // return FileType.Replay; - // case "osk": - // return FileType.Skin; - // case "osb": - // return FileType.Storyboard; - // case "avi": - // case "flv": - // case "mpg": - // case "wmv": - // case "m4v": - // case "mp4": - // return FileType.Video; - // case "jpg": - // case "jpeg": - // case "png": - // return FileType.Image; - // case "wav": - // return FileType.AudioSample; - // case "exe": - // return FileType.Exe; - // default: - // return FileType.Unknown; - // } - // } - // catch - // { - // return FileType.Unknown; - // } - // } - - public static int GetMaxPathLength(string directory) - { - int highestPathLength = directory.Length; - - foreach (string file in Directory.GetFiles(directory)) - { - if (file.Length > highestPathLength) - highestPathLength = file.Length; - } - - foreach (string dir in Directory.GetDirectories(directory)) - { - int tempPathLength = GetMaxPathLength(dir); - if (tempPathLength > highestPathLength) - highestPathLength = tempPathLength; - } - - return highestPathLength; - } - - public static string CleanStoryboardFilename(string filename) - { - return PathStandardise(filename.Trim('"')); - } - - //This is better than encoding as it doesn't check for origin specific data or remove invalid chars. - public static unsafe string RawBytesToString(byte[] encoded) - { - if (encoded.Length == 0) - return string.Empty; - - char[] converted = new char[(encoded.Length + 1) / 2]; - fixed (byte* bytePtr = encoded) - fixed (char* stringPtr = converted) - { - byte* stringBytes = (byte*)stringPtr; - byte* stringEnd = (byte*)stringPtr + converted.Length * 2; - byte* bytePtr2 = bytePtr; - do - { - *stringBytes = *bytePtr2++; - stringBytes++; - } while (stringBytes != stringEnd); - } - return new string(converted); - } - - // public static bool SetDirectoryPermissions(string directory) - // { - // SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); - // NTAccount acct = sid.Translate(typeof(NTAccount)) as NTAccount; - // if (acct != null) - // { - // string strEveryoneAccount = acct.ToString(); - - // try - // { - // AddDirectorySecurity(directory, strEveryoneAccount, FileSystemRights.FullControl, - // InheritanceFlags.None, PropagationFlags.NoPropagateInherit, - // AccessControlType.Allow); - // AddDirectorySecurity(directory, strEveryoneAccount, FileSystemRights.FullControl, - // InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, - // PropagationFlags.InheritOnly, AccessControlType.Allow); - - // RemoveReadOnlyRecursive(directory); - // } - // catch - // { - // return false; - // } - // } - - // return true; - // } - - public static void CreateBackup(string filename) - { - string backupFilename = filename + @"." + DateTime.Now.Ticks + @".bak"; - if (System.IO.File.Exists(filename) && !System.IO.File.Exists(backupFilename)) - { - Debug.Print(@"Backup created: " + backupFilename); - System.IO.File.Move(filename, backupFilename); - } - } - - public static string GetRelativePath(string path, string folder) - { - path = PathStandardise(path).TrimEnd('/'); - folder = PathStandardise(folder).TrimEnd('/'); - - if (path.Length < folder.Length + 1 || path[folder.Length] != '/' || !path.StartsWith(folder, StringComparison.Ordinal)) - throw new ArgumentException(path + " isn't contained in " + folder); - - return path.Substring(folder.Length + 1); - } - - public static string GetTempPath(string suffix = "") - { - string directory = Path.Combine(Path.GetTempPath(), @"osu!"); - Directory.CreateDirectory(directory); - return Path.Combine(directory, suffix); - } - - /// - /// Returns the path without the extension of the file. - /// Contrarily to Path.GetFileNameWithoutExtension, it keeps the path to the file ("sb/triangle.png" becomes "sb/triangle" and not "triangle") - /// - public static string StripExtension(string filepath) - { - int dotIndex = filepath.LastIndexOf('.'); - return dotIndex == -1 ? filepath : filepath.Substring(0, dotIndex); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace osu.Framework.IO.File +{ + public static class FileSafety + { + public const int MAX_PATH_LENGTH = 248; + + public const string CLEANUP_DIRECTORY = @"_cleanup"; + + public static string FilenameStrip(string entry) + { + foreach (char c in Path.GetInvalidFileNameChars()) + entry = entry.Replace(c.ToString(), string.Empty); + return entry; + } + + public static string PathStrip(string entry) + { + foreach (char c in Path.GetInvalidFileNameChars()) + entry = entry.Replace(c.ToString(), string.Empty); + entry = entry.Replace(".", string.Empty); + return entry; + } + + // Adds an ACL entry on the specified directory for the specified account. + // public static void AddDirectorySecurity(string filename, string account, FileSystemRights rights, InheritanceFlags inheritance, PropagationFlags propagation, AccessControlType controlType) + // { + // // Create a new DirectoryInfo object. + // DirectoryInfo dInfo = new DirectoryInfo(filename); + // // Get a DirectorySecurity object that represents the + // // current security settings. + // DirectorySecurity dSecurity = dInfo.GetAccessControl(); + // // Add the FileSystemAccessRule to the security settings. + // dSecurity.AddAccessRule(new FileSystemAccessRule(account, rights, inheritance, propagation, controlType)); + // // Set the new access settings. + // dInfo.SetAccessControl(dSecurity); + // } + + public static void RemoveReadOnlyRecursive(string s) + { + foreach (string f in Directory.GetFiles(s)) + { + FileInfo myFile = new FileInfo(f); + if ((myFile.Attributes & FileAttributes.ReadOnly) > 0) + myFile.Attributes &= ~FileAttributes.ReadOnly; + } + + foreach (string d in Directory.GetDirectories(s)) + RemoveReadOnlyRecursive(d); + } + + public static bool FileMove(string src, string dest, bool overwrite = true) + { + src = PathSanitise(src); + dest = PathSanitise(dest); + if (src == dest) + return true; //no move necessary + + try + { + if (overwrite) + FileDelete(dest); + System.IO.File.Move(src, dest); + } + catch + { + try + { + System.IO.File.Copy(src, dest); + return FileDelete(src); + } + catch + { + return false; + } + } + return true; + } + + /// + /// Converts all slashes and backslashes to OS-specific directory separator characters. Useful for sanitising user input. + /// + public static string PathSanitise(string path) + { + return path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar); + } + + /// + /// Converts all OS-specific directory separator characters to '/'. Useful for outputting to a config file or similar. + /// + public static string PathStandardise(string path) + { + return path.Replace(Path.DirectorySeparatorChar, '/'); + } + + [Flags] + internal enum MoveFileFlags + { + None = 0, + ReplaceExisting = 1, + CopyAllowed = 2, + DelayUntilReboot = 4, + WriteThrough = 8, + CreateHardlink = 16, + FailIfNotTrackable = 32, + } + + internal static class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool MoveFileEx( + string lpExistingFileName, + string lpNewFileName, + MoveFileFlags dwFlags); + } + + public static bool FileDeleteOnReboot(string filename) + { + filename = PathSanitise(filename); + + try + { + System.IO.File.Delete(filename); + return true; + } + catch + { + } + + string deathLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + System.IO.File.Move(filename, deathLocation); + } + catch + { + deathLocation = filename; + } + + return NativeMethods.MoveFileEx(deathLocation, null, MoveFileFlags.DelayUntilReboot); + } + + /// + /// Performs a file delete. If a delete operation fails (as can regularly happen on windows), the file is moved to a temporary directory for future cleanup (see ). + /// + /// The relative or full path of the file to delete. + /// True if deletion was successful by any means, or the file didn't exist. + public static bool FileDelete(string filename) + { + filename = PathSanitise(filename); + + if (!System.IO.File.Exists(filename)) return true; + + try + { + System.IO.File.Delete(filename); + return true; + } + catch + { + } + + try + { + //try alternative method: move to a cleanup folder and delete later. + if (!Directory.Exists(CLEANUP_DIRECTORY)) + { + DirectoryInfo di = Directory.CreateDirectory(CLEANUP_DIRECTORY); + di.Attributes |= FileAttributes.Hidden; + } + + System.IO.File.Move(filename, Path.Combine(CLEANUP_DIRECTORY, Guid.NewGuid().ToString())); + return true; + } + catch + { + } + + return false; + } + + /// + /// Delete any files in the cleanup directory (see ). + /// + public static void DeleteCleanupDirectory() + { + if (Directory.Exists(CLEANUP_DIRECTORY)) + Directory.Delete(CLEANUP_DIRECTORY, true); + } + + public static string AsciiOnly(string input) + { + if (input == null) return null; + + StringBuilder asc = new StringBuilder(input.Length); + //keep only ascii chars + foreach (char c in input) + if (c <= 126) + asc.Append(c); + return asc.ToString().Trim(); + } + + public static void RecursiveMove(string oldDirectory, string newDirectory) + { + oldDirectory = PathSanitise(oldDirectory); + newDirectory = PathSanitise(newDirectory); + + if (oldDirectory == newDirectory) + return; + + foreach (string dir in Directory.GetDirectories(oldDirectory)) + { + string newSubDirectory = dir; + newSubDirectory = Path.Combine(newDirectory, newSubDirectory.Remove(0, 1 + newSubDirectory.LastIndexOf(Path.DirectorySeparatorChar))); + + try + { + DirectoryInfo newDirectoryInfo = Directory.CreateDirectory(newSubDirectory); + + if ((new DirectoryInfo(dir).Attributes & FileAttributes.Hidden) > 0) + newDirectoryInfo.Attributes |= FileAttributes.Hidden; + } + catch + { + } + + RecursiveMove(dir, newSubDirectory); + } + + bool didExist = Directory.Exists(newDirectory); + if (!didExist) + { + DirectoryInfo newDirectoryInfo = Directory.CreateDirectory(newDirectory); + try + { + if ((new DirectoryInfo(oldDirectory).Attributes & FileAttributes.Hidden) > 0) + newDirectoryInfo.Attributes |= FileAttributes.Hidden; + } + catch + { + } + } + + foreach (string file in Directory.GetFiles(oldDirectory)) + { + string newFile = Path.Combine(newDirectory, Path.GetFileName(file)); + + bool didMove = FileMove(file, newFile, didExist); + if (!didMove) + { + try + { + System.IO.File.Copy(file, newFile); + } + catch + { + } + System.IO.File.Delete(file); + } + } + + Directory.Delete(oldDirectory, true); + } + + public static string GetExtension(string filename) => Path.GetExtension(filename)?.Trim('.').ToLower(); + + // public static FileType GetFileType(string filename) + // { + // try + // { + // string ext = GetExtension(filename); + + // switch (ext) + // { + // case "osu": + // return FileType.Beatmap; + // case "rar": + // return FileType.BeatmapPack; + // case "osz": + // return FileType.BeatmapPackage; + // case "osz2": + // return FileType.BeatmapPackage2; + // case "db": + // return FileType.Database; + // case "zip": + // return FileType.Zip; + //#if P2P + // case "osumagnet": + // return FileType.OsuMagnet; + //#endif + // case "osc": + // return FileType.OsuM; //osu!stream + // case "ogg": + // case "mp3": + // return FileType.AudioTrack; + // case "osr": + // return FileType.Replay; + // case "osk": + // return FileType.Skin; + // case "osb": + // return FileType.Storyboard; + // case "avi": + // case "flv": + // case "mpg": + // case "wmv": + // case "m4v": + // case "mp4": + // return FileType.Video; + // case "jpg": + // case "jpeg": + // case "png": + // return FileType.Image; + // case "wav": + // return FileType.AudioSample; + // case "exe": + // return FileType.Exe; + // default: + // return FileType.Unknown; + // } + // } + // catch + // { + // return FileType.Unknown; + // } + // } + + public static int GetMaxPathLength(string directory) + { + int highestPathLength = directory.Length; + + foreach (string file in Directory.GetFiles(directory)) + { + if (file.Length > highestPathLength) + highestPathLength = file.Length; + } + + foreach (string dir in Directory.GetDirectories(directory)) + { + int tempPathLength = GetMaxPathLength(dir); + if (tempPathLength > highestPathLength) + highestPathLength = tempPathLength; + } + + return highestPathLength; + } + + public static string CleanStoryboardFilename(string filename) + { + return PathStandardise(filename.Trim('"')); + } + + //This is better than encoding as it doesn't check for origin specific data or remove invalid chars. + public static unsafe string RawBytesToString(byte[] encoded) + { + if (encoded.Length == 0) + return string.Empty; + + char[] converted = new char[(encoded.Length + 1) / 2]; + fixed (byte* bytePtr = encoded) + fixed (char* stringPtr = converted) + { + byte* stringBytes = (byte*)stringPtr; + byte* stringEnd = (byte*)stringPtr + converted.Length * 2; + byte* bytePtr2 = bytePtr; + do + { + *stringBytes = *bytePtr2++; + stringBytes++; + } while (stringBytes != stringEnd); + } + return new string(converted); + } + + // public static bool SetDirectoryPermissions(string directory) + // { + // SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + // NTAccount acct = sid.Translate(typeof(NTAccount)) as NTAccount; + // if (acct != null) + // { + // string strEveryoneAccount = acct.ToString(); + + // try + // { + // AddDirectorySecurity(directory, strEveryoneAccount, FileSystemRights.FullControl, + // InheritanceFlags.None, PropagationFlags.NoPropagateInherit, + // AccessControlType.Allow); + // AddDirectorySecurity(directory, strEveryoneAccount, FileSystemRights.FullControl, + // InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, + // PropagationFlags.InheritOnly, AccessControlType.Allow); + + // RemoveReadOnlyRecursive(directory); + // } + // catch + // { + // return false; + // } + // } + + // return true; + // } + + public static void CreateBackup(string filename) + { + string backupFilename = filename + @"." + DateTime.Now.Ticks + @".bak"; + if (System.IO.File.Exists(filename) && !System.IO.File.Exists(backupFilename)) + { + Debug.Print(@"Backup created: " + backupFilename); + System.IO.File.Move(filename, backupFilename); + } + } + + public static string GetRelativePath(string path, string folder) + { + path = PathStandardise(path).TrimEnd('/'); + folder = PathStandardise(folder).TrimEnd('/'); + + if (path.Length < folder.Length + 1 || path[folder.Length] != '/' || !path.StartsWith(folder, StringComparison.Ordinal)) + throw new ArgumentException(path + " isn't contained in " + folder); + + return path.Substring(folder.Length + 1); + } + + public static string GetTempPath(string suffix = "") + { + string directory = Path.Combine(Path.GetTempPath(), @"osu!"); + Directory.CreateDirectory(directory); + return Path.Combine(directory, suffix); + } + + /// + /// Returns the path without the extension of the file. + /// Contrarily to Path.GetFileNameWithoutExtension, it keeps the path to the file ("sb/triangle.png" becomes "sb/triangle" and not "triangle") + /// + public static string StripExtension(string filepath) + { + int dotIndex = filepath.LastIndexOf('.'); + return dotIndex == -1 ? filepath : filepath.Substring(0, dotIndex); + } + } +} diff --git a/osu.Framework/IO/Network/FileWebRequest.cs b/osu.Framework/IO/Network/FileWebRequest.cs index 12fcec89f..dcefae2dd 100644 --- a/osu.Framework/IO/Network/FileWebRequest.cs +++ b/osu.Framework/IO/Network/FileWebRequest.cs @@ -1,41 +1,41 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using osu.Framework.IO.File; - -namespace osu.Framework.IO.Network -{ - /// - /// Downloads a file from the internet to a specified location - /// - public class FileWebRequest : WebRequest - { - public string Filename; - - protected override string Accept => "application/octet-stream"; - - protected override Stream CreateOutputStream() - { - string path = Path.GetDirectoryName(Filename); - if (!string.IsNullOrEmpty(path)) Directory.CreateDirectory(path); - - return new FileStream(Filename, FileMode.Create, FileAccess.Write, FileShare.Write, 32768); - } - - public FileWebRequest(string filename, string url) - : base(url) - { - Timeout *= 2; - Filename = filename; - } - - protected override void Complete(Exception e = null) - { - ResponseStream?.Close(); - if (e != null) FileSafety.FileDelete(Filename); - base.Complete(e); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using osu.Framework.IO.File; + +namespace osu.Framework.IO.Network +{ + /// + /// Downloads a file from the internet to a specified location + /// + public class FileWebRequest : WebRequest + { + public string Filename; + + protected override string Accept => "application/octet-stream"; + + protected override Stream CreateOutputStream() + { + string path = Path.GetDirectoryName(Filename); + if (!string.IsNullOrEmpty(path)) Directory.CreateDirectory(path); + + return new FileStream(Filename, FileMode.Create, FileAccess.Write, FileShare.Write, 32768); + } + + public FileWebRequest(string filename, string url) + : base(url) + { + Timeout *= 2; + Filename = filename; + } + + protected override void Complete(Exception e = null) + { + ResponseStream?.Close(); + if (e != null) FileSafety.FileDelete(Filename); + base.Complete(e); + } + } +} diff --git a/osu.Framework/IO/Network/HttpMethod.cs b/osu.Framework/IO/Network/HttpMethod.cs index 6cf1be993..a18a239ef 100644 --- a/osu.Framework/IO/Network/HttpMethod.cs +++ b/osu.Framework/IO/Network/HttpMethod.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.IO.Network -{ - public enum HttpMethod - { - GET, - POST - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.IO.Network +{ + public enum HttpMethod + { + GET, + POST + } +} diff --git a/osu.Framework/IO/Network/JsonWebRequest.cs b/osu.Framework/IO/Network/JsonWebRequest.cs index 4c6d9e253..1dcfbaa63 100644 --- a/osu.Framework/IO/Network/JsonWebRequest.cs +++ b/osu.Framework/IO/Network/JsonWebRequest.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using Newtonsoft.Json; - -namespace osu.Framework.IO.Network -{ - /// - /// A web request with a specific JSON response format. - /// - /// the response format. - public class JsonWebRequest : WebRequest - { - protected override string Accept => "application/json"; - - public JsonWebRequest(string url = null, params object[] args) - : base(url, args) - { - } - - protected override void ProcessResponse() => deserialisedResponse = JsonConvert.DeserializeObject(ResponseString); - - private T deserialisedResponse; - - public T ResponseObject => deserialisedResponse; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using Newtonsoft.Json; + +namespace osu.Framework.IO.Network +{ + /// + /// A web request with a specific JSON response format. + /// + /// the response format. + public class JsonWebRequest : WebRequest + { + protected override string Accept => "application/json"; + + public JsonWebRequest(string url = null, params object[] args) + : base(url, args) + { + } + + protected override void ProcessResponse() => deserialisedResponse = JsonConvert.DeserializeObject(ResponseString); + + private T deserialisedResponse; + + public T ResponseObject => deserialisedResponse; + } +} diff --git a/osu.Framework/IO/Network/UrlEncoding.cs b/osu.Framework/IO/Network/UrlEncoding.cs index 6356af30b..686d340f7 100644 --- a/osu.Framework/IO/Network/UrlEncoding.cs +++ b/osu.Framework/IO/Network/UrlEncoding.cs @@ -1,121 +1,121 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Text; - -namespace osu.Framework.IO.Network -{ - public static class UrlEncoding - { - public static string UrlEncode(string str) - { - if (str == null) - { - return null; - } - return UrlEncode(str, Encoding.UTF8, false); - } - - public static string UrlEncodeParam(string str) - { - if (str == null) - { - return null; - } - return UrlEncode(str, Encoding.UTF8, true); - } - - public static string UrlEncode(string str, Encoding e, bool paramEncode) - { - if (str == null) - { - return null; - } - return Encoding.ASCII.GetString(UrlEncodeToBytes(str, e, paramEncode)); - } - - public static byte[] UrlEncodeToBytes(string str, Encoding e, bool paramEncode) - { - if (str == null) - { - return null; - } - byte[] bytes = e.GetBytes(str); - return urlEncodeBytesToBytesPublic(bytes, 0, bytes.Length, false, paramEncode); - } - - private static byte[] urlEncodeBytesToBytesPublic(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue, bool paramEncode) - { - int num = 0; - int num2 = 0; - for (int i = 0; i < count; i++) - { - char ch = (char)bytes[offset + i]; - if (paramEncode && ch == ' ') - { - num++; - } - else if (!IsSafe(ch)) - { - num2++; - } - } - if (!alwaysCreateReturnValue && num == 0 && num2 == 0) - { - return bytes; - } - byte[] buffer = new byte[count + num2 * 2]; - int num4 = 0; - for (int j = 0; j < count; j++) - { - byte num6 = bytes[offset + j]; - char ch2 = (char)num6; - if (IsSafe(ch2)) - { - buffer[num4++] = num6; - } - else if (paramEncode && ch2 == ' ') - { - buffer[num4++] = 0x2b; - } - else - { - buffer[num4++] = 0x25; - buffer[num4++] = (byte)IntToHex((num6 >> 4) & 15); - buffer[num4++] = (byte)IntToHex(num6 & 15); - } - } - return buffer; - } - - public static bool IsSafe(char ch) - { - if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') - { - return true; - } - switch (ch) - { - case '\'': - case '(': - case ')': - case '*': - case '-': - case '.': - case '_': - case '!': - return true; - } - return false; - } - - public static char IntToHex(int n) - { - if (n <= 9) - { - return (char)(n + 0x30); - } - return (char)(n - 10 + 0x61); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Text; + +namespace osu.Framework.IO.Network +{ + public static class UrlEncoding + { + public static string UrlEncode(string str) + { + if (str == null) + { + return null; + } + return UrlEncode(str, Encoding.UTF8, false); + } + + public static string UrlEncodeParam(string str) + { + if (str == null) + { + return null; + } + return UrlEncode(str, Encoding.UTF8, true); + } + + public static string UrlEncode(string str, Encoding e, bool paramEncode) + { + if (str == null) + { + return null; + } + return Encoding.ASCII.GetString(UrlEncodeToBytes(str, e, paramEncode)); + } + + public static byte[] UrlEncodeToBytes(string str, Encoding e, bool paramEncode) + { + if (str == null) + { + return null; + } + byte[] bytes = e.GetBytes(str); + return urlEncodeBytesToBytesPublic(bytes, 0, bytes.Length, false, paramEncode); + } + + private static byte[] urlEncodeBytesToBytesPublic(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue, bool paramEncode) + { + int num = 0; + int num2 = 0; + for (int i = 0; i < count; i++) + { + char ch = (char)bytes[offset + i]; + if (paramEncode && ch == ' ') + { + num++; + } + else if (!IsSafe(ch)) + { + num2++; + } + } + if (!alwaysCreateReturnValue && num == 0 && num2 == 0) + { + return bytes; + } + byte[] buffer = new byte[count + num2 * 2]; + int num4 = 0; + for (int j = 0; j < count; j++) + { + byte num6 = bytes[offset + j]; + char ch2 = (char)num6; + if (IsSafe(ch2)) + { + buffer[num4++] = num6; + } + else if (paramEncode && ch2 == ' ') + { + buffer[num4++] = 0x2b; + } + else + { + buffer[num4++] = 0x25; + buffer[num4++] = (byte)IntToHex((num6 >> 4) & 15); + buffer[num4++] = (byte)IntToHex(num6 & 15); + } + } + return buffer; + } + + public static bool IsSafe(char ch) + { + if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') + { + return true; + } + switch (ch) + { + case '\'': + case '(': + case ')': + case '*': + case '-': + case '.': + case '_': + case '!': + return true; + } + return false; + } + + public static char IntToHex(int n) + { + if (n <= 9) + { + return (char)(n + 0x30); + } + return (char)(n - 10 + 0x61); + } + } +} diff --git a/osu.Framework/IO/Network/WebRequest.cs b/osu.Framework/IO/Network/WebRequest.cs index cf39f4a53..df3864750 100644 --- a/osu.Framework/IO/Network/WebRequest.cs +++ b/osu.Framework/IO/Network/WebRequest.cs @@ -1,692 +1,692 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Configuration; -using osu.Framework.Extensions.ExceptionExtensions; -using osu.Framework.Logging; - -namespace osu.Framework.IO.Network -{ - public class WebRequest : IDisposable - { - internal const int MAX_RETRIES = 1; - - /// - /// Invoked when a response has been received, but not data has been received. - /// - public event Action Started; - - /// - /// Invoked when the has finished successfully. - /// - public event Action Finished; - - /// - /// Invoked when the has failed. - /// - public event Action Failed; - - /// - /// Invoked when the download progress has changed. - /// - public event Action DownloadProgress; - - /// - /// Invoked when the upload progress has changed. - /// - public event Action UploadProgress; - - /// - /// Whether the was aborted due to an exception or a user abort request. - /// - public bool Aborted { get; private set; } - - private bool completed; - /// - /// Whether the has been run. - /// - public bool Completed - { - get { return completed; } - private set - { - completed = value; - if (!completed) return; - - // WebRequests can only be used once - no need to keep events bound - // This helps with disposal in PerformAsync usages - Started = null; - Finished = null; - DownloadProgress = null; - UploadProgress = null; - } - } - - private string url; - - /// - /// The URL of this request. - /// - public string Url - { - get { return url; } - set - { -#if !DEBUG - if (!value.StartsWith(@"https://")) - value = @"https://" + value.Replace(@"http://", @""); -#endif - url = value; - } - } - - /// - /// POST parameters. - /// - private readonly Dictionary parameters = new Dictionary(); - - /// - /// FILE parameters. - /// - private readonly IDictionary files = new Dictionary(); - - /// - /// The request headers. - /// - private readonly IDictionary headers = new Dictionary(); - - public const int DEFAULT_TIMEOUT = 10000; - - public HttpMethod Method; - - /// - /// The amount of time from last sent or received data to trigger a timeout and abort the request. - /// - public int Timeout = DEFAULT_TIMEOUT; - - /// - /// The type of content expected by this web request. - /// - protected virtual string Accept => string.Empty; - - internal int RetryCount { get; private set; } - - /// - /// Whether this request should internally retry (up to times) on a timeout before throwing an exception. - /// - public bool AllowRetryOnTimeout { get; set; } = true; - - private static readonly Logger logger; - - private static readonly HttpClient client; - - static WebRequest() - { - client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }); - client.DefaultRequestHeaders.UserAgent.ParseAdd("osu!"); - client.DefaultRequestHeaders.ExpectContinue = true; - - // Timeout is controlled manually through cancellation tokens because - // HttpClient does not properly timeout while reading chunked data - client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; - - logger = Logger.GetLogger(LoggingTarget.Network); - } - - public WebRequest(string url = null, params object[] args) - { - if (!string.IsNullOrEmpty(url)) - Url = args.Length == 0 ? url : string.Format(url, args); - } - - ~WebRequest() - { - Dispose(false); - } - - private int responseBytesRead; - - private const int buffer_size = 32768; - private byte[] buffer; - - private MemoryStream rawContent; - - public string ContentType; - - protected virtual Stream CreateOutputStream() - { - return new MemoryStream(); - } - - public Stream ResponseStream; - - public string ResponseString - { - get - { - try - { - ResponseStream.Seek(0, SeekOrigin.Begin); - StreamReader r = new StreamReader(ResponseStream, Encoding.UTF8); - return r.ReadToEnd(); - } - catch - { - return null; - } - } - } - - public byte[] ResponseData - { - get - { - try - { - byte[] data = new byte[ResponseStream.Length]; - ResponseStream.Seek(0, SeekOrigin.Begin); - ResponseStream.Read(data, 0, data.Length); - return data; - } - catch - { - return null; - } - } - } - - public HttpResponseHeaders ResponseHeaders => response.Headers; - - private CancellationTokenSource abortToken; - private CancellationTokenSource timeoutToken; - - private LengthTrackingStream requestStream; - private HttpResponseMessage response; - - private long contentLength => requestStream?.Length ?? 0; - - private const string form_boundary = "-----------------------------28947758029299"; - - private const string form_content_type = "multipart/form-data; boundary=" + form_boundary; - - /// - /// Performs the request asynchronously. - /// - public async Task PerformAsync() - { - if (Completed) - throw new InvalidOperationException($"The {nameof(WebRequest)} has already been run."); - try - { - await Task.Factory.StartNew(internalPerform, TaskCreationOptions.LongRunning); - } - catch (AggregateException ae) - { - ae.RethrowIfSingular(); - } - } - - private void internalPerform() - { - using (abortToken = new CancellationTokenSource()) - using (timeoutToken = new CancellationTokenSource()) - using (var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(abortToken.Token, timeoutToken.Token)) - { - try - { - PrePerform(); - - HttpRequestMessage request; - - switch (Method) - { - default: - throw new InvalidOperationException($"HTTP method {Method} is currently not supported"); - case HttpMethod.GET: - if (files.Count > 0) - throw new InvalidOperationException($"Cannot use {nameof(AddFile)} in a GET request. Please set the {nameof(Method)} to POST."); - - StringBuilder requestParameters = new StringBuilder(); - foreach (var p in parameters) - requestParameters.Append($@"{p.Key}={p.Value}&"); - string requestString = requestParameters.ToString().TrimEnd('&'); - - request = new HttpRequestMessage(System.Net.Http.HttpMethod.Get, string.IsNullOrEmpty(requestString) ? Url : $"{Url}?{requestString}"); - break; - case HttpMethod.POST: - request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, Url); - - Stream postContent; - - if (rawContent != null) - { - if (parameters.Count > 0) - throw new InvalidOperationException($"Cannot use {nameof(AddRaw)} in conjunction with {nameof(AddParameter)}"); - if (files.Count > 0) - throw new InvalidOperationException($"Cannot use {nameof(AddRaw)} in conjunction with {nameof(AddFile)}"); - - postContent = new MemoryStream(); - rawContent.Position = 0; - rawContent.CopyTo(postContent); - postContent.Position = 0; - } - else - { - if (!string.IsNullOrEmpty(ContentType) && ContentType != form_content_type) - throw new InvalidOperationException($"Cannot use custom {nameof(ContentType)} in a POST request."); - - ContentType = form_content_type; - - var formData = new MultipartFormDataContent(form_boundary); - - foreach (var p in parameters) - formData.Add(new StringContent(p.Value), p.Key); - - foreach (var p in files) - { - var byteContent = new ByteArrayContent(p.Value); - byteContent.Headers.Add("Content-Type", "application/octet-stream"); - formData.Add(byteContent, p.Key, p.Key); - } - - postContent = formData.ReadAsStreamAsync().Result; - } - - requestStream = new LengthTrackingStream(postContent); - requestStream.BytesRead.ValueChanged += v => - { - reportForwardProgress(); - UploadProgress?.Invoke(v, contentLength); - }; - - request.Content = new StreamContent(requestStream); - if (!string.IsNullOrEmpty(ContentType)) - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentType); - break; - } - - if (!string.IsNullOrEmpty(Accept)) - request.Headers.Accept.TryParseAdd(Accept); - - foreach (var kvp in headers) - request.Headers.Add(kvp.Key, kvp.Value); - - reportForwardProgress(); - - using (request) - { - response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedToken.Token).Result; - - ResponseStream = CreateOutputStream(); - - switch (Method) - { - case HttpMethod.GET: - //GETs are easy - beginResponse(linkedToken.Token); - break; - case HttpMethod.POST: - reportForwardProgress(); - UploadProgress?.Invoke(0, contentLength); - - beginResponse(linkedToken.Token); - break; - } - } - } - catch (Exception) when (timeoutToken.IsCancellationRequested) - { - Complete(new WebException($"Request to {Url} timed out after {timeSinceLastAction / 1000} seconds idle (read {responseBytesRead} bytes, retried {RetryCount} times).", WebExceptionStatus.Timeout)); - } - catch (Exception) when (abortToken.IsCancellationRequested) - { - Complete(new WebException($"Request to {Url} aborted by user.", WebExceptionStatus.RequestCanceled)); - } - catch (Exception e) - { - if (Completed) - // we may be coming from one of the exception blocks handled above (as Complete will rethrow all exceptions). - throw; - - Complete(e); - } - } - } - - /// - /// Performs the request synchronously. - /// - public void Perform() - { - try - { - PerformAsync().Wait(); - } - catch (AggregateException ae) - { - ae.RethrowIfSingular(); - } - } - - /// - /// Task to run direct before performing the request. - /// - protected virtual void PrePerform() - { - } - - private void beginResponse(CancellationToken cancellationToken) - { - using (var responseStream = response.Content.ReadAsStreamAsync().Result) - { - reportForwardProgress(); - Started?.Invoke(); - - buffer = new byte[buffer_size]; - - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - int read = responseStream.Read(buffer, 0, buffer_size); - - reportForwardProgress(); - - if (read > 0) - { - ResponseStream.Write(buffer, 0, read); - responseBytesRead += read; - DownloadProgress?.Invoke(responseBytesRead, response.Content.Headers.ContentLength ?? responseBytesRead); - } - else - { - ResponseStream.Seek(0, SeekOrigin.Begin); - Complete(); - break; - } - } - } - } - - protected virtual void Complete(Exception e = null) - { - if (Aborted) - return; - - var we = e as WebException; - - bool allowRetry = AllowRetryOnTimeout; - - if (e != null) - allowRetry &= we?.Status == WebExceptionStatus.Timeout; - else if (!response.IsSuccessStatusCode) - { - e = new WebException(response.StatusCode.ToString()); - - switch (response.StatusCode) - { - case HttpStatusCode.GatewayTimeout: - case HttpStatusCode.RequestTimeout: - break; - case HttpStatusCode.NotFound: - case HttpStatusCode.MethodNotAllowed: - case HttpStatusCode.Forbidden: - allowRetry = false; - break; - case HttpStatusCode.Unauthorized: - allowRetry = false; - break; - } - } - - if (e != null) - { - if (allowRetry && RetryCount < MAX_RETRIES && responseBytesRead == 0) - { - RetryCount++; - - logger.Add($@"Request to {Url} failed with {e} (retrying {RetryCount}/{MAX_RETRIES})."); - - //do a retry - internalPerform(); - return; - } - - logger.Add($"Request to {Url} failed with {e}."); - } - else - logger.Add($@"Request to {Url} successfully completed!"); - - try - { - ProcessResponse(); - } - catch (Exception se) - { - logger.Add($"Processing response from {Url} failed with {se}."); - e = e == null ? se : new AggregateException(e, se); - } - - Completed = true; - - if (e == null) - { - Finished?.Invoke(); - } - else - { - Failed?.Invoke(e); - Aborted = true; - throw e; - } - } - - /// - /// Performs any post-processing of the response. - /// Exceptions thrown in this method will be passed to . - /// - protected virtual void ProcessResponse() - { - } - - /// - /// Forcefully abort the request. - /// - public void Abort() - { - if (Aborted || Completed) return; - - Aborted = true; - Completed = true; - - try - { - abortToken?.Cancel(); - } - catch (ObjectDisposedException) - { - } - } - - /// - /// Adds a raw POST body to this request. - /// This may not be used in conjunction with and . - /// - /// The text. - public void AddRaw(string text) - { - AddRaw(Encoding.UTF8.GetBytes(text)); - } - - /// - /// Adds a raw POST body to this request. - /// This may not be used in conjunction with and . - /// - /// The raw data. - public void AddRaw(byte[] bytes) - { - AddRaw(new MemoryStream(bytes)); - } - - /// - /// Adds a raw POST body to this request. - /// This may not be used in conjunction with and . - /// - /// The stream containing the raw data. This stream will _not_ be finalized by this request. - public void AddRaw(Stream stream) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - - if (rawContent == null) - rawContent = new MemoryStream(); - - stream.CopyTo(rawContent); - } - - /// - /// Add a new FILE parameter to this request. Replaces any existing file with the same name. - /// This may not be used in conjunction with . GET requests may not contain files. - /// - /// The name of the file. This becomes the name of the file in a multi-part form POST content. - /// The file data. - public void AddFile(string name, byte[] data) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (data == null) throw new ArgumentNullException(nameof(data)); - - files[name] = data; - } - - /// - /// Add a new POST parameter to this request. Replaces any existing parameter with the same name. - /// This may not be used in conjunction with . - /// - /// The name of the parameter. - /// The parameter value. - public void AddParameter(string name, string value) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (value == null) throw new ArgumentNullException(nameof(value)); - - parameters[name] = value; - } - - /// - /// Adds a new header to this request. Replaces any existing header with the same name. - /// - /// The name of the header. - /// The header value. - public void AddHeader(string name, string value) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (value == null) throw new ArgumentNullException(nameof(value)); - - headers[name] = value; - } - - #region Timeout Handling - - private long lastAction; - - private long timeSinceLastAction => (DateTime.Now.Ticks - lastAction) / TimeSpan.TicksPerMillisecond; - - private void reportForwardProgress() - { - lastAction = DateTime.Now.Ticks; - timeoutToken.CancelAfter(Timeout); - } - - #endregion - - #region IDisposable Support - - private bool isDisposed; - - protected void Dispose(bool disposing) - { - if (isDisposed) return; - isDisposed = true; - - Abort(); - - requestStream?.Dispose(); - response?.Dispose(); - - if (!(ResponseStream is MemoryStream)) - ResponseStream?.Dispose(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - private class LengthTrackingStream : Stream - { - public readonly BindableLong BytesRead = new BindableLong(); - - private readonly Stream baseStream; - - public LengthTrackingStream(Stream baseStream) - { - this.baseStream = baseStream; - } - - public override void Flush() - { - baseStream.Flush(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int read = baseStream.Read(buffer, offset, count); - BytesRead.Value += read; - return read; - } - - public override long Seek(long offset, SeekOrigin origin) - { - return baseStream.Seek(offset, origin); - } - - public override void SetLength(long value) - { - baseStream.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - baseStream.Write(buffer, offset, count); - } - - public override bool CanRead => baseStream.CanRead; - public override bool CanSeek => baseStream.CanSeek; - public override bool CanWrite => baseStream.CanWrite; - public override long Length => baseStream.Length; - - public override long Position - { - get { return baseStream.Position; } - set { baseStream.Position = value; } - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - baseStream.Dispose(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Configuration; +using osu.Framework.Extensions.ExceptionExtensions; +using osu.Framework.Logging; + +namespace osu.Framework.IO.Network +{ + public class WebRequest : IDisposable + { + internal const int MAX_RETRIES = 1; + + /// + /// Invoked when a response has been received, but not data has been received. + /// + public event Action Started; + + /// + /// Invoked when the has finished successfully. + /// + public event Action Finished; + + /// + /// Invoked when the has failed. + /// + public event Action Failed; + + /// + /// Invoked when the download progress has changed. + /// + public event Action DownloadProgress; + + /// + /// Invoked when the upload progress has changed. + /// + public event Action UploadProgress; + + /// + /// Whether the was aborted due to an exception or a user abort request. + /// + public bool Aborted { get; private set; } + + private bool completed; + /// + /// Whether the has been run. + /// + public bool Completed + { + get { return completed; } + private set + { + completed = value; + if (!completed) return; + + // WebRequests can only be used once - no need to keep events bound + // This helps with disposal in PerformAsync usages + Started = null; + Finished = null; + DownloadProgress = null; + UploadProgress = null; + } + } + + private string url; + + /// + /// The URL of this request. + /// + public string Url + { + get { return url; } + set + { +#if !DEBUG + if (!value.StartsWith(@"https://")) + value = @"https://" + value.Replace(@"http://", @""); +#endif + url = value; + } + } + + /// + /// POST parameters. + /// + private readonly Dictionary parameters = new Dictionary(); + + /// + /// FILE parameters. + /// + private readonly IDictionary files = new Dictionary(); + + /// + /// The request headers. + /// + private readonly IDictionary headers = new Dictionary(); + + public const int DEFAULT_TIMEOUT = 10000; + + public HttpMethod Method; + + /// + /// The amount of time from last sent or received data to trigger a timeout and abort the request. + /// + public int Timeout = DEFAULT_TIMEOUT; + + /// + /// The type of content expected by this web request. + /// + protected virtual string Accept => string.Empty; + + internal int RetryCount { get; private set; } + + /// + /// Whether this request should internally retry (up to times) on a timeout before throwing an exception. + /// + public bool AllowRetryOnTimeout { get; set; } = true; + + private static readonly Logger logger; + + private static readonly HttpClient client; + + static WebRequest() + { + client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }); + client.DefaultRequestHeaders.UserAgent.ParseAdd("osu!"); + client.DefaultRequestHeaders.ExpectContinue = true; + + // Timeout is controlled manually through cancellation tokens because + // HttpClient does not properly timeout while reading chunked data + client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; + + logger = Logger.GetLogger(LoggingTarget.Network); + } + + public WebRequest(string url = null, params object[] args) + { + if (!string.IsNullOrEmpty(url)) + Url = args.Length == 0 ? url : string.Format(url, args); + } + + ~WebRequest() + { + Dispose(false); + } + + private int responseBytesRead; + + private const int buffer_size = 32768; + private byte[] buffer; + + private MemoryStream rawContent; + + public string ContentType; + + protected virtual Stream CreateOutputStream() + { + return new MemoryStream(); + } + + public Stream ResponseStream; + + public string ResponseString + { + get + { + try + { + ResponseStream.Seek(0, SeekOrigin.Begin); + StreamReader r = new StreamReader(ResponseStream, Encoding.UTF8); + return r.ReadToEnd(); + } + catch + { + return null; + } + } + } + + public byte[] ResponseData + { + get + { + try + { + byte[] data = new byte[ResponseStream.Length]; + ResponseStream.Seek(0, SeekOrigin.Begin); + ResponseStream.Read(data, 0, data.Length); + return data; + } + catch + { + return null; + } + } + } + + public HttpResponseHeaders ResponseHeaders => response.Headers; + + private CancellationTokenSource abortToken; + private CancellationTokenSource timeoutToken; + + private LengthTrackingStream requestStream; + private HttpResponseMessage response; + + private long contentLength => requestStream?.Length ?? 0; + + private const string form_boundary = "-----------------------------28947758029299"; + + private const string form_content_type = "multipart/form-data; boundary=" + form_boundary; + + /// + /// Performs the request asynchronously. + /// + public async Task PerformAsync() + { + if (Completed) + throw new InvalidOperationException($"The {nameof(WebRequest)} has already been run."); + try + { + await Task.Factory.StartNew(internalPerform, TaskCreationOptions.LongRunning); + } + catch (AggregateException ae) + { + ae.RethrowIfSingular(); + } + } + + private void internalPerform() + { + using (abortToken = new CancellationTokenSource()) + using (timeoutToken = new CancellationTokenSource()) + using (var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(abortToken.Token, timeoutToken.Token)) + { + try + { + PrePerform(); + + HttpRequestMessage request; + + switch (Method) + { + default: + throw new InvalidOperationException($"HTTP method {Method} is currently not supported"); + case HttpMethod.GET: + if (files.Count > 0) + throw new InvalidOperationException($"Cannot use {nameof(AddFile)} in a GET request. Please set the {nameof(Method)} to POST."); + + StringBuilder requestParameters = new StringBuilder(); + foreach (var p in parameters) + requestParameters.Append($@"{p.Key}={p.Value}&"); + string requestString = requestParameters.ToString().TrimEnd('&'); + + request = new HttpRequestMessage(System.Net.Http.HttpMethod.Get, string.IsNullOrEmpty(requestString) ? Url : $"{Url}?{requestString}"); + break; + case HttpMethod.POST: + request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, Url); + + Stream postContent; + + if (rawContent != null) + { + if (parameters.Count > 0) + throw new InvalidOperationException($"Cannot use {nameof(AddRaw)} in conjunction with {nameof(AddParameter)}"); + if (files.Count > 0) + throw new InvalidOperationException($"Cannot use {nameof(AddRaw)} in conjunction with {nameof(AddFile)}"); + + postContent = new MemoryStream(); + rawContent.Position = 0; + rawContent.CopyTo(postContent); + postContent.Position = 0; + } + else + { + if (!string.IsNullOrEmpty(ContentType) && ContentType != form_content_type) + throw new InvalidOperationException($"Cannot use custom {nameof(ContentType)} in a POST request."); + + ContentType = form_content_type; + + var formData = new MultipartFormDataContent(form_boundary); + + foreach (var p in parameters) + formData.Add(new StringContent(p.Value), p.Key); + + foreach (var p in files) + { + var byteContent = new ByteArrayContent(p.Value); + byteContent.Headers.Add("Content-Type", "application/octet-stream"); + formData.Add(byteContent, p.Key, p.Key); + } + + postContent = formData.ReadAsStreamAsync().Result; + } + + requestStream = new LengthTrackingStream(postContent); + requestStream.BytesRead.ValueChanged += v => + { + reportForwardProgress(); + UploadProgress?.Invoke(v, contentLength); + }; + + request.Content = new StreamContent(requestStream); + if (!string.IsNullOrEmpty(ContentType)) + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentType); + break; + } + + if (!string.IsNullOrEmpty(Accept)) + request.Headers.Accept.TryParseAdd(Accept); + + foreach (var kvp in headers) + request.Headers.Add(kvp.Key, kvp.Value); + + reportForwardProgress(); + + using (request) + { + response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedToken.Token).Result; + + ResponseStream = CreateOutputStream(); + + switch (Method) + { + case HttpMethod.GET: + //GETs are easy + beginResponse(linkedToken.Token); + break; + case HttpMethod.POST: + reportForwardProgress(); + UploadProgress?.Invoke(0, contentLength); + + beginResponse(linkedToken.Token); + break; + } + } + } + catch (Exception) when (timeoutToken.IsCancellationRequested) + { + Complete(new WebException($"Request to {Url} timed out after {timeSinceLastAction / 1000} seconds idle (read {responseBytesRead} bytes, retried {RetryCount} times).", WebExceptionStatus.Timeout)); + } + catch (Exception) when (abortToken.IsCancellationRequested) + { + Complete(new WebException($"Request to {Url} aborted by user.", WebExceptionStatus.RequestCanceled)); + } + catch (Exception e) + { + if (Completed) + // we may be coming from one of the exception blocks handled above (as Complete will rethrow all exceptions). + throw; + + Complete(e); + } + } + } + + /// + /// Performs the request synchronously. + /// + public void Perform() + { + try + { + PerformAsync().Wait(); + } + catch (AggregateException ae) + { + ae.RethrowIfSingular(); + } + } + + /// + /// Task to run direct before performing the request. + /// + protected virtual void PrePerform() + { + } + + private void beginResponse(CancellationToken cancellationToken) + { + using (var responseStream = response.Content.ReadAsStreamAsync().Result) + { + reportForwardProgress(); + Started?.Invoke(); + + buffer = new byte[buffer_size]; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + int read = responseStream.Read(buffer, 0, buffer_size); + + reportForwardProgress(); + + if (read > 0) + { + ResponseStream.Write(buffer, 0, read); + responseBytesRead += read; + DownloadProgress?.Invoke(responseBytesRead, response.Content.Headers.ContentLength ?? responseBytesRead); + } + else + { + ResponseStream.Seek(0, SeekOrigin.Begin); + Complete(); + break; + } + } + } + } + + protected virtual void Complete(Exception e = null) + { + if (Aborted) + return; + + var we = e as WebException; + + bool allowRetry = AllowRetryOnTimeout; + + if (e != null) + allowRetry &= we?.Status == WebExceptionStatus.Timeout; + else if (!response.IsSuccessStatusCode) + { + e = new WebException(response.StatusCode.ToString()); + + switch (response.StatusCode) + { + case HttpStatusCode.GatewayTimeout: + case HttpStatusCode.RequestTimeout: + break; + case HttpStatusCode.NotFound: + case HttpStatusCode.MethodNotAllowed: + case HttpStatusCode.Forbidden: + allowRetry = false; + break; + case HttpStatusCode.Unauthorized: + allowRetry = false; + break; + } + } + + if (e != null) + { + if (allowRetry && RetryCount < MAX_RETRIES && responseBytesRead == 0) + { + RetryCount++; + + logger.Add($@"Request to {Url} failed with {e} (retrying {RetryCount}/{MAX_RETRIES})."); + + //do a retry + internalPerform(); + return; + } + + logger.Add($"Request to {Url} failed with {e}."); + } + else + logger.Add($@"Request to {Url} successfully completed!"); + + try + { + ProcessResponse(); + } + catch (Exception se) + { + logger.Add($"Processing response from {Url} failed with {se}."); + e = e == null ? se : new AggregateException(e, se); + } + + Completed = true; + + if (e == null) + { + Finished?.Invoke(); + } + else + { + Failed?.Invoke(e); + Aborted = true; + throw e; + } + } + + /// + /// Performs any post-processing of the response. + /// Exceptions thrown in this method will be passed to . + /// + protected virtual void ProcessResponse() + { + } + + /// + /// Forcefully abort the request. + /// + public void Abort() + { + if (Aborted || Completed) return; + + Aborted = true; + Completed = true; + + try + { + abortToken?.Cancel(); + } + catch (ObjectDisposedException) + { + } + } + + /// + /// Adds a raw POST body to this request. + /// This may not be used in conjunction with and . + /// + /// The text. + public void AddRaw(string text) + { + AddRaw(Encoding.UTF8.GetBytes(text)); + } + + /// + /// Adds a raw POST body to this request. + /// This may not be used in conjunction with and . + /// + /// The raw data. + public void AddRaw(byte[] bytes) + { + AddRaw(new MemoryStream(bytes)); + } + + /// + /// Adds a raw POST body to this request. + /// This may not be used in conjunction with and . + /// + /// The stream containing the raw data. This stream will _not_ be finalized by this request. + public void AddRaw(Stream stream) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + if (rawContent == null) + rawContent = new MemoryStream(); + + stream.CopyTo(rawContent); + } + + /// + /// Add a new FILE parameter to this request. Replaces any existing file with the same name. + /// This may not be used in conjunction with . GET requests may not contain files. + /// + /// The name of the file. This becomes the name of the file in a multi-part form POST content. + /// The file data. + public void AddFile(string name, byte[] data) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (data == null) throw new ArgumentNullException(nameof(data)); + + files[name] = data; + } + + /// + /// Add a new POST parameter to this request. Replaces any existing parameter with the same name. + /// This may not be used in conjunction with . + /// + /// The name of the parameter. + /// The parameter value. + public void AddParameter(string name, string value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (value == null) throw new ArgumentNullException(nameof(value)); + + parameters[name] = value; + } + + /// + /// Adds a new header to this request. Replaces any existing header with the same name. + /// + /// The name of the header. + /// The header value. + public void AddHeader(string name, string value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (value == null) throw new ArgumentNullException(nameof(value)); + + headers[name] = value; + } + + #region Timeout Handling + + private long lastAction; + + private long timeSinceLastAction => (DateTime.Now.Ticks - lastAction) / TimeSpan.TicksPerMillisecond; + + private void reportForwardProgress() + { + lastAction = DateTime.Now.Ticks; + timeoutToken.CancelAfter(Timeout); + } + + #endregion + + #region IDisposable Support + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + isDisposed = true; + + Abort(); + + requestStream?.Dispose(); + response?.Dispose(); + + if (!(ResponseStream is MemoryStream)) + ResponseStream?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + private class LengthTrackingStream : Stream + { + public readonly BindableLong BytesRead = new BindableLong(); + + private readonly Stream baseStream; + + public LengthTrackingStream(Stream baseStream) + { + this.baseStream = baseStream; + } + + public override void Flush() + { + baseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = baseStream.Read(buffer, offset, count); + BytesRead.Value += read; + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return baseStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + baseStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + baseStream.Write(buffer, offset, count); + } + + public override bool CanRead => baseStream.CanRead; + public override bool CanSeek => baseStream.CanSeek; + public override bool CanWrite => baseStream.CanWrite; + public override long Length => baseStream.Length; + + public override long Position + { + get { return baseStream.Position; } + set { baseStream.Position = value; } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + baseStream.Dispose(); + } + } + } +} diff --git a/osu.Framework/IO/Serialization/SortedListConverter.cs b/osu.Framework/IO/Serialization/SortedListConverter.cs index 303454c2e..7af37ef23 100644 --- a/osu.Framework/IO/Serialization/SortedListConverter.cs +++ b/osu.Framework/IO/Serialization/SortedListConverter.cs @@ -1,30 +1,30 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using Newtonsoft.Json; -using osu.Framework.Lists; - -namespace osu.Framework.IO.Serialization -{ - public class SortedListConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => typeof(ISortedList).IsAssignableFrom(objectType); - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var list = (ISortedList)value; - list.SerializeTo(writer, serializer); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (!(existingValue is ISortedList iList)) - iList = (ISortedList)Activator.CreateInstance(objectType); - - iList.DeserializeFrom(reader, serializer); - - return iList; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using Newtonsoft.Json; +using osu.Framework.Lists; + +namespace osu.Framework.IO.Serialization +{ + public class SortedListConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => typeof(ISortedList).IsAssignableFrom(objectType); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var list = (ISortedList)value; + list.SerializeTo(writer, serializer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (!(existingValue is ISortedList iList)) + iList = (ISortedList)Activator.CreateInstance(objectType); + + iList.DeserializeFrom(reader, serializer); + + return iList; + } + } +} diff --git a/osu.Framework/IO/Stores/CachedResourceStore.cs b/osu.Framework/IO/Stores/CachedResourceStore.cs index 96bf7ad28..8c0829e30 100644 --- a/osu.Framework/IO/Stores/CachedResourceStore.cs +++ b/osu.Framework/IO/Stores/CachedResourceStore.cs @@ -1,108 +1,108 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.IO.Stores -{ - public class CachedResourceStore : ResourceStore - { - private readonly Dictionary cache = new Dictionary(); - - /// - /// Initializes a resource store with no stores. - /// - public CachedResourceStore() - { - } - - /// - /// Initializes a resource store with a single store. - /// - /// A collection of stores to add. - public CachedResourceStore(IResourceStore[] stores) - : base(stores) - { - } - - /// - /// Initializes a resource store with a collection of stores. - /// - /// The store. - public CachedResourceStore(IResourceStore store) - : base(store) - { - } - - /// - /// Notifies a bound delegate that the resource has changed. - /// - /// The resource that has changed. - protected override void NotifyChanged(string name) - { - cache.Remove(name); - base.NotifyChanged(name); - } - - /// - /// Adds a resource store to this store. - /// - /// The store to add. - public override void AddStore(IResourceStore store) - { - base.AddStore(store); - - ChangeableResourceStore crm = store as ChangeableResourceStore; - if (crm != null) - crm.OnChanged += NotifyChanged; - } - - /// - /// Removes a store from this store. - /// - /// The store to remove. - public override void RemoveStore(IResourceStore store) - { - base.RemoveStore(store); - - ChangeableResourceStore crm = store as ChangeableResourceStore; - if (crm != null) - crm.OnChanged -= NotifyChanged; - } - - /// - /// Retrieves an object from the store. - /// - /// The name of the object. - /// The object. - public override T Get(string name) - { - if (cache.TryGetValue(name, out T result)) - return result; - - result = base.Get(name); - - if (result != null) - cache[name] = result; - - return result; - } - - /// - /// Releases a resource from the cache. - /// - /// The resource to release. - public void Release(string name) - { - cache.Remove(name); - } - - /// - /// Releases all resources stored in the cache. - /// - public void ResetCache() - { - cache.Clear(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.IO.Stores +{ + public class CachedResourceStore : ResourceStore + { + private readonly Dictionary cache = new Dictionary(); + + /// + /// Initializes a resource store with no stores. + /// + public CachedResourceStore() + { + } + + /// + /// Initializes a resource store with a single store. + /// + /// A collection of stores to add. + public CachedResourceStore(IResourceStore[] stores) + : base(stores) + { + } + + /// + /// Initializes a resource store with a collection of stores. + /// + /// The store. + public CachedResourceStore(IResourceStore store) + : base(store) + { + } + + /// + /// Notifies a bound delegate that the resource has changed. + /// + /// The resource that has changed. + protected override void NotifyChanged(string name) + { + cache.Remove(name); + base.NotifyChanged(name); + } + + /// + /// Adds a resource store to this store. + /// + /// The store to add. + public override void AddStore(IResourceStore store) + { + base.AddStore(store); + + ChangeableResourceStore crm = store as ChangeableResourceStore; + if (crm != null) + crm.OnChanged += NotifyChanged; + } + + /// + /// Removes a store from this store. + /// + /// The store to remove. + public override void RemoveStore(IResourceStore store) + { + base.RemoveStore(store); + + ChangeableResourceStore crm = store as ChangeableResourceStore; + if (crm != null) + crm.OnChanged -= NotifyChanged; + } + + /// + /// Retrieves an object from the store. + /// + /// The name of the object. + /// The object. + public override T Get(string name) + { + if (cache.TryGetValue(name, out T result)) + return result; + + result = base.Get(name); + + if (result != null) + cache[name] = result; + + return result; + } + + /// + /// Releases a resource from the cache. + /// + /// The resource to release. + public void Release(string name) + { + cache.Remove(name); + } + + /// + /// Releases all resources stored in the cache. + /// + public void ResetCache() + { + cache.Clear(); + } + } +} diff --git a/osu.Framework/IO/Stores/ChangeableResourceStore.cs b/osu.Framework/IO/Stores/ChangeableResourceStore.cs index 8ae40cfdd..ec24c12d1 100644 --- a/osu.Framework/IO/Stores/ChangeableResourceStore.cs +++ b/osu.Framework/IO/Stores/ChangeableResourceStore.cs @@ -1,17 +1,17 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.IO.Stores -{ - public class ChangeableResourceStore : ResourceStore - { - public event Action OnChanged; - - protected void TriggerOnChanged(string name) - { - OnChanged?.Invoke(name); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.IO.Stores +{ + public class ChangeableResourceStore : ResourceStore + { + public event Action OnChanged; + + protected void TriggerOnChanged(string name) + { + OnChanged?.Invoke(name); + } + } +} diff --git a/osu.Framework/IO/Stores/DllResourceStore.cs b/osu.Framework/IO/Stores/DllResourceStore.cs index bba6a5f82..03492adad 100644 --- a/osu.Framework/IO/Stores/DllResourceStore.cs +++ b/osu.Framework/IO/Stores/DllResourceStore.cs @@ -1,42 +1,42 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.IO; -using System.Reflection; - -namespace osu.Framework.IO.Stores -{ - public class DllResourceStore : IResourceStore - { - private readonly Assembly assembly; - private readonly string space; - - public DllResourceStore(string dllName) - { - assembly = Assembly.LoadFrom(dllName); - space = Path.GetFileNameWithoutExtension(dllName); - } - - public byte[] Get(string name) - { - using (Stream input = GetStream(name)) - { - if (input == null) - return null; - - byte[] buffer = new byte[input.Length]; - input.Read(buffer, 0, buffer.Length); - return buffer; - } - } - - public Stream GetStream(string name) - { - var split = name.Split('/'); - for (int i = 0; i < split.Length - 1; i++) - split[i] = split[i].Replace('-', '_'); - - return assembly?.GetManifestResourceStream($@"{space}.{string.Join(".", split)}"); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.IO; +using System.Reflection; + +namespace osu.Framework.IO.Stores +{ + public class DllResourceStore : IResourceStore + { + private readonly Assembly assembly; + private readonly string space; + + public DllResourceStore(string dllName) + { + assembly = Assembly.LoadFrom(dllName); + space = Path.GetFileNameWithoutExtension(dllName); + } + + public byte[] Get(string name) + { + using (Stream input = GetStream(name)) + { + if (input == null) + return null; + + byte[] buffer = new byte[input.Length]; + input.Read(buffer, 0, buffer.Length); + return buffer; + } + } + + public Stream GetStream(string name) + { + var split = name.Split('/'); + for (int i = 0; i < split.Length - 1; i++) + split[i] = split[i].Replace('-', '_'); + + return assembly?.GetManifestResourceStream($@"{space}.{string.Join(".", split)}"); + } + } +} diff --git a/osu.Framework/IO/Stores/FileSystemResourceStore.cs b/osu.Framework/IO/Stores/FileSystemResourceStore.cs index a2de58991..cebbe73dd 100644 --- a/osu.Framework/IO/Stores/FileSystemResourceStore.cs +++ b/osu.Framework/IO/Stores/FileSystemResourceStore.cs @@ -1,67 +1,67 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; - -namespace osu.Framework.IO.Stores -{ - public class FileSystemResourceStore : ChangeableResourceStore, IDisposable - { - private readonly FileSystemWatcher watcher; - private readonly string directory; - - private bool isDisposed; - - public FileSystemResourceStore(string directory) - { - this.directory = directory; - - watcher = new FileSystemWatcher(directory) - { - EnableRaisingEvents = true - }; - watcher.Renamed += watcherChanged; - watcher.Changed += watcherChanged; - watcher.Created += watcherChanged; - } - - #region Disposal - - ~FileSystemResourceStore() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (isDisposed) - return; - isDisposed = true; - - watcher.Renamed -= watcherChanged; - watcher.Changed -= watcherChanged; - watcher.Created -= watcherChanged; - - watcher.Dispose(); - } - - #endregion - - private void watcherChanged(object sender, FileSystemEventArgs e) - { - TriggerOnChanged(e.FullPath.Replace(directory, string.Empty)); - } - - public override byte[] Get(string name) - { - return System.IO.File.ReadAllBytes(Path.Combine(directory, name)); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; + +namespace osu.Framework.IO.Stores +{ + public class FileSystemResourceStore : ChangeableResourceStore, IDisposable + { + private readonly FileSystemWatcher watcher; + private readonly string directory; + + private bool isDisposed; + + public FileSystemResourceStore(string directory) + { + this.directory = directory; + + watcher = new FileSystemWatcher(directory) + { + EnableRaisingEvents = true + }; + watcher.Renamed += watcherChanged; + watcher.Changed += watcherChanged; + watcher.Created += watcherChanged; + } + + #region Disposal + + ~FileSystemResourceStore() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + + watcher.Renamed -= watcherChanged; + watcher.Changed -= watcherChanged; + watcher.Created -= watcherChanged; + + watcher.Dispose(); + } + + #endregion + + private void watcherChanged(object sender, FileSystemEventArgs e) + { + TriggerOnChanged(e.FullPath.Replace(directory, string.Empty)); + } + + public override byte[] Get(string name) + { + return System.IO.File.ReadAllBytes(Path.Combine(directory, name)); + } + } +} diff --git a/osu.Framework/IO/Stores/GlyphStore.cs b/osu.Framework/IO/Stores/GlyphStore.cs index 12b776eb0..a25ef01c5 100644 --- a/osu.Framework/IO/Stores/GlyphStore.cs +++ b/osu.Framework/IO/Stores/GlyphStore.cs @@ -1,205 +1,205 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using Cyotek.Drawing.BitmapFont; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Textures; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Logging; - -namespace osu.Framework.IO.Stores -{ - public class GlyphStore : IResourceStore - { - private readonly string assetName; - - private readonly string fontName; - - private const float default_size = 96; - - private readonly ResourceStore store; - private BitmapFont font; - - private readonly TimedExpiryCache texturePages = new TimedExpiryCache(); - - private Task fontLoadTask; - - public GlyphStore(ResourceStore store, string assetName = null, bool precache = false) - { - this.store = store; - this.assetName = assetName; - - fontName = assetName?.Split('/').Last(); - - fontLoadTask = readFontMetadataAsync(precache); - } - - private async Task readFontMetadataAsync(bool precache) - { - await Task.Run(() => - { - try - { - font = new BitmapFont(); - using (var s = store.GetStream($@"{assetName}.fnt")) - font.LoadText(s); - - if (precache) - for (int i = 0; i < font.Pages.Length; i++) - getTexturePage(i); - } - catch - { - Logger.Log($"Couldn't load font asset from {assetName}."); - throw; - } - }); - - fontLoadTask = null; - } - - public bool HasGlyph(char c) => font.Characters.ContainsKey(c); - public int GetBaseHeight() => font.BaseHeight; - public int? GetBaseHeight(string name) - { - if (name != fontName) - return null; - - return font.BaseHeight; - } - - public RawTexture Get(string name) - { - if (name.Length > 1 && !name.StartsWith($@"{fontName}/", StringComparison.Ordinal)) - return null; - - try - { - fontLoadTask?.Wait(); - } - catch - { - return null; - } - - if (!font.Characters.TryGetValue(name.Last(), out Character c)) - return null; - - RawTexture page = getTexturePage(c.TexturePage); - loadedGlyphCount++; - - int width = c.Bounds.Width + c.Offset.X + 1; - int height = c.Bounds.Height + c.Offset.Y + 1; - int length = width * height * 4; - byte[] pixels = new byte[length]; - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int desti = y * width * 4 + x * 4; - if (x >= c.Offset.X && y >= c.Offset.Y - && x - c.Offset.X < c.Bounds.Width && y - c.Offset.Y < c.Bounds.Height) - { - int srci = (c.Bounds.Y + y - c.Offset.Y) * page.Width * 4 - + (c.Bounds.X + x - c.Offset.X) * 4; - pixels[desti] = page.Pixels[srci]; - pixels[desti + 1] = page.Pixels[srci + 1]; - pixels[desti + 2] = page.Pixels[srci + 2]; - pixels[desti + 3] = page.Pixels[srci + 3]; - } - else - { - pixels[desti] = 255; - pixels[desti + 1] = 255; - pixels[desti + 2] = 255; - pixels[desti + 3] = 0; - } - } - } - - return new RawTexture - { - Pixels = pixels, - PixelFormat = OpenTK.Graphics.ES30.PixelFormat.Rgba, - Width = width, - Height = height, - }; - } - - private RawTexture getTexturePage(int texturePage) - { - if (!texturePages.TryGetValue(texturePage, out RawTexture t)) - { - loadedPageCount++; - using (var stream = store.GetStream($@"{assetName}_{texturePage.ToString().PadLeft((font.Pages.Length - 1).ToString().Length, '0')}.png")) - texturePages.Add(texturePage, t = RawTexture.FromStream(stream)); - } - - return t; - } - - public Stream GetStream(string name) - { - throw new NotSupportedException(); - } - - private int loadedPageCount; - private int loadedGlyphCount; - - public override string ToString() => $@"GlyphStore({assetName}) LoadedPages:{loadedPageCount} LoadedGlyphs:{loadedGlyphCount}"; - } - - public class FontStore : TextureStore - { - private readonly List glyphStores = new List(); - - public FontStore() - { - } - - public FontStore(GlyphStore glyphStore) - : base(glyphStore) - { - } - - public override void AddStore(IResourceStore store) - { - var gs = store as GlyphStore; - if (gs != null) - glyphStores.Add(gs); - base.AddStore(store); - } - public override void RemoveStore(IResourceStore store) - { - var gs = store as GlyphStore; - if (gs != null) - glyphStores.Remove(gs); - base.RemoveStore(store); - } - - public float? GetBaseHeight(char c) - { - foreach (var store in glyphStores) - { - if (store.HasGlyph(c)) - return store.GetBaseHeight() / ScaleAdjust; - } - return null; - } - public float? GetBaseHeight(string fontName) - { - foreach (var store in glyphStores) - { - var bh = store.GetBaseHeight(fontName); - if (bh.HasValue) - return bh.Value / ScaleAdjust; - } - return null; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using Cyotek.Drawing.BitmapFont; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Textures; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Logging; + +namespace osu.Framework.IO.Stores +{ + public class GlyphStore : IResourceStore + { + private readonly string assetName; + + private readonly string fontName; + + private const float default_size = 96; + + private readonly ResourceStore store; + private BitmapFont font; + + private readonly TimedExpiryCache texturePages = new TimedExpiryCache(); + + private Task fontLoadTask; + + public GlyphStore(ResourceStore store, string assetName = null, bool precache = false) + { + this.store = store; + this.assetName = assetName; + + fontName = assetName?.Split('/').Last(); + + fontLoadTask = readFontMetadataAsync(precache); + } + + private async Task readFontMetadataAsync(bool precache) + { + await Task.Run(() => + { + try + { + font = new BitmapFont(); + using (var s = store.GetStream($@"{assetName}.fnt")) + font.LoadText(s); + + if (precache) + for (int i = 0; i < font.Pages.Length; i++) + getTexturePage(i); + } + catch + { + Logger.Log($"Couldn't load font asset from {assetName}."); + throw; + } + }); + + fontLoadTask = null; + } + + public bool HasGlyph(char c) => font.Characters.ContainsKey(c); + public int GetBaseHeight() => font.BaseHeight; + public int? GetBaseHeight(string name) + { + if (name != fontName) + return null; + + return font.BaseHeight; + } + + public RawTexture Get(string name) + { + if (name.Length > 1 && !name.StartsWith($@"{fontName}/", StringComparison.Ordinal)) + return null; + + try + { + fontLoadTask?.Wait(); + } + catch + { + return null; + } + + if (!font.Characters.TryGetValue(name.Last(), out Character c)) + return null; + + RawTexture page = getTexturePage(c.TexturePage); + loadedGlyphCount++; + + int width = c.Bounds.Width + c.Offset.X + 1; + int height = c.Bounds.Height + c.Offset.Y + 1; + int length = width * height * 4; + byte[] pixels = new byte[length]; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int desti = y * width * 4 + x * 4; + if (x >= c.Offset.X && y >= c.Offset.Y + && x - c.Offset.X < c.Bounds.Width && y - c.Offset.Y < c.Bounds.Height) + { + int srci = (c.Bounds.Y + y - c.Offset.Y) * page.Width * 4 + + (c.Bounds.X + x - c.Offset.X) * 4; + pixels[desti] = page.Pixels[srci]; + pixels[desti + 1] = page.Pixels[srci + 1]; + pixels[desti + 2] = page.Pixels[srci + 2]; + pixels[desti + 3] = page.Pixels[srci + 3]; + } + else + { + pixels[desti] = 255; + pixels[desti + 1] = 255; + pixels[desti + 2] = 255; + pixels[desti + 3] = 0; + } + } + } + + return new RawTexture + { + Pixels = pixels, + PixelFormat = OpenTK.Graphics.ES30.PixelFormat.Rgba, + Width = width, + Height = height, + }; + } + + private RawTexture getTexturePage(int texturePage) + { + if (!texturePages.TryGetValue(texturePage, out RawTexture t)) + { + loadedPageCount++; + using (var stream = store.GetStream($@"{assetName}_{texturePage.ToString().PadLeft((font.Pages.Length - 1).ToString().Length, '0')}.png")) + texturePages.Add(texturePage, t = RawTexture.FromStream(stream)); + } + + return t; + } + + public Stream GetStream(string name) + { + throw new NotSupportedException(); + } + + private int loadedPageCount; + private int loadedGlyphCount; + + public override string ToString() => $@"GlyphStore({assetName}) LoadedPages:{loadedPageCount} LoadedGlyphs:{loadedGlyphCount}"; + } + + public class FontStore : TextureStore + { + private readonly List glyphStores = new List(); + + public FontStore() + { + } + + public FontStore(GlyphStore glyphStore) + : base(glyphStore) + { + } + + public override void AddStore(IResourceStore store) + { + var gs = store as GlyphStore; + if (gs != null) + glyphStores.Add(gs); + base.AddStore(store); + } + public override void RemoveStore(IResourceStore store) + { + var gs = store as GlyphStore; + if (gs != null) + glyphStores.Remove(gs); + base.RemoveStore(store); + } + + public float? GetBaseHeight(char c) + { + foreach (var store in glyphStores) + { + if (store.HasGlyph(c)) + return store.GetBaseHeight() / ScaleAdjust; + } + return null; + } + public float? GetBaseHeight(string fontName) + { + foreach (var store in glyphStores) + { + var bh = store.GetBaseHeight(fontName); + if (bh.HasValue) + return bh.Value / ScaleAdjust; + } + return null; + } + } +} diff --git a/osu.Framework/IO/Stores/IResourceStore.cs b/osu.Framework/IO/Stores/IResourceStore.cs index 177f8eaba..8685ecb37 100644 --- a/osu.Framework/IO/Stores/IResourceStore.cs +++ b/osu.Framework/IO/Stores/IResourceStore.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.IO; - -namespace osu.Framework.IO.Stores -{ - public interface IResourceStore - { - /// - /// Retrieves an object from the store. - /// - /// The name of the object. - /// The object. - T Get(string name); - - Stream GetStream(string name); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.IO; + +namespace osu.Framework.IO.Stores +{ + public interface IResourceStore + { + /// + /// Retrieves an object from the store. + /// + /// The name of the object. + /// The object. + T Get(string name); + + Stream GetStream(string name); + } +} diff --git a/osu.Framework/IO/Stores/NamespacedResourceStore.cs b/osu.Framework/IO/Stores/NamespacedResourceStore.cs index 6c8b543d1..85cf94b56 100644 --- a/osu.Framework/IO/Stores/NamespacedResourceStore.cs +++ b/osu.Framework/IO/Stores/NamespacedResourceStore.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; - -namespace osu.Framework.IO.Stores -{ - public class NamespacedResourceStore : ResourceStore - { - public string Namespace; - - /// - /// Initializes a resource store with a single store. - /// - /// The store. - /// The namespace to add. - public NamespacedResourceStore(IResourceStore store, string ns) - : base(store) - { - Namespace = ns; - } - - protected override List GetFilenames(string name) - { - return base.GetFilenames($@"{Namespace}/{name}"); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Framework.IO.Stores +{ + public class NamespacedResourceStore : ResourceStore + { + public string Namespace; + + /// + /// Initializes a resource store with a single store. + /// + /// The store. + /// The namespace to add. + public NamespacedResourceStore(IResourceStore store, string ns) + : base(store) + { + Namespace = ns; + } + + protected override List GetFilenames(string name) + { + return base.GetFilenames($@"{Namespace}/{name}"); + } + } +} diff --git a/osu.Framework/IO/Stores/OnlineStore.cs b/osu.Framework/IO/Stores/OnlineStore.cs index 946fd60c8..81b294aaa 100644 --- a/osu.Framework/IO/Stores/OnlineStore.cs +++ b/osu.Framework/IO/Stores/OnlineStore.cs @@ -1,44 +1,44 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using System.Threading.Tasks; -using WebRequest = osu.Framework.IO.Network.WebRequest; - -namespace osu.Framework.IO.Stores -{ - public class OnlineStore : IResourceStore - { - public async Task GetAsync(string url) - { - if (!url.StartsWith(@"https://", StringComparison.Ordinal)) - return null; - - try - { - WebRequest req = new WebRequest($@"{url}"); - await req.PerformAsync(); - return req.ResponseData; - } - catch - { - return null; - } - } - - public byte[] Get(string url) - { - return GetAsync(url).Result; - } - - public Stream GetStream(string url) - { - var ret = Get(url); - - if (ret == null) return null; - - return new MemoryStream(ret); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using System.Threading.Tasks; +using WebRequest = osu.Framework.IO.Network.WebRequest; + +namespace osu.Framework.IO.Stores +{ + public class OnlineStore : IResourceStore + { + public async Task GetAsync(string url) + { + if (!url.StartsWith(@"https://", StringComparison.Ordinal)) + return null; + + try + { + WebRequest req = new WebRequest($@"{url}"); + await req.PerformAsync(); + return req.ResponseData; + } + catch + { + return null; + } + } + + public byte[] Get(string url) + { + return GetAsync(url).Result; + } + + public Stream GetStream(string url) + { + var ret = Get(url); + + if (ret == null) return null; + + return new MemoryStream(ret); + } + } +} diff --git a/osu.Framework/IO/Stores/ResourceStore.cs b/osu.Framework/IO/Stores/ResourceStore.cs index e004c29ff..7f6493c3a 100644 --- a/osu.Framework/IO/Stores/ResourceStore.cs +++ b/osu.Framework/IO/Stores/ResourceStore.cs @@ -1,165 +1,165 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace osu.Framework.IO.Stores -{ - public class ResourceStore : IResourceStore - { - private readonly Dictionary actionList = new Dictionary(); - - private readonly List> stores = new List>(); - - private readonly List searchExtensions = new List(); - - /// - /// Initializes a resource store with no stores. - /// - public ResourceStore() - { - } - - /// - /// Initializes a resource store with a single store. - /// - /// The store. - public ResourceStore(IResourceStore store = null) - { - if (store != null) - AddStore(store); - } - - /// - /// Initializes a resource store with a collection of stores. - /// - /// The collection of stores. - public ResourceStore(IResourceStore[] stores) - { - foreach (var resourceStore in stores.Cast>()) - AddStore(resourceStore); - } - - /// - /// Notifies a bound delegate that the resource has changed. - /// - /// The resource that has changed. - protected virtual void NotifyChanged(string name) - { - if (!actionList.TryGetValue(name, out Action action)) - return; - - action?.Invoke(); - } - - /// - /// Adds a resource store to this store. - /// - /// The store to add. - public virtual void AddStore(IResourceStore store) - { - stores.Add(store); - } - - /// - /// Removes a store from this store. - /// - /// The store to remove. - public virtual void RemoveStore(IResourceStore store) - { - stores.Remove(store); - } - - /// - /// Retrieves an object from the store. - /// - /// The name of the object. - /// The object. - public virtual T Get(string name) - { - List filenames = GetFilenames(name); - - // Cache miss - get the resource - foreach (IResourceStore store in stores) - { - foreach (string f in filenames) - { - T result = store.Get(f); - if (result != null) - return result; - } - } - - return default(T); - } - - public Stream GetStream(string name) - { - List filenames = GetFilenames(name); - - // Cache miss - get the resource - foreach (IResourceStore store in stores) - { - foreach (string f in filenames) - { - try - { - var result = store.GetStream(f); - if (result != null) - return result; - } - catch - { - } - } - } - - return null; - } - - protected virtual List GetFilenames(string name) - { - List filenames = new List - { - name - }; - //add file extension if it's missing. - if (!name.Contains(@".")) - foreach (string ext in searchExtensions) - filenames.Add($@"{name}.{ext}"); - - return filenames; - } - - /// - /// Binds a reload function to an object held by the store. - /// - /// The name of the object. - /// The reload function to bind. - public void BindReload(string name, Action onReload) - { - if (onReload == null) - return; - - // Check if there's already a reload action bound - if (actionList.ContainsKey(name)) - throw new InvalidOperationException($"A reload delegate is already bound to the resource '{name}'."); - - actionList[name] = onReload; - } - - /// - /// Add a file extension to automatically append to any lookups on this store. - /// - public void AddExtension(string extension) - { - extension = extension.Trim('.'); - - if (!searchExtensions.Contains(extension)) - searchExtensions.Add(extension); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace osu.Framework.IO.Stores +{ + public class ResourceStore : IResourceStore + { + private readonly Dictionary actionList = new Dictionary(); + + private readonly List> stores = new List>(); + + private readonly List searchExtensions = new List(); + + /// + /// Initializes a resource store with no stores. + /// + public ResourceStore() + { + } + + /// + /// Initializes a resource store with a single store. + /// + /// The store. + public ResourceStore(IResourceStore store = null) + { + if (store != null) + AddStore(store); + } + + /// + /// Initializes a resource store with a collection of stores. + /// + /// The collection of stores. + public ResourceStore(IResourceStore[] stores) + { + foreach (var resourceStore in stores.Cast>()) + AddStore(resourceStore); + } + + /// + /// Notifies a bound delegate that the resource has changed. + /// + /// The resource that has changed. + protected virtual void NotifyChanged(string name) + { + if (!actionList.TryGetValue(name, out Action action)) + return; + + action?.Invoke(); + } + + /// + /// Adds a resource store to this store. + /// + /// The store to add. + public virtual void AddStore(IResourceStore store) + { + stores.Add(store); + } + + /// + /// Removes a store from this store. + /// + /// The store to remove. + public virtual void RemoveStore(IResourceStore store) + { + stores.Remove(store); + } + + /// + /// Retrieves an object from the store. + /// + /// The name of the object. + /// The object. + public virtual T Get(string name) + { + List filenames = GetFilenames(name); + + // Cache miss - get the resource + foreach (IResourceStore store in stores) + { + foreach (string f in filenames) + { + T result = store.Get(f); + if (result != null) + return result; + } + } + + return default(T); + } + + public Stream GetStream(string name) + { + List filenames = GetFilenames(name); + + // Cache miss - get the resource + foreach (IResourceStore store in stores) + { + foreach (string f in filenames) + { + try + { + var result = store.GetStream(f); + if (result != null) + return result; + } + catch + { + } + } + } + + return null; + } + + protected virtual List GetFilenames(string name) + { + List filenames = new List + { + name + }; + //add file extension if it's missing. + if (!name.Contains(@".")) + foreach (string ext in searchExtensions) + filenames.Add($@"{name}.{ext}"); + + return filenames; + } + + /// + /// Binds a reload function to an object held by the store. + /// + /// The name of the object. + /// The reload function to bind. + public void BindReload(string name, Action onReload) + { + if (onReload == null) + return; + + // Check if there's already a reload action bound + if (actionList.ContainsKey(name)) + throw new InvalidOperationException($"A reload delegate is already bound to the resource '{name}'."); + + actionList[name] = onReload; + } + + /// + /// Add a file extension to automatically append to any lookups on this store. + /// + public void AddExtension(string extension) + { + extension = extension.Trim('.'); + + if (!searchExtensions.Contains(extension)) + searchExtensions.Add(extension); + } + } +} diff --git a/osu.Framework/IO/Stores/StorageBackedResourceStore.cs b/osu.Framework/IO/Stores/StorageBackedResourceStore.cs index bc97834dd..d79260d88 100644 --- a/osu.Framework/IO/Stores/StorageBackedResourceStore.cs +++ b/osu.Framework/IO/Stores/StorageBackedResourceStore.cs @@ -1,36 +1,36 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.IO; -using osu.Framework.Platform; - -namespace osu.Framework.IO.Stores -{ - /// - /// A resource store that uses an underlying backing. - /// - public class StorageBackedResourceStore : IResourceStore - { - private readonly Storage storage; - - public StorageBackedResourceStore(Storage storage) - { - this.storage = storage; - } - - public byte[] Get(string name) - { - using (Stream stream = storage.GetStream(name)) - { - byte[] buffer = new byte[stream.Length]; - stream.Read(buffer, 0, buffer.Length); - return buffer; - } - } - - public Stream GetStream(string name) - { - return storage.GetStream(name); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.IO; +using osu.Framework.Platform; + +namespace osu.Framework.IO.Stores +{ + /// + /// A resource store that uses an underlying backing. + /// + public class StorageBackedResourceStore : IResourceStore + { + private readonly Storage storage; + + public StorageBackedResourceStore(Storage storage) + { + this.storage = storage; + } + + public byte[] Get(string name) + { + using (Stream stream = storage.GetStream(name)) + { + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return buffer; + } + } + + public Stream GetStream(string name) + { + return storage.GetStream(name); + } + } +} diff --git a/osu.Framework/IStateful.cs b/osu.Framework/IStateful.cs index 46bf16aac..cc7fdfdb5 100644 --- a/osu.Framework/IStateful.cs +++ b/osu.Framework/IStateful.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework -{ - /// - /// An object which has a state and allows external consumers to change the current state. - /// - /// Generally an Enum type local to the class implementing this interface. - public interface IStateful - where T : struct, IComparable - { - /// - /// Invoked when the state of this has changed. - /// - event Action StateChanged; - - /// - /// The current state of this object. - /// - T State { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework +{ + /// + /// An object which has a state and allows external consumers to change the current state. + /// + /// Generally an Enum type local to the class implementing this interface. + public interface IStateful + where T : struct, IComparable + { + /// + /// Invoked when the state of this has changed. + /// + event Action StateChanged; + + /// + /// The current state of this object. + /// + T State { get; set; } + } +} diff --git a/osu.Framework/IUpdateable.cs b/osu.Framework/IUpdateable.cs index 2d6f8e547..1f9fd98b2 100644 --- a/osu.Framework/IUpdateable.cs +++ b/osu.Framework/IUpdateable.cs @@ -1,10 +1,10 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework -{ - public interface IUpdateable - { - void Update(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework +{ + public interface IUpdateable + { + void Update(); + } +} diff --git a/osu.Framework/Input/Bindings/IKeyBindingHandler.cs b/osu.Framework/Input/Bindings/IKeyBindingHandler.cs index 60f88cf58..d06d72e64 100644 --- a/osu.Framework/Input/Bindings/IKeyBindingHandler.cs +++ b/osu.Framework/Input/Bindings/IKeyBindingHandler.cs @@ -1,31 +1,31 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; - -namespace osu.Framework.Input.Bindings -{ - /// - /// A drawable that handles key bindings. - /// - /// The type of bindings, commonly an enum. - public interface IKeyBindingHandler : IDrawable - where T : struct - { - /// - /// Triggered when an action is pressed. - /// - /// The action. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - bool OnPressed(T action); - - /// - /// Triggered when an action is released. - /// - /// The action. - /// True if this Drawable handled the event. If false, then the event - /// is propagated up the scene graph to the next eligible Drawable. - bool OnReleased(T action); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; + +namespace osu.Framework.Input.Bindings +{ + /// + /// A drawable that handles key bindings. + /// + /// The type of bindings, commonly an enum. + public interface IKeyBindingHandler : IDrawable + where T : struct + { + /// + /// Triggered when an action is pressed. + /// + /// The action. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + bool OnPressed(T action); + + /// + /// Triggered when an action is released. + /// + /// The action. + /// True if this Drawable handled the event. If false, then the event + /// is propagated up the scene graph to the next eligible Drawable. + bool OnReleased(T action); + } +} diff --git a/osu.Framework/Input/Bindings/InputKey.cs b/osu.Framework/Input/Bindings/InputKey.cs index 011ba6de9..de1afe062 100644 --- a/osu.Framework/Input/Bindings/InputKey.cs +++ b/osu.Framework/Input/Bindings/InputKey.cs @@ -1,615 +1,615 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Input.Bindings -{ - /// - /// A collection of keys, mouse and other controllers' buttons. - /// - public enum InputKey - { - /// - /// No key pressed. - /// - None = 0, - /// - /// The shift key. - /// - Shift = 1, - /// - /// The control key. - /// - Control = 3, - /// - /// The alt key. - /// - Alt = 5, - /// - /// The win key. - /// - Super = 7, - /// - /// The menu key. - /// - Menu = 9, - /// - /// The F1 key. - /// - F1 = 10, - /// - /// The F2 key. - /// - F2 = 11, - /// - /// The F3 key. - /// - F3 = 12, - /// - /// The F4 key. - /// - F4 = 13, - /// - /// The F5 key. - /// - F5 = 14, - /// - /// The F6 key. - /// - F6 = 15, - /// - /// The F7 key. - /// - F7 = 16, - /// - /// The F8 key. - /// - F8 = 17, - /// - /// The F9 key. - /// - F9 = 18, - /// - /// The F10 key. - /// - F10 = 19, - /// - /// The F11 key. - /// - F11 = 20, - /// - /// The F12 key. - /// - F12 = 21, - /// - /// The F13 key. - /// - F13 = 22, - /// - /// The F14 key. - /// - F14 = 23, - /// - /// The F15 key. - /// - F15 = 24, - /// - /// The F16 key. - /// - F16 = 25, - /// - /// The F17 key. - /// - F17 = 26, - /// - /// The F18 key. - /// - F18 = 27, - /// - /// The F19 key. - /// - F19 = 28, - /// - /// The F20 key. - /// - F20 = 29, - /// - /// The F21 key. - /// - F21 = 30, - /// - /// The F22 key. - /// - F22 = 31, - /// - /// The F23 key. - /// - F23 = 32, - /// - /// The F24 key. - /// - F24 = 33, - /// - /// The F25 key. - /// - F25 = 34, - /// - /// The F26 key. - /// - F26 = 35, - /// - /// The F27 key. - /// - F27 = 36, - /// - /// The F28 key. - /// - F28 = 37, - /// - /// The F29 key. - /// - F29 = 38, - /// - /// The F30 key. - /// - F30 = 39, - /// - /// The F31 key. - /// - F31 = 40, - /// - /// The F32 key. - /// - F32 = 41, - /// - /// The F33 key. - /// - F33 = 42, - /// - /// The F34 key. - /// - F34 = 43, - /// - /// The F35 key. - /// - F35 = 44, - /// - /// The up arrow key. - /// - Up = 45, - /// - /// The down arrow key. - /// - Down = 46, - /// - /// The left arrow key. - /// - Left = 47, - /// - /// The right arrow key. - /// - Right = 48, - /// - /// The enter key. - /// - Enter = 49, - /// - /// The escape key. - /// - Escape = 50, - /// - /// The space key. - /// - Space = 51, - /// - /// The tab key. - /// - Tab = 52, - /// - /// The backspace key. - /// - BackSpace = 53, - /// - /// The backspace key (equivalent to BackSpace). - /// - Back = 53, - /// - /// The insert key. - /// - Insert = 54, - /// - /// The delete key. - /// - Delete = 55, - /// - /// The page up key. - /// - PageUp = 56, - /// - /// The page down key. - /// - PageDown = 57, - /// - /// The home key. - /// - Home = 58, - /// - /// The end key. - /// - End = 59, - /// - /// The caps lock key. - /// - CapsLock = 60, - /// - /// The scroll lock key. - /// - ScrollLock = 61, - /// - /// The print screen key. - /// - PrintScreen = 62, - /// - /// The pause key. - /// - Pause = 63, - /// - /// The num lock key. - /// - NumLock = 64, - /// - /// The clear key (Keypad5 with NumLock disabled, on typical keyboards). - /// - Clear = 65, - /// - /// The sleep key. - /// - Sleep = 66, - /// - /// The keypad 0 key. - /// - Keypad0 = 67, - /// - /// The keypad 1 key. - /// - Keypad1 = 68, - /// - /// The keypad 2 key. - /// - Keypad2 = 69, - /// - /// The keypad 3 key. - /// - Keypad3 = 70, - /// - /// The keypad 4 key. - /// - Keypad4 = 71, - /// - /// The keypad 5 key. - /// - Keypad5 = 72, - /// - /// The keypad 6 key. - /// - Keypad6 = 73, - /// - /// The keypad 7 key. - /// - Keypad7 = 74, - /// - /// The keypad 8 key. - /// - Keypad8 = 75, - /// - /// The keypad 9 key. - /// - Keypad9 = 76, - /// - /// The keypad divide key. - /// - KeypadDivide = 77, - /// - /// The keypad multiply key. - /// - KeypadMultiply = 78, - /// - /// The keypad subtract key. - /// - KeypadSubtract = 79, - /// - /// The keypad minus key (equivalent to KeypadSubtract). - /// - KeypadMinus = 79, - /// - /// The keypad add key. - /// - KeypadAdd = 80, - /// - /// The keypad plus key (equivalent to KeypadAdd). - /// - KeypadPlus = 80, - /// - /// The keypad decimal key. - /// - KeypadDecimal = 81, - /// - /// The keypad period key (equivalent to KeypadDecimal). - /// - KeypadPeriod = 81, - /// - /// The keypad enter key. - /// - KeypadEnter = 82, - /// - /// The A key. - /// - A = 83, - /// - /// The B key. - /// - B = 84, - /// - /// The C key. - /// - C = 85, - /// - /// The D key. - /// - D = 86, - /// - /// The E key. - /// - E = 87, - /// - /// The F key. - /// - F = 88, - /// - /// The G key. - /// - G = 89, - /// - /// The H key. - /// - H = 90, - /// - /// The I key. - /// - I = 91, - /// - /// The J key. - /// - J = 92, - /// - /// The K key. - /// - K = 93, - /// - /// The L key. - /// - L = 94, - /// - /// The M key. - /// - M = 95, - /// - /// The N key. - /// - N = 96, - /// - /// The O key. - /// - O = 97, - /// - /// The P key. - /// - P = 98, - /// - /// The Q key. - /// - Q = 99, - /// - /// The R key. - /// - R = 100, - /// - /// The S key. - /// - S = 101, - /// - /// The T key. - /// - T = 102, - /// - /// The U key. - /// - U = 103, - /// - /// The V key. - /// - V = 104, - /// - /// The W key. - /// - W = 105, - /// - /// The X key. - /// - X = 106, - /// - /// The Y key. - /// - Y = 107, - /// - /// The Z key. - /// - Z = 108, - /// - /// The number 0 key. - /// - Number0 = 109, - /// - /// The number 1 key. - /// - Number1 = 110, - /// - /// The number 2 key. - /// - Number2 = 111, - /// - /// The number 3 key. - /// - Number3 = 112, - /// - /// The number 4 key. - /// - Number4 = 113, - /// - /// The number 5 key. - /// - Number5 = 114, - /// - /// The number 6 key. - /// - Number6 = 115, - /// - /// The number 7 key. - /// - Number7 = 116, - /// - /// The number 8 key. - /// - Number8 = 117, - /// - /// The number 9 key. - /// - Number9 = 118, - /// - /// The tilde key. - /// - Tilde = 119, - /// - /// The grave key (equivaent to Tilde). - /// - Grave = 119, - /// - /// The minus key. - /// - Minus = 120, - /// - /// The plus key. - /// - Plus = 121, - /// - /// The left bracket key. - /// - BracketLeft = 122, - /// - /// The left bracket key (equivalent to BracketLeft). - /// - LBracket = 122, - /// - /// The right bracket key. - /// - BracketRight = 123, - /// - /// The right bracket key (equivalent to BracketRight). - /// - RBracket = 123, - /// - /// The semicolon key. - /// - Semicolon = 124, - /// - /// The quote key. - /// - Quote = 125, - /// - /// The comma key. - /// - Comma = 126, - /// - /// The period key. - /// - Period = 127, - /// - /// The slash key. - /// - Slash = 128, - /// - /// The backslash key. - /// - BackSlash = 129, - /// - /// The secondary backslash key. - /// - NonUSBackSlash = 130, - /// - /// Indicates the last available keyboard key. - /// - LastKey = 131, - - FirstMouseButton = 132, - - /// - /// The left mouse button. - /// - MouseLeft = 132, - /// - /// The middle mouse button. - /// - MouseMiddle = 133, - /// - /// The right mouse button. - /// - MouseRight = 134, - /// - /// The first extra mouse button. - /// - MouseButton1 = 135, - /// - /// The second extra mouse button. - /// - MouseButton2 = 136, - /// - /// The third extra mouse button. - /// - MouseButton3 = 137, - /// - /// The fourth extra mouse button. - /// - MouseButton4 = 138, - /// - /// The fifth extra mouse button. - /// - MouseButton5 = 139, - /// - /// The sixth extra mouse button. - /// - MouseButton6 = 140, - /// - /// The seventh extra mouse button. - /// - MouseButton7 = 141, - /// - /// The eigth extra mouse button. - /// - MouseButton8 = 142, - /// - /// The ninth extra mouse button. - /// - MouseButton9 = 143, - /// - /// Indicates the last available mouse button. - /// - MouseLastButton = 144, - /// - /// Mouse wheel rolled up. - /// - MouseWheelUp = 145, - /// - /// Mouse wheel rolled down. - /// - MouseWheelDown = 146 - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Input.Bindings +{ + /// + /// A collection of keys, mouse and other controllers' buttons. + /// + public enum InputKey + { + /// + /// No key pressed. + /// + None = 0, + /// + /// The shift key. + /// + Shift = 1, + /// + /// The control key. + /// + Control = 3, + /// + /// The alt key. + /// + Alt = 5, + /// + /// The win key. + /// + Super = 7, + /// + /// The menu key. + /// + Menu = 9, + /// + /// The F1 key. + /// + F1 = 10, + /// + /// The F2 key. + /// + F2 = 11, + /// + /// The F3 key. + /// + F3 = 12, + /// + /// The F4 key. + /// + F4 = 13, + /// + /// The F5 key. + /// + F5 = 14, + /// + /// The F6 key. + /// + F6 = 15, + /// + /// The F7 key. + /// + F7 = 16, + /// + /// The F8 key. + /// + F8 = 17, + /// + /// The F9 key. + /// + F9 = 18, + /// + /// The F10 key. + /// + F10 = 19, + /// + /// The F11 key. + /// + F11 = 20, + /// + /// The F12 key. + /// + F12 = 21, + /// + /// The F13 key. + /// + F13 = 22, + /// + /// The F14 key. + /// + F14 = 23, + /// + /// The F15 key. + /// + F15 = 24, + /// + /// The F16 key. + /// + F16 = 25, + /// + /// The F17 key. + /// + F17 = 26, + /// + /// The F18 key. + /// + F18 = 27, + /// + /// The F19 key. + /// + F19 = 28, + /// + /// The F20 key. + /// + F20 = 29, + /// + /// The F21 key. + /// + F21 = 30, + /// + /// The F22 key. + /// + F22 = 31, + /// + /// The F23 key. + /// + F23 = 32, + /// + /// The F24 key. + /// + F24 = 33, + /// + /// The F25 key. + /// + F25 = 34, + /// + /// The F26 key. + /// + F26 = 35, + /// + /// The F27 key. + /// + F27 = 36, + /// + /// The F28 key. + /// + F28 = 37, + /// + /// The F29 key. + /// + F29 = 38, + /// + /// The F30 key. + /// + F30 = 39, + /// + /// The F31 key. + /// + F31 = 40, + /// + /// The F32 key. + /// + F32 = 41, + /// + /// The F33 key. + /// + F33 = 42, + /// + /// The F34 key. + /// + F34 = 43, + /// + /// The F35 key. + /// + F35 = 44, + /// + /// The up arrow key. + /// + Up = 45, + /// + /// The down arrow key. + /// + Down = 46, + /// + /// The left arrow key. + /// + Left = 47, + /// + /// The right arrow key. + /// + Right = 48, + /// + /// The enter key. + /// + Enter = 49, + /// + /// The escape key. + /// + Escape = 50, + /// + /// The space key. + /// + Space = 51, + /// + /// The tab key. + /// + Tab = 52, + /// + /// The backspace key. + /// + BackSpace = 53, + /// + /// The backspace key (equivalent to BackSpace). + /// + Back = 53, + /// + /// The insert key. + /// + Insert = 54, + /// + /// The delete key. + /// + Delete = 55, + /// + /// The page up key. + /// + PageUp = 56, + /// + /// The page down key. + /// + PageDown = 57, + /// + /// The home key. + /// + Home = 58, + /// + /// The end key. + /// + End = 59, + /// + /// The caps lock key. + /// + CapsLock = 60, + /// + /// The scroll lock key. + /// + ScrollLock = 61, + /// + /// The print screen key. + /// + PrintScreen = 62, + /// + /// The pause key. + /// + Pause = 63, + /// + /// The num lock key. + /// + NumLock = 64, + /// + /// The clear key (Keypad5 with NumLock disabled, on typical keyboards). + /// + Clear = 65, + /// + /// The sleep key. + /// + Sleep = 66, + /// + /// The keypad 0 key. + /// + Keypad0 = 67, + /// + /// The keypad 1 key. + /// + Keypad1 = 68, + /// + /// The keypad 2 key. + /// + Keypad2 = 69, + /// + /// The keypad 3 key. + /// + Keypad3 = 70, + /// + /// The keypad 4 key. + /// + Keypad4 = 71, + /// + /// The keypad 5 key. + /// + Keypad5 = 72, + /// + /// The keypad 6 key. + /// + Keypad6 = 73, + /// + /// The keypad 7 key. + /// + Keypad7 = 74, + /// + /// The keypad 8 key. + /// + Keypad8 = 75, + /// + /// The keypad 9 key. + /// + Keypad9 = 76, + /// + /// The keypad divide key. + /// + KeypadDivide = 77, + /// + /// The keypad multiply key. + /// + KeypadMultiply = 78, + /// + /// The keypad subtract key. + /// + KeypadSubtract = 79, + /// + /// The keypad minus key (equivalent to KeypadSubtract). + /// + KeypadMinus = 79, + /// + /// The keypad add key. + /// + KeypadAdd = 80, + /// + /// The keypad plus key (equivalent to KeypadAdd). + /// + KeypadPlus = 80, + /// + /// The keypad decimal key. + /// + KeypadDecimal = 81, + /// + /// The keypad period key (equivalent to KeypadDecimal). + /// + KeypadPeriod = 81, + /// + /// The keypad enter key. + /// + KeypadEnter = 82, + /// + /// The A key. + /// + A = 83, + /// + /// The B key. + /// + B = 84, + /// + /// The C key. + /// + C = 85, + /// + /// The D key. + /// + D = 86, + /// + /// The E key. + /// + E = 87, + /// + /// The F key. + /// + F = 88, + /// + /// The G key. + /// + G = 89, + /// + /// The H key. + /// + H = 90, + /// + /// The I key. + /// + I = 91, + /// + /// The J key. + /// + J = 92, + /// + /// The K key. + /// + K = 93, + /// + /// The L key. + /// + L = 94, + /// + /// The M key. + /// + M = 95, + /// + /// The N key. + /// + N = 96, + /// + /// The O key. + /// + O = 97, + /// + /// The P key. + /// + P = 98, + /// + /// The Q key. + /// + Q = 99, + /// + /// The R key. + /// + R = 100, + /// + /// The S key. + /// + S = 101, + /// + /// The T key. + /// + T = 102, + /// + /// The U key. + /// + U = 103, + /// + /// The V key. + /// + V = 104, + /// + /// The W key. + /// + W = 105, + /// + /// The X key. + /// + X = 106, + /// + /// The Y key. + /// + Y = 107, + /// + /// The Z key. + /// + Z = 108, + /// + /// The number 0 key. + /// + Number0 = 109, + /// + /// The number 1 key. + /// + Number1 = 110, + /// + /// The number 2 key. + /// + Number2 = 111, + /// + /// The number 3 key. + /// + Number3 = 112, + /// + /// The number 4 key. + /// + Number4 = 113, + /// + /// The number 5 key. + /// + Number5 = 114, + /// + /// The number 6 key. + /// + Number6 = 115, + /// + /// The number 7 key. + /// + Number7 = 116, + /// + /// The number 8 key. + /// + Number8 = 117, + /// + /// The number 9 key. + /// + Number9 = 118, + /// + /// The tilde key. + /// + Tilde = 119, + /// + /// The grave key (equivaent to Tilde). + /// + Grave = 119, + /// + /// The minus key. + /// + Minus = 120, + /// + /// The plus key. + /// + Plus = 121, + /// + /// The left bracket key. + /// + BracketLeft = 122, + /// + /// The left bracket key (equivalent to BracketLeft). + /// + LBracket = 122, + /// + /// The right bracket key. + /// + BracketRight = 123, + /// + /// The right bracket key (equivalent to BracketRight). + /// + RBracket = 123, + /// + /// The semicolon key. + /// + Semicolon = 124, + /// + /// The quote key. + /// + Quote = 125, + /// + /// The comma key. + /// + Comma = 126, + /// + /// The period key. + /// + Period = 127, + /// + /// The slash key. + /// + Slash = 128, + /// + /// The backslash key. + /// + BackSlash = 129, + /// + /// The secondary backslash key. + /// + NonUSBackSlash = 130, + /// + /// Indicates the last available keyboard key. + /// + LastKey = 131, + + FirstMouseButton = 132, + + /// + /// The left mouse button. + /// + MouseLeft = 132, + /// + /// The middle mouse button. + /// + MouseMiddle = 133, + /// + /// The right mouse button. + /// + MouseRight = 134, + /// + /// The first extra mouse button. + /// + MouseButton1 = 135, + /// + /// The second extra mouse button. + /// + MouseButton2 = 136, + /// + /// The third extra mouse button. + /// + MouseButton3 = 137, + /// + /// The fourth extra mouse button. + /// + MouseButton4 = 138, + /// + /// The fifth extra mouse button. + /// + MouseButton5 = 139, + /// + /// The sixth extra mouse button. + /// + MouseButton6 = 140, + /// + /// The seventh extra mouse button. + /// + MouseButton7 = 141, + /// + /// The eigth extra mouse button. + /// + MouseButton8 = 142, + /// + /// The ninth extra mouse button. + /// + MouseButton9 = 143, + /// + /// Indicates the last available mouse button. + /// + MouseLastButton = 144, + /// + /// Mouse wheel rolled up. + /// + MouseWheelUp = 145, + /// + /// Mouse wheel rolled down. + /// + MouseWheelDown = 146 + } +} diff --git a/osu.Framework/Input/Bindings/KeyBinding.cs b/osu.Framework/Input/Bindings/KeyBinding.cs index fd1f2f841..2a7dbbd1f 100644 --- a/osu.Framework/Input/Bindings/KeyBinding.cs +++ b/osu.Framework/Input/Bindings/KeyBinding.cs @@ -1,59 +1,59 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Input.Bindings -{ - /// - /// A binding of a to an action. - /// - public class KeyBinding - { - /// - /// The combination of keys which will trigger this binding. - /// - public KeyCombination KeyCombination; - - /// - /// The resultant action which is triggered by this binding. - /// - public object Action; - - /// - /// Construct a new instance. - /// - /// The combination of keys which will trigger this binding. - /// The resultant action which is triggered by this binding. Usually an enum type. - public KeyBinding(KeyCombination keys, object action) - { - KeyCombination = keys; - - Action = action; - } - - /// - /// Construct a new instance. - /// - /// The key which will trigger this binding. - /// The resultant action which is triggered by this binding. Usually an enum type. - public KeyBinding(InputKey key, object action) - : this((KeyCombination)key, action) - { - } - - /// - /// Constructor for derived classes that may require serialisation. - /// - public KeyBinding() - { - } - - /// - /// Get the action associated with this binding, cast to the required enum type. - /// - /// The enum type. - /// A cast representation of . - public virtual T GetAction() => (T)Action; - - public override string ToString() => $"{KeyCombination}=>{Action}"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Input.Bindings +{ + /// + /// A binding of a to an action. + /// + public class KeyBinding + { + /// + /// The combination of keys which will trigger this binding. + /// + public KeyCombination KeyCombination; + + /// + /// The resultant action which is triggered by this binding. + /// + public object Action; + + /// + /// Construct a new instance. + /// + /// The combination of keys which will trigger this binding. + /// The resultant action which is triggered by this binding. Usually an enum type. + public KeyBinding(KeyCombination keys, object action) + { + KeyCombination = keys; + + Action = action; + } + + /// + /// Construct a new instance. + /// + /// The key which will trigger this binding. + /// The resultant action which is triggered by this binding. Usually an enum type. + public KeyBinding(InputKey key, object action) + : this((KeyCombination)key, action) + { + } + + /// + /// Constructor for derived classes that may require serialisation. + /// + public KeyBinding() + { + } + + /// + /// Get the action associated with this binding, cast to the required enum type. + /// + /// The enum type. + /// A cast representation of . + public virtual T GetAction() => (T)Action; + + public override string ToString() => $"{KeyCombination}=>{Action}"; + } +} diff --git a/osu.Framework/Input/Bindings/KeyBindingContainer.cs b/osu.Framework/Input/Bindings/KeyBindingContainer.cs index 0a3653cad..4a256e41d 100644 --- a/osu.Framework/Input/Bindings/KeyBindingContainer.cs +++ b/osu.Framework/Input/Bindings/KeyBindingContainer.cs @@ -1,261 +1,261 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; - -namespace osu.Framework.Input.Bindings -{ - /// - /// Maps input actions to custom action data of type . Use in conjunction with s implementing . - /// - /// The type of the custom action. - public abstract class KeyBindingContainer : KeyBindingContainer - where T : struct - { - private readonly SimultaneousBindingMode simultaneousMode; - - /// - /// Create a new instance. - /// - /// Specify how to deal with multiple matches of s and s. - protected KeyBindingContainer(SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) - { - RelativeSizeAxes = Axes.Both; - - this.simultaneousMode = simultaneousMode; - } - - private readonly List pressedBindings = new List(); - - private readonly List pressedActions = new List(); - - /// - /// All actions in a currently pressed state. - /// - public IEnumerable PressedActions => pressedActions; - - private bool isModifier(InputKey k) => k < InputKey.F1; - - /// - /// The input queue to be used for processing key bindings. Based on the non-positional . - /// Can be overridden to change priorities. - /// - protected virtual IEnumerable KeyBindingInputQueue => localQueue; - - private readonly List localQueue = new List(); - - /// - /// Override to enable or disable sending of repeated actions (disabled by default). - /// Each repeated action will have its own pressed/released event pair. - /// - protected virtual bool SendRepeats => false; - - /// - /// Whether this should attempt to handle input before any of its children. - /// - protected virtual bool Prioritised => false; - - protected override bool OnWheel(InputState state) - { - InputKey key = state.Mouse.WheelDelta > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown; - - // we need to create a local cloned state to ensure the underlying code in handleNewReleased thinks we are in a sane state, - // even though we are pressing and releasing an InputKey in a single frame. - // the important part of this cloned state is the value of Wheel reset to zero. - var clonedState = state.Clone(); - clonedState.Mouse = new MouseState { Buttons = clonedState.Mouse.Buttons }; - - return handleNewPressed(state, key, false) | handleNewReleased(clonedState, key); - } - - internal override bool BuildKeyboardInputQueue(List queue) - { - localQueue.Clear(); - - if (!base.BuildKeyboardInputQueue(localQueue)) - return false; - - if (Prioritised) - { - localQueue.Remove(this); - localQueue.Add(this); - } - - queue.AddRange(localQueue); - - localQueue.Reverse(); - return true; - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => handleNewPressed(state, KeyCombination.FromMouseButton(args.Button), false); - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => handleNewReleased(state, KeyCombination.FromMouseButton(args.Button)); - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (args.Repeat && !SendRepeats) - { - if (pressedBindings.Count > 0) - return true; - - return false; - } - - return handleNewPressed(state, KeyCombination.FromKey(args.Key), args.Repeat); - } - - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => handleNewReleased(state, KeyCombination.FromKey(args.Key)); - - private bool handleNewPressed(InputState state, InputKey newKey, bool repeat) - { - var pressedCombination = KeyCombination.FromInputState(state); - - bool handled = false; - var bindings = repeat ? KeyBindings : KeyBindings.Except(pressedBindings); - var newlyPressed = bindings.Where(m => - m.KeyCombination.Keys.Contains(newKey) // only handle bindings matching current key (not required for correct logic) - && m.KeyCombination.IsPressed(pressedCombination)); - - if (isModifier(newKey)) - // if the current key pressed was a modifier, only handle modifier-only bindings. - newlyPressed = newlyPressed.Where(b => b.KeyCombination.Keys.All(isModifier)); - - // we want to always handle bindings with more keys before bindings with less. - newlyPressed = newlyPressed.OrderByDescending(b => b.KeyCombination.Keys.Count()).ToList(); - - if (!repeat) - pressedBindings.AddRange(newlyPressed); - - foreach (var newBinding in newlyPressed) - { - handled |= PropagatePressed(KeyBindingInputQueue, newBinding.GetAction()); - - // we only want to handle the first valid binding (the one with the most keys) in non-simultaneous mode. - if (simultaneousMode == SimultaneousBindingMode.None && handled) - break; - } - - return handled; - } - - protected virtual bool PropagatePressed(IEnumerable drawables, T pressed) - { - IDrawable handled = null; - - // we handled a new binding and there is an existing one. if we don't want concurrency, let's propagate a released event. - if (simultaneousMode == SimultaneousBindingMode.None) - { - // we want to release any existing pressed actions. - foreach (var action in pressedActions) - drawables.OfType>().ForEach(d => d.OnReleased(action)); - pressedActions.Clear(); - } - - // only handle if we are a new non-pressed action (or a concurrency mode that supports multiple simultaneous triggers). - if (simultaneousMode == SimultaneousBindingMode.All || !pressedActions.Contains(pressed)) - { - pressedActions.Add(pressed); - handled = drawables.OfType>().FirstOrDefault(d => d.OnPressed(pressed)); - } - - if (handled != null) - Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handled != null; - } - - private bool handleNewReleased(InputState state, InputKey releasedKey) - { - var pressedCombination = KeyCombination.FromInputState(state); - - bool handled = false; - - var newlyReleased = pressedBindings.Where(b => !b.KeyCombination.IsPressed(pressedCombination)).ToList(); - - Trace.Assert(newlyReleased.All(b => b.KeyCombination.Keys.Contains(releasedKey))); - - foreach (var binding in newlyReleased) - { - pressedBindings.Remove(binding); - - var action = binding.GetAction(); - - handled |= PropagateReleased(KeyBindingInputQueue, action); - } - - return handled; - } - - protected virtual bool PropagateReleased(IEnumerable drawables, T released) - { - IDrawable handled = null; - - // we either want multiple release events due to the simultaneous mode, or we only want one when we - // - were pressed (as an action) - // - are the last pressed binding with this action - if (simultaneousMode == SimultaneousBindingMode.All || pressedActions.Contains(released) && pressedBindings.All(b => !b.GetAction().Equals(released))) - { - handled = drawables.OfType>().FirstOrDefault(d => d.OnReleased(released)); - pressedActions.Remove(released); - } - - if (handled != null) - Logger.Log($"Released ({released}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handled != null; - } - - public void TriggerReleased(T released) => PropagateReleased(KeyBindingInputQueue, released); - - public void TriggerPressed(T pressed) => PropagatePressed(KeyBindingInputQueue, pressed); - } - - /// - /// Maps input actions to custom action data. - /// - public abstract class KeyBindingContainer : Container - { - protected IEnumerable KeyBindings; - - public abstract IEnumerable DefaultKeyBindings { get; } - - protected override void LoadComplete() - { - base.LoadComplete(); - ReloadMappings(); - } - - protected virtual void ReloadMappings() - { - KeyBindings = DefaultKeyBindings; - } - } - - public enum SimultaneousBindingMode - { - /// - /// One action can be in a pressed state at once. If a new matching binding is encountered, any existing binding is first released. - /// - None, - - /// - /// Unique actions are allowed to be pressed at the same time. There may therefore be more than one action in an actuated state at once. - /// If one action has multiple bindings, only the first will trigger an . - /// The last binding to be released will trigger an . - /// - Unique, - - /// - /// Unique actions are allowed to be pressed at the same time, as well as multiple times from different bindings. There may therefore be - /// more than one action in an pressed state at once, as well as multiple consecutive events - /// for a single action (followed by an eventual balancing number of events). - /// - All, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; + +namespace osu.Framework.Input.Bindings +{ + /// + /// Maps input actions to custom action data of type . Use in conjunction with s implementing . + /// + /// The type of the custom action. + public abstract class KeyBindingContainer : KeyBindingContainer + where T : struct + { + private readonly SimultaneousBindingMode simultaneousMode; + + /// + /// Create a new instance. + /// + /// Specify how to deal with multiple matches of s and s. + protected KeyBindingContainer(SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) + { + RelativeSizeAxes = Axes.Both; + + this.simultaneousMode = simultaneousMode; + } + + private readonly List pressedBindings = new List(); + + private readonly List pressedActions = new List(); + + /// + /// All actions in a currently pressed state. + /// + public IEnumerable PressedActions => pressedActions; + + private bool isModifier(InputKey k) => k < InputKey.F1; + + /// + /// The input queue to be used for processing key bindings. Based on the non-positional . + /// Can be overridden to change priorities. + /// + protected virtual IEnumerable KeyBindingInputQueue => localQueue; + + private readonly List localQueue = new List(); + + /// + /// Override to enable or disable sending of repeated actions (disabled by default). + /// Each repeated action will have its own pressed/released event pair. + /// + protected virtual bool SendRepeats => false; + + /// + /// Whether this should attempt to handle input before any of its children. + /// + protected virtual bool Prioritised => false; + + protected override bool OnWheel(InputState state) + { + InputKey key = state.Mouse.WheelDelta > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown; + + // we need to create a local cloned state to ensure the underlying code in handleNewReleased thinks we are in a sane state, + // even though we are pressing and releasing an InputKey in a single frame. + // the important part of this cloned state is the value of Wheel reset to zero. + var clonedState = state.Clone(); + clonedState.Mouse = new MouseState { Buttons = clonedState.Mouse.Buttons }; + + return handleNewPressed(state, key, false) | handleNewReleased(clonedState, key); + } + + internal override bool BuildKeyboardInputQueue(List queue) + { + localQueue.Clear(); + + if (!base.BuildKeyboardInputQueue(localQueue)) + return false; + + if (Prioritised) + { + localQueue.Remove(this); + localQueue.Add(this); + } + + queue.AddRange(localQueue); + + localQueue.Reverse(); + return true; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => handleNewPressed(state, KeyCombination.FromMouseButton(args.Button), false); + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => handleNewReleased(state, KeyCombination.FromMouseButton(args.Button)); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat && !SendRepeats) + { + if (pressedBindings.Count > 0) + return true; + + return false; + } + + return handleNewPressed(state, KeyCombination.FromKey(args.Key), args.Repeat); + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => handleNewReleased(state, KeyCombination.FromKey(args.Key)); + + private bool handleNewPressed(InputState state, InputKey newKey, bool repeat) + { + var pressedCombination = KeyCombination.FromInputState(state); + + bool handled = false; + var bindings = repeat ? KeyBindings : KeyBindings.Except(pressedBindings); + var newlyPressed = bindings.Where(m => + m.KeyCombination.Keys.Contains(newKey) // only handle bindings matching current key (not required for correct logic) + && m.KeyCombination.IsPressed(pressedCombination)); + + if (isModifier(newKey)) + // if the current key pressed was a modifier, only handle modifier-only bindings. + newlyPressed = newlyPressed.Where(b => b.KeyCombination.Keys.All(isModifier)); + + // we want to always handle bindings with more keys before bindings with less. + newlyPressed = newlyPressed.OrderByDescending(b => b.KeyCombination.Keys.Count()).ToList(); + + if (!repeat) + pressedBindings.AddRange(newlyPressed); + + foreach (var newBinding in newlyPressed) + { + handled |= PropagatePressed(KeyBindingInputQueue, newBinding.GetAction()); + + // we only want to handle the first valid binding (the one with the most keys) in non-simultaneous mode. + if (simultaneousMode == SimultaneousBindingMode.None && handled) + break; + } + + return handled; + } + + protected virtual bool PropagatePressed(IEnumerable drawables, T pressed) + { + IDrawable handled = null; + + // we handled a new binding and there is an existing one. if we don't want concurrency, let's propagate a released event. + if (simultaneousMode == SimultaneousBindingMode.None) + { + // we want to release any existing pressed actions. + foreach (var action in pressedActions) + drawables.OfType>().ForEach(d => d.OnReleased(action)); + pressedActions.Clear(); + } + + // only handle if we are a new non-pressed action (or a concurrency mode that supports multiple simultaneous triggers). + if (simultaneousMode == SimultaneousBindingMode.All || !pressedActions.Contains(pressed)) + { + pressedActions.Add(pressed); + handled = drawables.OfType>().FirstOrDefault(d => d.OnPressed(pressed)); + } + + if (handled != null) + Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handled != null; + } + + private bool handleNewReleased(InputState state, InputKey releasedKey) + { + var pressedCombination = KeyCombination.FromInputState(state); + + bool handled = false; + + var newlyReleased = pressedBindings.Where(b => !b.KeyCombination.IsPressed(pressedCombination)).ToList(); + + Trace.Assert(newlyReleased.All(b => b.KeyCombination.Keys.Contains(releasedKey))); + + foreach (var binding in newlyReleased) + { + pressedBindings.Remove(binding); + + var action = binding.GetAction(); + + handled |= PropagateReleased(KeyBindingInputQueue, action); + } + + return handled; + } + + protected virtual bool PropagateReleased(IEnumerable drawables, T released) + { + IDrawable handled = null; + + // we either want multiple release events due to the simultaneous mode, or we only want one when we + // - were pressed (as an action) + // - are the last pressed binding with this action + if (simultaneousMode == SimultaneousBindingMode.All || pressedActions.Contains(released) && pressedBindings.All(b => !b.GetAction().Equals(released))) + { + handled = drawables.OfType>().FirstOrDefault(d => d.OnReleased(released)); + pressedActions.Remove(released); + } + + if (handled != null) + Logger.Log($"Released ({released}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handled != null; + } + + public void TriggerReleased(T released) => PropagateReleased(KeyBindingInputQueue, released); + + public void TriggerPressed(T pressed) => PropagatePressed(KeyBindingInputQueue, pressed); + } + + /// + /// Maps input actions to custom action data. + /// + public abstract class KeyBindingContainer : Container + { + protected IEnumerable KeyBindings; + + public abstract IEnumerable DefaultKeyBindings { get; } + + protected override void LoadComplete() + { + base.LoadComplete(); + ReloadMappings(); + } + + protected virtual void ReloadMappings() + { + KeyBindings = DefaultKeyBindings; + } + } + + public enum SimultaneousBindingMode + { + /// + /// One action can be in a pressed state at once. If a new matching binding is encountered, any existing binding is first released. + /// + None, + + /// + /// Unique actions are allowed to be pressed at the same time. There may therefore be more than one action in an actuated state at once. + /// If one action has multiple bindings, only the first will trigger an . + /// The last binding to be released will trigger an . + /// + Unique, + + /// + /// Unique actions are allowed to be pressed at the same time, as well as multiple times from different bindings. There may therefore be + /// more than one action in an pressed state at once, as well as multiple consecutive events + /// for a single action (followed by an eventual balancing number of events). + /// + All, + } +} diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index ea73e83f2..ad5bc976b 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -1,231 +1,231 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using OpenTK.Input; - -namespace osu.Framework.Input.Bindings -{ - /// - /// Represent a combination of more than one s. - /// - public class KeyCombination : IEquatable - { - /// - /// The keys. - /// - public readonly IEnumerable Keys; - - /// - /// Construct a new instance. - /// - /// The keys. - public KeyCombination(IEnumerable keys) - { - Keys = keys.OrderBy(k => (int)k).ToArray(); - } - - /// - /// Construct a new instance. - /// - /// A comma-separated (KeyCode) string representation of the keys. - public KeyCombination(string keys) - : this(keys.Split(',').Select(s => (InputKey)int.Parse(s))) - { - } - - /// - /// Check whether the provided input is a valid pressedKeys for this combination. - /// - /// The potential pressedKeys for this combination. - /// Whether the pressedKeys keys are valid. - public bool IsPressed(KeyCombination pressedKeys) => !Keys.Except(pressedKeys.Keys).Any(); - - public bool Equals(KeyCombination other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Keys.SequenceEqual(other.Keys); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((KeyCombination)obj); - } - - public override int GetHashCode() => Keys != null ? Keys.Select(b => b.GetHashCode()).Aggregate((h1, h2) => h1 * 17 + h2) : 0; - - public static implicit operator KeyCombination(InputKey singleKey) => new KeyCombination(new[] { singleKey }); - - public static implicit operator KeyCombination(string stringRepresentation) => new KeyCombination(stringRepresentation); - - public static implicit operator KeyCombination(InputKey[] keys) => new KeyCombination(keys); - - public override string ToString() => Keys?.Select(b => ((int)b).ToString()).Aggregate((s1, s2) => $"{s1},{s2}") ?? string.Empty; - - public string ReadableString() => Keys?.Select(getReadableKey).Aggregate((s1, s2) => $"{s1}+{s2}") ?? string.Empty; - - private string getReadableKey(InputKey key) - { - switch (key) - { - case InputKey.None: - return string.Empty; - case InputKey.Shift: - return "Shift"; - case InputKey.Control: - return "Ctrl"; - case InputKey.Alt: - return "Alt"; - case InputKey.Super: - return "Win"; - case InputKey.Escape: - return "Esc"; - case InputKey.BackSpace: - return "Backsp"; - case InputKey.Insert: - return "Ins"; - case InputKey.Delete: - return "Del"; - case InputKey.PageUp: - return "Pgup"; - case InputKey.PageDown: - return "Pgdn"; - case InputKey.CapsLock: - return "Caps"; - case InputKey.Number0: - case InputKey.Keypad0: - return "0"; - case InputKey.Number1: - case InputKey.Keypad1: - return "1"; - case InputKey.Number2: - case InputKey.Keypad2: - return "2"; - case InputKey.Number3: - case InputKey.Keypad3: - return "3"; - case InputKey.Number4: - case InputKey.Keypad4: - return "4"; - case InputKey.Number5: - case InputKey.Keypad5: - return "5"; - case InputKey.Number6: - case InputKey.Keypad6: - return "6"; - case InputKey.Number7: - case InputKey.Keypad7: - return "7"; - case InputKey.Number8: - case InputKey.Keypad8: - return "8"; - case InputKey.Number9: - case InputKey.Keypad9: - return "9"; - case InputKey.Tilde: - return "~"; - case InputKey.Minus: - return "-"; - case InputKey.Plus: - return "+"; - case InputKey.BracketLeft: - return "("; - case InputKey.BracketRight: - return ")"; - case InputKey.Semicolon: - return ";"; - case InputKey.Quote: - return "\""; - case InputKey.Comma: - return ","; - case InputKey.Period: - return "."; - case InputKey.Slash: - return "/"; - case InputKey.BackSlash: - case InputKey.NonUSBackSlash: - return "\\"; - case InputKey.MouseLeft: - return "M1"; - case InputKey.MouseMiddle: - return "M3"; - case InputKey.MouseRight: - return "M2"; - case InputKey.MouseButton1: - return "M4"; - case InputKey.MouseButton2: - return "M5"; - case InputKey.MouseButton3: - return "M6"; - case InputKey.MouseButton4: - return "M7"; - case InputKey.MouseButton5: - return "M8"; - case InputKey.MouseButton6: - return "M9"; - case InputKey.MouseButton7: - return "M10"; - case InputKey.MouseButton8: - return "M11"; - case InputKey.MouseButton9: - return "M12"; - case InputKey.MouseWheelDown: - return "Wheel Down"; - case InputKey.MouseWheelUp: - return "Wheel Up"; - default: - return key.ToString(); - } - } - - public static InputKey FromKey(Key key) - { - switch (key) - { - case Key.RShift: - return InputKey.Shift; - case Key.RAlt: - return InputKey.Alt; - case Key.RControl: - return InputKey.Control; - case Key.RWin: - return InputKey.Super; - } - - return (InputKey)key; - } - - public static InputKey FromMouseButton(MouseButton button) - { - return (InputKey)((int)InputKey.FirstMouseButton + button); - } - - public static KeyCombination FromInputState(InputState state) - { - List keys = new List(); - - if (state.Mouse != null) - { - foreach (var button in state.Mouse.Buttons) - keys.Add(FromMouseButton(button)); - - if (state.Mouse.WheelDelta > 0) keys.Add(InputKey.MouseWheelUp); - if (state.Mouse.WheelDelta < 0) keys.Add(InputKey.MouseWheelDown); - } - - if (state.Keyboard != null) - { - foreach (var key in state.Keyboard.Keys) - keys.Add(FromKey(key)); - } - - return new KeyCombination(keys); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenTK.Input; + +namespace osu.Framework.Input.Bindings +{ + /// + /// Represent a combination of more than one s. + /// + public class KeyCombination : IEquatable + { + /// + /// The keys. + /// + public readonly IEnumerable Keys; + + /// + /// Construct a new instance. + /// + /// The keys. + public KeyCombination(IEnumerable keys) + { + Keys = keys.OrderBy(k => (int)k).ToArray(); + } + + /// + /// Construct a new instance. + /// + /// A comma-separated (KeyCode) string representation of the keys. + public KeyCombination(string keys) + : this(keys.Split(',').Select(s => (InputKey)int.Parse(s))) + { + } + + /// + /// Check whether the provided input is a valid pressedKeys for this combination. + /// + /// The potential pressedKeys for this combination. + /// Whether the pressedKeys keys are valid. + public bool IsPressed(KeyCombination pressedKeys) => !Keys.Except(pressedKeys.Keys).Any(); + + public bool Equals(KeyCombination other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Keys.SequenceEqual(other.Keys); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((KeyCombination)obj); + } + + public override int GetHashCode() => Keys != null ? Keys.Select(b => b.GetHashCode()).Aggregate((h1, h2) => h1 * 17 + h2) : 0; + + public static implicit operator KeyCombination(InputKey singleKey) => new KeyCombination(new[] { singleKey }); + + public static implicit operator KeyCombination(string stringRepresentation) => new KeyCombination(stringRepresentation); + + public static implicit operator KeyCombination(InputKey[] keys) => new KeyCombination(keys); + + public override string ToString() => Keys?.Select(b => ((int)b).ToString()).Aggregate((s1, s2) => $"{s1},{s2}") ?? string.Empty; + + public string ReadableString() => Keys?.Select(getReadableKey).Aggregate((s1, s2) => $"{s1}+{s2}") ?? string.Empty; + + private string getReadableKey(InputKey key) + { + switch (key) + { + case InputKey.None: + return string.Empty; + case InputKey.Shift: + return "Shift"; + case InputKey.Control: + return "Ctrl"; + case InputKey.Alt: + return "Alt"; + case InputKey.Super: + return "Win"; + case InputKey.Escape: + return "Esc"; + case InputKey.BackSpace: + return "Backsp"; + case InputKey.Insert: + return "Ins"; + case InputKey.Delete: + return "Del"; + case InputKey.PageUp: + return "Pgup"; + case InputKey.PageDown: + return "Pgdn"; + case InputKey.CapsLock: + return "Caps"; + case InputKey.Number0: + case InputKey.Keypad0: + return "0"; + case InputKey.Number1: + case InputKey.Keypad1: + return "1"; + case InputKey.Number2: + case InputKey.Keypad2: + return "2"; + case InputKey.Number3: + case InputKey.Keypad3: + return "3"; + case InputKey.Number4: + case InputKey.Keypad4: + return "4"; + case InputKey.Number5: + case InputKey.Keypad5: + return "5"; + case InputKey.Number6: + case InputKey.Keypad6: + return "6"; + case InputKey.Number7: + case InputKey.Keypad7: + return "7"; + case InputKey.Number8: + case InputKey.Keypad8: + return "8"; + case InputKey.Number9: + case InputKey.Keypad9: + return "9"; + case InputKey.Tilde: + return "~"; + case InputKey.Minus: + return "-"; + case InputKey.Plus: + return "+"; + case InputKey.BracketLeft: + return "("; + case InputKey.BracketRight: + return ")"; + case InputKey.Semicolon: + return ";"; + case InputKey.Quote: + return "\""; + case InputKey.Comma: + return ","; + case InputKey.Period: + return "."; + case InputKey.Slash: + return "/"; + case InputKey.BackSlash: + case InputKey.NonUSBackSlash: + return "\\"; + case InputKey.MouseLeft: + return "M1"; + case InputKey.MouseMiddle: + return "M3"; + case InputKey.MouseRight: + return "M2"; + case InputKey.MouseButton1: + return "M4"; + case InputKey.MouseButton2: + return "M5"; + case InputKey.MouseButton3: + return "M6"; + case InputKey.MouseButton4: + return "M7"; + case InputKey.MouseButton5: + return "M8"; + case InputKey.MouseButton6: + return "M9"; + case InputKey.MouseButton7: + return "M10"; + case InputKey.MouseButton8: + return "M11"; + case InputKey.MouseButton9: + return "M12"; + case InputKey.MouseWheelDown: + return "Wheel Down"; + case InputKey.MouseWheelUp: + return "Wheel Up"; + default: + return key.ToString(); + } + } + + public static InputKey FromKey(Key key) + { + switch (key) + { + case Key.RShift: + return InputKey.Shift; + case Key.RAlt: + return InputKey.Alt; + case Key.RControl: + return InputKey.Control; + case Key.RWin: + return InputKey.Super; + } + + return (InputKey)key; + } + + public static InputKey FromMouseButton(MouseButton button) + { + return (InputKey)((int)InputKey.FirstMouseButton + button); + } + + public static KeyCombination FromInputState(InputState state) + { + List keys = new List(); + + if (state.Mouse != null) + { + foreach (var button in state.Mouse.Buttons) + keys.Add(FromMouseButton(button)); + + if (state.Mouse.WheelDelta > 0) keys.Add(InputKey.MouseWheelUp); + if (state.Mouse.WheelDelta < 0) keys.Add(InputKey.MouseWheelDown); + } + + if (state.Keyboard != null) + { + foreach (var key in state.Keyboard.Keys) + keys.Add(FromKey(key)); + } + + return new KeyCombination(keys); + } + } +} diff --git a/osu.Framework/Input/CustomInputManager.cs b/osu.Framework/Input/CustomInputManager.cs index 663f8df78..e830a871c 100644 --- a/osu.Framework/Input/CustomInputManager.cs +++ b/osu.Framework/Input/CustomInputManager.cs @@ -1,41 +1,41 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Input.Handlers; - -namespace osu.Framework.Input -{ - /// - /// An implementation which allows managing of s manually. - /// - public class CustomInputManager : InputManager - { - protected override IEnumerable InputHandlers => inputHandlers; - - private readonly List inputHandlers = new List(); - - protected void AddHandler(InputHandler handler) - { - if (!handler.Initialize(Host)) return; - - int index = inputHandlers.BinarySearch(handler, new InputHandlerComparer()); - if (index < 0) - { - index = ~index; - } - - inputHandlers.Insert(index, handler); - } - - protected void RemoveHandler(InputHandler handler) => inputHandlers.Remove(handler); - - protected override void Dispose(bool isDisposing) - { - foreach (var h in inputHandlers) - h.Dispose(); - - base.Dispose(isDisposing); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input.Handlers; + +namespace osu.Framework.Input +{ + /// + /// An implementation which allows managing of s manually. + /// + public class CustomInputManager : InputManager + { + protected override IEnumerable InputHandlers => inputHandlers; + + private readonly List inputHandlers = new List(); + + protected void AddHandler(InputHandler handler) + { + if (!handler.Initialize(Host)) return; + + int index = inputHandlers.BinarySearch(handler, new InputHandlerComparer()); + if (index < 0) + { + index = ~index; + } + + inputHandlers.Insert(index, handler); + } + + protected void RemoveHandler(InputHandler handler) => inputHandlers.Remove(handler); + + protected override void Dispose(bool isDisposing) + { + foreach (var h in inputHandlers) + h.Dispose(); + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Framework/Input/FrameworkActionContainer.cs b/osu.Framework/Input/FrameworkActionContainer.cs index 42387287a..2daf68194 100644 --- a/osu.Framework/Input/FrameworkActionContainer.cs +++ b/osu.Framework/Input/FrameworkActionContainer.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; - -namespace osu.Framework.Input -{ - public class FrameworkActionContainer : KeyBindingContainer - { - public override IEnumerable DefaultKeyBindings => new[] - { - new KeyBinding(new[] { InputKey.Control, InputKey.F1 }, FrameworkAction.ToggleDrawVisualiser), - new KeyBinding(new[] { InputKey.Control, InputKey.F11 }, FrameworkAction.CycleFrameStatistics), - new KeyBinding(new[] { InputKey.Control, InputKey.F10 }, FrameworkAction.ToggleLogOverlay), - new KeyBinding(new[] { InputKey.Alt, InputKey.Enter }, FrameworkAction.ToggleFullscreen), - }; - - protected override bool Prioritised => true; - - protected override IEnumerable KeyBindingInputQueue => base.KeyBindingInputQueue.Prepend(Child); - } - - public enum FrameworkAction - { - CycleFrameStatistics, - ToggleDrawVisualiser, - ToggleLogOverlay, - ToggleFullscreen - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; + +namespace osu.Framework.Input +{ + public class FrameworkActionContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(new[] { InputKey.Control, InputKey.F1 }, FrameworkAction.ToggleDrawVisualiser), + new KeyBinding(new[] { InputKey.Control, InputKey.F11 }, FrameworkAction.CycleFrameStatistics), + new KeyBinding(new[] { InputKey.Control, InputKey.F10 }, FrameworkAction.ToggleLogOverlay), + new KeyBinding(new[] { InputKey.Alt, InputKey.Enter }, FrameworkAction.ToggleFullscreen), + }; + + protected override bool Prioritised => true; + + protected override IEnumerable KeyBindingInputQueue => base.KeyBindingInputQueue.Prepend(Child); + } + + public enum FrameworkAction + { + CycleFrameStatistics, + ToggleDrawVisualiser, + ToggleLogOverlay, + ToggleFullscreen + } +} diff --git a/osu.Framework/Input/GameWindowTextInput.cs b/osu.Framework/Input/GameWindowTextInput.cs index 60a359cd2..abb1fb84d 100644 --- a/osu.Framework/Input/GameWindowTextInput.cs +++ b/osu.Framework/Input/GameWindowTextInput.cs @@ -1,81 +1,81 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Platform; - -namespace osu.Framework.Input -{ - public class GameWindowTextInput : ITextInputSource - { - private readonly GameWindow window; - - private string pending = string.Empty; - - public GameWindowTextInput(GameWindow window) - { - this.window = window; - } - - private void window_KeyPress(object sender, OpenTK.KeyPressEventArgs e) - { - // Drop any keypresses if the control, alt, or windows/command key are being held. - // This is a workaround for an issue on macOS where OpenTK will fire KeyPress events even - // if modifier keys are held. This can be reverted when it is fixed on OpenTK's side. - if (RuntimeInfo.OS == RuntimeInfo.Platform.MacOsx) - { - var state = OpenTK.Input.Keyboard.GetState(); - if (state.IsKeyDown(OpenTK.Input.Key.LControl) - || state.IsKeyDown(OpenTK.Input.Key.RControl) - || state.IsKeyDown(OpenTK.Input.Key.LAlt) - || state.IsKeyDown(OpenTK.Input.Key.RAlt) - || state.IsKeyDown(OpenTK.Input.Key.LWin) - || state.IsKeyDown(OpenTK.Input.Key.RWin)) - return; - // arbitrary choice here, but it caters for any non-printable keys on an A1243 Apple Keyboard - if (e.KeyChar > 63000) - return; - } - pending += e.KeyChar; - } - - public bool ImeActive => false; - - public string GetPendingText() - { - try - { - return pending; - } - finally - { - pending = string.Empty; - } - } - - public void Deactivate(object sender) - { - window.KeyPress -= window_KeyPress; - } - - public void Activate(object sender) - { - window.KeyPress += window_KeyPress; - } - - private void imeCompose() - { - //todo: implement - OnNewImeComposition?.Invoke(string.Empty); - } - - private void imeResult() - { - //todo: implement - OnNewImeResult?.Invoke(string.Empty); - } - - public event Action OnNewImeComposition; - public event Action OnNewImeResult; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Platform; + +namespace osu.Framework.Input +{ + public class GameWindowTextInput : ITextInputSource + { + private readonly GameWindow window; + + private string pending = string.Empty; + + public GameWindowTextInput(GameWindow window) + { + this.window = window; + } + + private void window_KeyPress(object sender, OpenTK.KeyPressEventArgs e) + { + // Drop any keypresses if the control, alt, or windows/command key are being held. + // This is a workaround for an issue on macOS where OpenTK will fire KeyPress events even + // if modifier keys are held. This can be reverted when it is fixed on OpenTK's side. + if (RuntimeInfo.OS == RuntimeInfo.Platform.MacOsx) + { + var state = OpenTK.Input.Keyboard.GetState(); + if (state.IsKeyDown(OpenTK.Input.Key.LControl) + || state.IsKeyDown(OpenTK.Input.Key.RControl) + || state.IsKeyDown(OpenTK.Input.Key.LAlt) + || state.IsKeyDown(OpenTK.Input.Key.RAlt) + || state.IsKeyDown(OpenTK.Input.Key.LWin) + || state.IsKeyDown(OpenTK.Input.Key.RWin)) + return; + // arbitrary choice here, but it caters for any non-printable keys on an A1243 Apple Keyboard + if (e.KeyChar > 63000) + return; + } + pending += e.KeyChar; + } + + public bool ImeActive => false; + + public string GetPendingText() + { + try + { + return pending; + } + finally + { + pending = string.Empty; + } + } + + public void Deactivate(object sender) + { + window.KeyPress -= window_KeyPress; + } + + public void Activate(object sender) + { + window.KeyPress += window_KeyPress; + } + + private void imeCompose() + { + //todo: implement + OnNewImeComposition?.Invoke(string.Empty); + } + + private void imeResult() + { + //todo: implement + OnNewImeResult?.Invoke(string.Empty); + } + + public event Action OnNewImeComposition; + public event Action OnNewImeResult; + } +} diff --git a/osu.Framework/Input/Handlers/IHasCursorSensitivity.cs b/osu.Framework/Input/Handlers/IHasCursorSensitivity.cs index 959c408c1..b16f4a653 100644 --- a/osu.Framework/Input/Handlers/IHasCursorSensitivity.cs +++ b/osu.Framework/Input/Handlers/IHasCursorSensitivity.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; - -namespace osu.Framework.Input.Handlers -{ - /// - /// An input handler which can have its sensitivity changed. - /// - public interface IHasCursorSensitivity - { - BindableDouble Sensitivity { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Framework.Input.Handlers +{ + /// + /// An input handler which can have its sensitivity changed. + /// + public interface IHasCursorSensitivity + { + BindableDouble Sensitivity { get; } + } +} diff --git a/osu.Framework/Input/Handlers/InputHandler.cs b/osu.Framework/Input/Handlers/InputHandler.cs index 16c0d914f..d14f1c145 100644 --- a/osu.Framework/Input/Handlers/InputHandler.cs +++ b/osu.Framework/Input/Handlers/InputHandler.cs @@ -1,94 +1,94 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using osu.Framework.Platform; -using System.Collections.Generic; -using osu.Framework.Configuration; - -namespace osu.Framework.Input.Handlers -{ - public abstract class InputHandler : IDisposable - { - /// - /// Used to initialize resources specific to this InputHandler. It gets called once. - /// - /// Success of the initialization. - public abstract bool Initialize(GameHost host); - - protected ConcurrentQueue PendingStates = new ConcurrentQueue(); - - private readonly object pendingStatesRetrievalLock = new object(); - - /// - /// Retrieve a list of all pending states since the last call to this method. - /// - public virtual List GetPendingStates() - { - lock (pendingStatesRetrievalLock) - { - List pending = new List(); - - while (PendingStates.TryDequeue(out InputState s)) - pending.Add(s); - - return pending; - } - } - - /// - /// Indicates whether this InputHandler is currently delivering input by the user. When handling input the OsuGame uses the first InputHandler which is active. - /// - public abstract bool IsActive { get; } - - /// - /// Indicated how high of a priority this handler has. The active handler with the highest priority is controlling the cursor at any given time. - /// - public abstract int Priority { get; } - - /// - /// Whether this InputHandler should be collecting s to return on the next call - /// - public readonly BindableBool Enabled = new BindableBool(true); - - public override string ToString() => GetType().Name; - - #region IDisposable Support - - protected bool IsDisposed; - - protected virtual void Dispose(bool disposing) - { - if (IsDisposed) - return; - - Enabled.Value = false; - IsDisposed = true; - } - - ~InputHandler() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - } - - public class InputHandlerComparer : IComparer - { - public int Compare(InputHandler h1, InputHandler h2) - { - if (h1 == null) throw new ArgumentNullException(nameof(h1)); - if (h2 == null) throw new ArgumentNullException(nameof(h2)); - - return h2.Priority.CompareTo(h1.Priority); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using osu.Framework.Platform; +using System.Collections.Generic; +using osu.Framework.Configuration; + +namespace osu.Framework.Input.Handlers +{ + public abstract class InputHandler : IDisposable + { + /// + /// Used to initialize resources specific to this InputHandler. It gets called once. + /// + /// Success of the initialization. + public abstract bool Initialize(GameHost host); + + protected ConcurrentQueue PendingStates = new ConcurrentQueue(); + + private readonly object pendingStatesRetrievalLock = new object(); + + /// + /// Retrieve a list of all pending states since the last call to this method. + /// + public virtual List GetPendingStates() + { + lock (pendingStatesRetrievalLock) + { + List pending = new List(); + + while (PendingStates.TryDequeue(out InputState s)) + pending.Add(s); + + return pending; + } + } + + /// + /// Indicates whether this InputHandler is currently delivering input by the user. When handling input the OsuGame uses the first InputHandler which is active. + /// + public abstract bool IsActive { get; } + + /// + /// Indicated how high of a priority this handler has. The active handler with the highest priority is controlling the cursor at any given time. + /// + public abstract int Priority { get; } + + /// + /// Whether this InputHandler should be collecting s to return on the next call + /// + public readonly BindableBool Enabled = new BindableBool(true); + + public override string ToString() => GetType().Name; + + #region IDisposable Support + + protected bool IsDisposed; + + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) + return; + + Enabled.Value = false; + IsDisposed = true; + } + + ~InputHandler() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } + + public class InputHandlerComparer : IComparer + { + public int Compare(InputHandler h1, InputHandler h2) + { + if (h1 == null) throw new ArgumentNullException(nameof(h1)); + if (h2 == null) throw new ArgumentNullException(nameof(h2)); + + return h2.Priority.CompareTo(h1.Priority); + } + } +} diff --git a/osu.Framework/Input/Handlers/Keyboard/OpenTKKeyboardHandler.cs b/osu.Framework/Input/Handlers/Keyboard/OpenTKKeyboardHandler.cs index bf93b21fe..18c5c61c5 100644 --- a/osu.Framework/Input/Handlers/Keyboard/OpenTKKeyboardHandler.cs +++ b/osu.Framework/Input/Handlers/Keyboard/OpenTKKeyboardHandler.cs @@ -1,64 +1,64 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Platform; -using osu.Framework.Statistics; -using OpenTK.Input; - -namespace osu.Framework.Input.Handlers.Keyboard -{ - internal class OpenTKKeyboardHandler : InputHandler - { - public override bool IsActive => true; - - public override int Priority => 0; - - private OpenTK.Input.KeyboardState lastState; - - public override bool Initialize(GameHost host) - { - Enabled.ValueChanged += enabled => - { - if (enabled) - { - host.Window.KeyDown += handleState; - host.Window.KeyUp += handleState; - } - else - { - host.Window.KeyDown -= handleState; - host.Window.KeyUp -= handleState; - } - }; - Enabled.TriggerChange(); - return true; - } - - private void handleState(object sender, KeyboardKeyEventArgs e) - { - var state = e.Keyboard; - - if (state.Equals(lastState)) - return; - - lastState = state; - - PendingStates.Enqueue(new InputState { Keyboard = new TkKeyboardState(state) }); - FrameStatistics.Increment(StatisticsCounterType.KeyEvents); - } - - private class TkKeyboardState : KeyboardState - { - private static readonly IEnumerable all_keys = Enum.GetValues(typeof(Key)).Cast(); - - public TkKeyboardState(OpenTK.Input.KeyboardState tkState) - { - if (tkState.IsAnyKeyDown) - Keys = all_keys.Where(tkState.IsKeyDown); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using OpenTK.Input; + +namespace osu.Framework.Input.Handlers.Keyboard +{ + internal class OpenTKKeyboardHandler : InputHandler + { + public override bool IsActive => true; + + public override int Priority => 0; + + private OpenTK.Input.KeyboardState lastState; + + public override bool Initialize(GameHost host) + { + Enabled.ValueChanged += enabled => + { + if (enabled) + { + host.Window.KeyDown += handleState; + host.Window.KeyUp += handleState; + } + else + { + host.Window.KeyDown -= handleState; + host.Window.KeyUp -= handleState; + } + }; + Enabled.TriggerChange(); + return true; + } + + private void handleState(object sender, KeyboardKeyEventArgs e) + { + var state = e.Keyboard; + + if (state.Equals(lastState)) + return; + + lastState = state; + + PendingStates.Enqueue(new InputState { Keyboard = new TkKeyboardState(state) }); + FrameStatistics.Increment(StatisticsCounterType.KeyEvents); + } + + private class TkKeyboardState : KeyboardState + { + private static readonly IEnumerable all_keys = Enum.GetValues(typeof(Key)).Cast(); + + public TkKeyboardState(OpenTK.Input.KeyboardState tkState) + { + if (tkState.IsAnyKeyDown) + Keys = all_keys.Where(tkState.IsKeyDown); + } + } + } +} diff --git a/osu.Framework/Input/Handlers/Mouse/OpenTKEventMouseState.cs b/osu.Framework/Input/Handlers/Mouse/OpenTKEventMouseState.cs index c9c79a4af..440af988c 100644 --- a/osu.Framework/Input/Handlers/Mouse/OpenTKEventMouseState.cs +++ b/osu.Framework/Input/Handlers/Mouse/OpenTKEventMouseState.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Input.Handlers.Mouse -{ - /// - /// An OpenTK state which came from an event callback. - /// - internal class OpenTKEventMouseState : OpenTKMouseState - { - public OpenTKEventMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) - : base(tkState, active, mappedPosition) - { - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Input.Handlers.Mouse +{ + /// + /// An OpenTK state which came from an event callback. + /// + internal class OpenTKEventMouseState : OpenTKMouseState + { + public OpenTKEventMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) + : base(tkState, active, mappedPosition) + { + } + } +} diff --git a/osu.Framework/Input/Handlers/Mouse/OpenTKMouseHandler.cs b/osu.Framework/Input/Handlers/Mouse/OpenTKMouseHandler.cs index 84012da96..dd22a444f 100644 --- a/osu.Framework/Input/Handlers/Mouse/OpenTKMouseHandler.cs +++ b/osu.Framework/Input/Handlers/Mouse/OpenTKMouseHandler.cs @@ -1,102 +1,102 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Drawing; -using osu.Framework.Platform; -using osu.Framework.Statistics; -using osu.Framework.Threading; -using OpenTK; - -namespace osu.Framework.Input.Handlers.Mouse -{ - internal class OpenTKMouseHandler : InputHandler - { - private OpenTK.Input.MouseState? lastState; - private GameHost host; - - private bool mouseInWindow; - - private ScheduledDelegate scheduled; - - public override bool Initialize(GameHost host) - { - this.host = host; - - host.Window.MouseLeave += (s, e) => mouseInWindow = false; - host.Window.MouseEnter += (s, e) => mouseInWindow = true; - - mouseInWindow = host.Window.CursorInWindow; - - Enabled.ValueChanged += enabled => - { - if (enabled) - { - host.Window.MouseMove += handleMouseEvent; - host.Window.MouseDown += handleMouseEvent; - host.Window.MouseUp += handleMouseEvent; - host.Window.MouseWheel += handleMouseEvent; - - // polling is used to keep a valid mouse position when we aren't receiving events. - host.InputThread.Scheduler.Add(scheduled = new ScheduledDelegate(delegate - { - // we should be getting events if the mouse is inside the window. - if (mouseInWindow || !host.Window.Visible || host.Window.WindowState == WindowState.Minimized) return; - - var state = OpenTK.Input.Mouse.GetCursorState(); - - if (state.Equals(lastState)) return; - - lastState = state; - - var mapped = host.Window.PointToClient(new Point(state.X, state.Y)); - - handleState(new OpenTKPollMouseState(state, host.IsActive, new Vector2(mapped.X, mapped.Y))); - }, 0, 1000.0 / 60)); - } - else - { - scheduled?.Cancel(); - - host.Window.MouseMove -= handleMouseEvent; - host.Window.MouseDown -= handleMouseEvent; - host.Window.MouseUp -= handleMouseEvent; - host.Window.MouseWheel -= handleMouseEvent; - - lastState = null; - } - }; - Enabled.TriggerChange(); - return true; - } - - private void handleMouseEvent(object sender, OpenTK.Input.MouseEventArgs e) - { - if (!mouseInWindow) - return; - - if (e.Mouse.X < 0 || e.Mouse.Y < 0) - // todo: investigate further why we are getting negative values from OpenTK events - // on windows when crossing centre screen boundaries (width/2 or height/2). - return; - - handleState(new OpenTKEventMouseState(e.Mouse, host.IsActive, null)); - } - - private void handleState(MouseState state) - { - PendingStates.Enqueue(new InputState { Mouse = state }); - FrameStatistics.Increment(StatisticsCounterType.MouseEvents); - } - - /// - /// This input handler is always active, handling the cursor position if no other input handler does. - /// - public override bool IsActive => true; - - /// - /// Lowest priority. We want the normal mouse handler to only kick in if all other handlers don't do anything. - /// - public override int Priority => 0; - } -} - +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Drawing; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using OpenTK; + +namespace osu.Framework.Input.Handlers.Mouse +{ + internal class OpenTKMouseHandler : InputHandler + { + private OpenTK.Input.MouseState? lastState; + private GameHost host; + + private bool mouseInWindow; + + private ScheduledDelegate scheduled; + + public override bool Initialize(GameHost host) + { + this.host = host; + + host.Window.MouseLeave += (s, e) => mouseInWindow = false; + host.Window.MouseEnter += (s, e) => mouseInWindow = true; + + mouseInWindow = host.Window.CursorInWindow; + + Enabled.ValueChanged += enabled => + { + if (enabled) + { + host.Window.MouseMove += handleMouseEvent; + host.Window.MouseDown += handleMouseEvent; + host.Window.MouseUp += handleMouseEvent; + host.Window.MouseWheel += handleMouseEvent; + + // polling is used to keep a valid mouse position when we aren't receiving events. + host.InputThread.Scheduler.Add(scheduled = new ScheduledDelegate(delegate + { + // we should be getting events if the mouse is inside the window. + if (mouseInWindow || !host.Window.Visible || host.Window.WindowState == WindowState.Minimized) return; + + var state = OpenTK.Input.Mouse.GetCursorState(); + + if (state.Equals(lastState)) return; + + lastState = state; + + var mapped = host.Window.PointToClient(new Point(state.X, state.Y)); + + handleState(new OpenTKPollMouseState(state, host.IsActive, new Vector2(mapped.X, mapped.Y))); + }, 0, 1000.0 / 60)); + } + else + { + scheduled?.Cancel(); + + host.Window.MouseMove -= handleMouseEvent; + host.Window.MouseDown -= handleMouseEvent; + host.Window.MouseUp -= handleMouseEvent; + host.Window.MouseWheel -= handleMouseEvent; + + lastState = null; + } + }; + Enabled.TriggerChange(); + return true; + } + + private void handleMouseEvent(object sender, OpenTK.Input.MouseEventArgs e) + { + if (!mouseInWindow) + return; + + if (e.Mouse.X < 0 || e.Mouse.Y < 0) + // todo: investigate further why we are getting negative values from OpenTK events + // on windows when crossing centre screen boundaries (width/2 or height/2). + return; + + handleState(new OpenTKEventMouseState(e.Mouse, host.IsActive, null)); + } + + private void handleState(MouseState state) + { + PendingStates.Enqueue(new InputState { Mouse = state }); + FrameStatistics.Increment(StatisticsCounterType.MouseEvents); + } + + /// + /// This input handler is always active, handling the cursor position if no other input handler does. + /// + public override bool IsActive => true; + + /// + /// Lowest priority. We want the normal mouse handler to only kick in if all other handlers don't do anything. + /// + public override int Priority => 0; + } +} + diff --git a/osu.Framework/Input/Handlers/Mouse/OpenTKMouseState.cs b/osu.Framework/Input/Handlers/Mouse/OpenTKMouseState.cs index fed31741b..ca91a1ff8 100644 --- a/osu.Framework/Input/Handlers/Mouse/OpenTKMouseState.cs +++ b/osu.Framework/Input/Handlers/Mouse/OpenTKMouseState.cs @@ -1,43 +1,43 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using OpenTK.Input; - -namespace osu.Framework.Input.Handlers.Mouse -{ - internal abstract class OpenTKMouseState : MouseState - { - public readonly bool WasActive; - - public OpenTK.Input.MouseState RawState; - - public override int WheelDelta => WasActive ? base.WheelDelta : 0; - - protected OpenTKMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) - { - WasActive = active; - - RawState = tkState; - - // While not focused, let's silently ignore everything but position. - if (active && tkState.IsAnyButtonDown) - { - addIfPressed(tkState.LeftButton, MouseButton.Left); - addIfPressed(tkState.MiddleButton, MouseButton.Middle); - addIfPressed(tkState.RightButton, MouseButton.Right); - addIfPressed(tkState.XButton1, MouseButton.Button1); - addIfPressed(tkState.XButton2, MouseButton.Button2); - } - - Wheel = tkState.Wheel; - Position = new Vector2(mappedPosition?.X ?? tkState.X, mappedPosition?.Y ?? tkState.Y); - } - - private void addIfPressed(ButtonState tkState, MouseButton button) - { - if (tkState == ButtonState.Pressed) - SetPressed(button, true); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using OpenTK.Input; + +namespace osu.Framework.Input.Handlers.Mouse +{ + internal abstract class OpenTKMouseState : MouseState + { + public readonly bool WasActive; + + public OpenTK.Input.MouseState RawState; + + public override int WheelDelta => WasActive ? base.WheelDelta : 0; + + protected OpenTKMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) + { + WasActive = active; + + RawState = tkState; + + // While not focused, let's silently ignore everything but position. + if (active && tkState.IsAnyButtonDown) + { + addIfPressed(tkState.LeftButton, MouseButton.Left); + addIfPressed(tkState.MiddleButton, MouseButton.Middle); + addIfPressed(tkState.RightButton, MouseButton.Right); + addIfPressed(tkState.XButton1, MouseButton.Button1); + addIfPressed(tkState.XButton2, MouseButton.Button2); + } + + Wheel = tkState.Wheel; + Position = new Vector2(mappedPosition?.X ?? tkState.X, mappedPosition?.Y ?? tkState.Y); + } + + private void addIfPressed(ButtonState tkState, MouseButton button) + { + if (tkState == ButtonState.Pressed) + SetPressed(button, true); + } + } +} diff --git a/osu.Framework/Input/Handlers/Mouse/OpenTKPollMouseState.cs b/osu.Framework/Input/Handlers/Mouse/OpenTKPollMouseState.cs index f16eb2a4c..e6475d107 100644 --- a/osu.Framework/Input/Handlers/Mouse/OpenTKPollMouseState.cs +++ b/osu.Framework/Input/Handlers/Mouse/OpenTKPollMouseState.cs @@ -1,18 +1,18 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; - -namespace osu.Framework.Input.Handlers.Mouse -{ - /// - /// An OpenTK state which was retrieved via polling. - /// - internal class OpenTKPollMouseState : OpenTKMouseState - { - public OpenTKPollMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) - : base(tkState, active, mappedPosition) - { - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; + +namespace osu.Framework.Input.Handlers.Mouse +{ + /// + /// An OpenTK state which was retrieved via polling. + /// + internal class OpenTKPollMouseState : OpenTKMouseState + { + public OpenTKPollMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) + : base(tkState, active, mappedPosition) + { + } + } +} diff --git a/osu.Framework/Input/Handlers/Mouse/OpenTKRawMouseHandler.cs b/osu.Framework/Input/Handlers/Mouse/OpenTKRawMouseHandler.cs index 8ba10f733..27b15206a 100644 --- a/osu.Framework/Input/Handlers/Mouse/OpenTKRawMouseHandler.cs +++ b/osu.Framework/Input/Handlers/Mouse/OpenTKRawMouseHandler.cs @@ -1,212 +1,212 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.Platform; -using osu.Framework.Statistics; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Platform.Windows; - -namespace osu.Framework.Input.Handlers.Mouse -{ - internal class OpenTKRawMouseHandler : InputHandler, IHasCursorSensitivity - { - private ScheduledDelegate scheduled; - - private bool mouseInWindow; - - private readonly BindableDouble sensitivity = new BindableDouble(1) { MinValue = 0.1, MaxValue = 10 }; - - public BindableDouble Sensitivity => sensitivity; - - private readonly Bindable confineMode = new Bindable(); - private readonly Bindable windowMode = new Bindable(); - private readonly BindableBool mapAbsoluteInputToWindow = new BindableBool(); - - private int mostSeenStates; - private readonly List lastStates = new List(); - - private GameHost host; - - public override bool Initialize(GameHost host) - { - host.Window.MouseEnter += window_MouseEnter; - host.Window.MouseLeave += window_MouseLeave; - - this.host = host; - - mouseInWindow = host.Window.CursorInWindow; - - // Get the bindables we need to determine whether to confine the mouse to window or not - DesktopGameWindow desktopWindow = host.Window as DesktopGameWindow; - if (desktopWindow != null) - { - confineMode.BindTo(desktopWindow.ConfineMouseMode); - windowMode.BindTo(desktopWindow.WindowMode); - mapAbsoluteInputToWindow.BindTo(desktopWindow.MapAbsoluteInputToWindow); - } - - Enabled.ValueChanged += enabled => - { - if (enabled) - { - host.InputThread.Scheduler.Add(scheduled = new ScheduledDelegate(delegate - { - if (!host.Window.Visible || host.Window.WindowState == WindowState.Minimized) - return; - - if ((mouseInWindow || lastStates.Any(s => s.HasAnyButtonPressed)) && host.Window.Focused) - { - var newStates = new List(mostSeenStates + 1); - - for (int i = 0; i <= mostSeenStates + 1; i++) - { - var s = OpenTK.Input.Mouse.GetState(i); - if (s.IsConnected || i < mostSeenStates) - { - newStates.Add(s); - mostSeenStates = i; - } - } - - while (lastStates.Count < newStates.Count) - lastStates.Add(null); - - for (int i = 0; i < newStates.Count; i++) - { - if (newStates[i].IsConnected != true) - { - lastStates[i] = null; - continue; - } - - var state = newStates[i]; - var lastState = lastStates[i]; - - if (lastState != null && state.Equals(lastState.RawState)) - continue; - - var newState = new OpenTKPollMouseState(state, host.IsActive, getUpdatedPosition(state, lastState)) - { - LastState = lastState - }; - - lastStates[i] = newState; - - if (lastState != null) - { - PendingStates.Enqueue(new InputState { Mouse = newState }); - FrameStatistics.Increment(StatisticsCounterType.MouseEvents); - } - } - } - else - { - var state = OpenTK.Input.Mouse.GetCursorState(); - var screenPoint = host.Window.PointToClient(new Point(state.X, state.Y)); - PendingStates.Enqueue(new InputState { Mouse = new UnfocusedMouseState(new OpenTK.Input.MouseState(), host.IsActive, new Vector2(screenPoint.X, screenPoint.Y)) }); - FrameStatistics.Increment(StatisticsCounterType.MouseEvents); - - lastStates.Clear(); - } - }, 0, 0)); - } - else - { - scheduled?.Cancel(); - lastStates.Clear(); - } - }; - - Enabled.TriggerChange(); - return true; - } - - private Vector2 getUpdatedPosition(OpenTK.Input.MouseState state, OpenTKMouseState lastState) - { - Vector2 currentPosition; - - if ((state.RawFlags & RawMouseFlags.MOUSE_MOVE_ABSOLUTE) > 0) - { - const int raw_input_resolution = 65536; - - if (mapAbsoluteInputToWindow) - { - // map directly to local window - currentPosition.X = ((float)((state.X - raw_input_resolution / 2f) * sensitivity.Value) + raw_input_resolution / 2f) / raw_input_resolution * host.Window.Width; - currentPosition.Y = ((float)((state.Y - raw_input_resolution / 2f) * sensitivity.Value) + raw_input_resolution / 2f) / raw_input_resolution - * host.Window.Height; - } - else - { - Rectangle screenRect = (state.RawFlags & RawMouseFlags.MOUSE_VIRTUAL_DESKTOP) > 0 - ? Platform.Windows.Native.Input.GetVirtualScreenRect() - : new Rectangle(0, 0, DisplayDevice.Default.Width, DisplayDevice.Default.Height); - - // map to full screen space - currentPosition.X = (float)state.X / raw_input_resolution * screenRect.Width + screenRect.X; - currentPosition.Y = (float)state.Y / raw_input_resolution * screenRect.Height + screenRect.Y; - - // find local window coordinates - var clientPos = host.Window.PointToClient(new Point((int)Math.Round(currentPosition.X), (int)Math.Round(currentPosition.Y))); - - // apply sensitivity from window's centre - currentPosition.X = (float)((clientPos.X - host.Window.Width / 2f) * sensitivity.Value + host.Window.Width / 2f); - currentPosition.Y = (float)((clientPos.Y - host.Window.Height / 2f) * sensitivity.Value + host.Window.Height / 2f); - } - } - else - { - if (lastState == null) - { - // when we return from being outside of the window, we want to set the new position of our game cursor - // to where the OS cursor is, just once. - var cursorState = OpenTK.Input.Mouse.GetCursorState(); - var screenPoint = host.Window.PointToClient(new Point(cursorState.X, cursorState.Y)); - currentPosition = new Vector2(screenPoint.X, screenPoint.Y); - } - else - { - currentPosition = lastState.Position + new Vector2(state.X - lastState.RawState.X, state.Y - lastState.RawState.Y) * (float)sensitivity.Value; - - // When confining, clamp to the window size. - if (confineMode.Value == ConfineMouseMode.Always || confineMode.Value == ConfineMouseMode.Fullscreen && windowMode.Value == WindowMode.Fullscreen) - currentPosition = Vector2.Clamp(currentPosition, Vector2.Zero, new Vector2(host.Window.Width, host.Window.Height)); - - // update the windows cursor to match our raw cursor position. - // this is important when sensitivity is decreased below 1.0, where we need to ensure the cursor stays within the window. - var screenPoint = host.Window.PointToScreen(new Point((int)currentPosition.X, (int)currentPosition.Y)); - OpenTK.Input.Mouse.SetPosition(screenPoint.X, screenPoint.Y); - } - } - - return currentPosition; - } - - private void window_MouseLeave(object sender, EventArgs e) => mouseInWindow = false; - private void window_MouseEnter(object sender, EventArgs e) => mouseInWindow = true; - - /// - /// This input handler is always active, handling the cursor position if no other input handler does. - /// - public override bool IsActive => true; - - /// - /// Lowest priority. We want the normal mouse handler to only kick in if all other handlers don't do anything. - /// - public override int Priority => 0; - - private class UnfocusedMouseState : OpenTKMouseState - { - public UnfocusedMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) - : base(tkState, active, mappedPosition) - { - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Platform.Windows; + +namespace osu.Framework.Input.Handlers.Mouse +{ + internal class OpenTKRawMouseHandler : InputHandler, IHasCursorSensitivity + { + private ScheduledDelegate scheduled; + + private bool mouseInWindow; + + private readonly BindableDouble sensitivity = new BindableDouble(1) { MinValue = 0.1, MaxValue = 10 }; + + public BindableDouble Sensitivity => sensitivity; + + private readonly Bindable confineMode = new Bindable(); + private readonly Bindable windowMode = new Bindable(); + private readonly BindableBool mapAbsoluteInputToWindow = new BindableBool(); + + private int mostSeenStates; + private readonly List lastStates = new List(); + + private GameHost host; + + public override bool Initialize(GameHost host) + { + host.Window.MouseEnter += window_MouseEnter; + host.Window.MouseLeave += window_MouseLeave; + + this.host = host; + + mouseInWindow = host.Window.CursorInWindow; + + // Get the bindables we need to determine whether to confine the mouse to window or not + DesktopGameWindow desktopWindow = host.Window as DesktopGameWindow; + if (desktopWindow != null) + { + confineMode.BindTo(desktopWindow.ConfineMouseMode); + windowMode.BindTo(desktopWindow.WindowMode); + mapAbsoluteInputToWindow.BindTo(desktopWindow.MapAbsoluteInputToWindow); + } + + Enabled.ValueChanged += enabled => + { + if (enabled) + { + host.InputThread.Scheduler.Add(scheduled = new ScheduledDelegate(delegate + { + if (!host.Window.Visible || host.Window.WindowState == WindowState.Minimized) + return; + + if ((mouseInWindow || lastStates.Any(s => s.HasAnyButtonPressed)) && host.Window.Focused) + { + var newStates = new List(mostSeenStates + 1); + + for (int i = 0; i <= mostSeenStates + 1; i++) + { + var s = OpenTK.Input.Mouse.GetState(i); + if (s.IsConnected || i < mostSeenStates) + { + newStates.Add(s); + mostSeenStates = i; + } + } + + while (lastStates.Count < newStates.Count) + lastStates.Add(null); + + for (int i = 0; i < newStates.Count; i++) + { + if (newStates[i].IsConnected != true) + { + lastStates[i] = null; + continue; + } + + var state = newStates[i]; + var lastState = lastStates[i]; + + if (lastState != null && state.Equals(lastState.RawState)) + continue; + + var newState = new OpenTKPollMouseState(state, host.IsActive, getUpdatedPosition(state, lastState)) + { + LastState = lastState + }; + + lastStates[i] = newState; + + if (lastState != null) + { + PendingStates.Enqueue(new InputState { Mouse = newState }); + FrameStatistics.Increment(StatisticsCounterType.MouseEvents); + } + } + } + else + { + var state = OpenTK.Input.Mouse.GetCursorState(); + var screenPoint = host.Window.PointToClient(new Point(state.X, state.Y)); + PendingStates.Enqueue(new InputState { Mouse = new UnfocusedMouseState(new OpenTK.Input.MouseState(), host.IsActive, new Vector2(screenPoint.X, screenPoint.Y)) }); + FrameStatistics.Increment(StatisticsCounterType.MouseEvents); + + lastStates.Clear(); + } + }, 0, 0)); + } + else + { + scheduled?.Cancel(); + lastStates.Clear(); + } + }; + + Enabled.TriggerChange(); + return true; + } + + private Vector2 getUpdatedPosition(OpenTK.Input.MouseState state, OpenTKMouseState lastState) + { + Vector2 currentPosition; + + if ((state.RawFlags & RawMouseFlags.MOUSE_MOVE_ABSOLUTE) > 0) + { + const int raw_input_resolution = 65536; + + if (mapAbsoluteInputToWindow) + { + // map directly to local window + currentPosition.X = ((float)((state.X - raw_input_resolution / 2f) * sensitivity.Value) + raw_input_resolution / 2f) / raw_input_resolution * host.Window.Width; + currentPosition.Y = ((float)((state.Y - raw_input_resolution / 2f) * sensitivity.Value) + raw_input_resolution / 2f) / raw_input_resolution + * host.Window.Height; + } + else + { + Rectangle screenRect = (state.RawFlags & RawMouseFlags.MOUSE_VIRTUAL_DESKTOP) > 0 + ? Platform.Windows.Native.Input.GetVirtualScreenRect() + : new Rectangle(0, 0, DisplayDevice.Default.Width, DisplayDevice.Default.Height); + + // map to full screen space + currentPosition.X = (float)state.X / raw_input_resolution * screenRect.Width + screenRect.X; + currentPosition.Y = (float)state.Y / raw_input_resolution * screenRect.Height + screenRect.Y; + + // find local window coordinates + var clientPos = host.Window.PointToClient(new Point((int)Math.Round(currentPosition.X), (int)Math.Round(currentPosition.Y))); + + // apply sensitivity from window's centre + currentPosition.X = (float)((clientPos.X - host.Window.Width / 2f) * sensitivity.Value + host.Window.Width / 2f); + currentPosition.Y = (float)((clientPos.Y - host.Window.Height / 2f) * sensitivity.Value + host.Window.Height / 2f); + } + } + else + { + if (lastState == null) + { + // when we return from being outside of the window, we want to set the new position of our game cursor + // to where the OS cursor is, just once. + var cursorState = OpenTK.Input.Mouse.GetCursorState(); + var screenPoint = host.Window.PointToClient(new Point(cursorState.X, cursorState.Y)); + currentPosition = new Vector2(screenPoint.X, screenPoint.Y); + } + else + { + currentPosition = lastState.Position + new Vector2(state.X - lastState.RawState.X, state.Y - lastState.RawState.Y) * (float)sensitivity.Value; + + // When confining, clamp to the window size. + if (confineMode.Value == ConfineMouseMode.Always || confineMode.Value == ConfineMouseMode.Fullscreen && windowMode.Value == WindowMode.Fullscreen) + currentPosition = Vector2.Clamp(currentPosition, Vector2.Zero, new Vector2(host.Window.Width, host.Window.Height)); + + // update the windows cursor to match our raw cursor position. + // this is important when sensitivity is decreased below 1.0, where we need to ensure the cursor stays within the window. + var screenPoint = host.Window.PointToScreen(new Point((int)currentPosition.X, (int)currentPosition.Y)); + OpenTK.Input.Mouse.SetPosition(screenPoint.X, screenPoint.Y); + } + } + + return currentPosition; + } + + private void window_MouseLeave(object sender, EventArgs e) => mouseInWindow = false; + private void window_MouseEnter(object sender, EventArgs e) => mouseInWindow = true; + + /// + /// This input handler is always active, handling the cursor position if no other input handler does. + /// + public override bool IsActive => true; + + /// + /// Lowest priority. We want the normal mouse handler to only kick in if all other handlers don't do anything. + /// + public override int Priority => 0; + + private class UnfocusedMouseState : OpenTKMouseState + { + public UnfocusedMouseState(OpenTK.Input.MouseState tkState, bool active, Vector2? mappedPosition) + : base(tkState, active, mappedPosition) + { + } + } + } +} diff --git a/osu.Framework/Input/IHandleGlobalInput.cs b/osu.Framework/Input/IHandleGlobalInput.cs index 0c9e10b02..cd62bd1e6 100644 --- a/osu.Framework/Input/IHandleGlobalInput.cs +++ b/osu.Framework/Input/IHandleGlobalInput.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Input -{ - /// - /// Denotes that this class handles input globally. - /// - public interface IHandleGlobalInput - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Input +{ + /// + /// Denotes that this class handles input globally. + /// + public interface IHandleGlobalInput + { + } +} diff --git a/osu.Framework/Input/IKeyboardState.cs b/osu.Framework/Input/IKeyboardState.cs index 82c985417..a3bff33d9 100644 --- a/osu.Framework/Input/IKeyboardState.cs +++ b/osu.Framework/Input/IKeyboardState.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public interface IKeyboardState - { - bool AltPressed { get; } - bool ControlPressed { get; } - bool ShiftPressed { get; } - - /// - /// Win key on Windows, or Command key on Mac. - /// - bool SuperPressed { get; } - - IEnumerable Keys { get; set; } - - IKeyboardState Clone(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public interface IKeyboardState + { + bool AltPressed { get; } + bool ControlPressed { get; } + bool ShiftPressed { get; } + + /// + /// Win key on Windows, or Command key on Mac. + /// + bool SuperPressed { get; } + + IEnumerable Keys { get; set; } + + IKeyboardState Clone(); + } +} diff --git a/osu.Framework/Input/IMouseState.cs b/osu.Framework/Input/IMouseState.cs index 8db42bd34..5139c098c 100644 --- a/osu.Framework/Input/IMouseState.cs +++ b/osu.Framework/Input/IMouseState.cs @@ -1,39 +1,39 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using OpenTK; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public interface IMouseState - { - IMouseState NativeState { get; } - - IMouseState LastState { get; set; } - - IReadOnlyList Buttons { get; } - - Vector2 Delta { get; } - Vector2 Position { get; } - - Vector2 LastPosition { get; } - - Vector2? PositionMouseDown { get; set; } - - bool HasMainButtonPressed { get; } - - bool HasAnyButtonPressed { get; } - - bool IsPressed(MouseButton button); - - void SetPressed(MouseButton button, bool pressed); - - int Wheel { get; } - - int WheelDelta { get; } - - IMouseState Clone(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using OpenTK; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public interface IMouseState + { + IMouseState NativeState { get; } + + IMouseState LastState { get; set; } + + IReadOnlyList Buttons { get; } + + Vector2 Delta { get; } + Vector2 Position { get; } + + Vector2 LastPosition { get; } + + Vector2? PositionMouseDown { get; set; } + + bool HasMainButtonPressed { get; } + + bool HasAnyButtonPressed { get; } + + bool IsPressed(MouseButton button); + + void SetPressed(MouseButton button, bool pressed); + + int Wheel { get; } + + int WheelDelta { get; } + + IMouseState Clone(); + } +} diff --git a/osu.Framework/Input/IRequireHighFrequencyMousePosition.cs b/osu.Framework/Input/IRequireHighFrequencyMousePosition.cs index 058543534..ac2abdb21 100644 --- a/osu.Framework/Input/IRequireHighFrequencyMousePosition.cs +++ b/osu.Framework/Input/IRequireHighFrequencyMousePosition.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; - -namespace osu.Framework.Input -{ - /// - /// Guarantees that a drawable will receive at least one OnMouseMove position update - /// per update frame (in addition to any input-triggered occurrences). - /// - public interface IRequireHighFrequencyMousePosition : IDrawable - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; + +namespace osu.Framework.Input +{ + /// + /// Guarantees that a drawable will receive at least one OnMouseMove position update + /// per update frame (in addition to any input-triggered occurrences). + /// + public interface IRequireHighFrequencyMousePosition : IDrawable + { + } +} diff --git a/osu.Framework/Input/ITextInputSource.cs b/osu.Framework/Input/ITextInputSource.cs index 2f0637edc..e01c5716f 100644 --- a/osu.Framework/Input/ITextInputSource.cs +++ b/osu.Framework/Input/ITextInputSource.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Input -{ - /// - /// A source from which we can retrieve user text input. - /// Generally hides a native implementation from the game framework. - /// - public interface ITextInputSource - { - bool ImeActive { get; } - - string GetPendingText(); - - void Deactivate(object sender); - - void Activate(object sender); - - event Action OnNewImeComposition; - event Action OnNewImeResult; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Input +{ + /// + /// A source from which we can retrieve user text input. + /// Generally hides a native implementation from the game framework. + /// + public interface ITextInputSource + { + bool ImeActive { get; } + + string GetPendingText(); + + void Deactivate(object sender); + + void Activate(object sender); + + event Action OnNewImeComposition; + event Action OnNewImeResult; + } +} diff --git a/osu.Framework/Input/InputManager.cs b/osu.Framework/Input/InputManager.cs index a38208f20..48ec1e648 100644 --- a/osu.Framework/Input/InputManager.cs +++ b/osu.Framework/Input/InputManager.cs @@ -1,834 +1,834 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Handlers; -using osu.Framework.Logging; -using osu.Framework.Platform; -using OpenTK; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public abstract class InputManager : Container, IRequireHighFrequencyMousePosition - { - /// - /// The initial delay before key repeat begins. - /// - private const int repeat_initial_delay = 250; - - /// - /// The delay between key repeats after the initial repeat. - /// - private const int repeat_tick_rate = 70; - - /// - /// The maximum time between two clicks for a double-click to be considered. - /// - private const int double_click_time = 250; - - /// - /// The distance that must be moved until a dragged click becomes invalid. - /// - private const float click_drag_distance = 10; - - /// - /// The time of the last input action. - /// - public double LastActionTime; - - protected GameHost Host; - - internal Drawable FocusedDrawable; - - protected abstract IEnumerable InputHandlers { get; } - - private double lastClickTime; - - private double keyboardRepeatTime; - - private bool isDragging; - - private bool isValidClick; - - /// - /// The last processed state. - /// - public InputState CurrentState = new InputState(); - - /// - /// The sequential list in which to handle mouse input. - /// - private readonly List positionalInputQueue = new List(); - - /// - /// The sequential list in which to handle keyboard input. - /// - private readonly List inputQueue = new List(); - - /// - /// The which is currently being dragged. null if none is. - /// - public Drawable DraggedDrawable { get; private set; } - - /// - /// Contains the previously hovered s prior to when - /// got updated. - /// - private readonly List lastHoveredDrawables = new List(); - - /// - /// Contains all hovered s in top-down order up to the first - /// which returned true in its method. - /// Top-down in this case means reverse draw order, i.e. the front-most visible - /// first, and s after their children. - /// - private readonly List hoveredDrawables = new List(); - - /// - /// The which returned true in its - /// method, or null if none did so. - /// - private Drawable hoverHandledDrawable; - - /// - /// Contains all hovered s in top-down order up to the first - /// which returned true in its method. - /// Top-down in this case means reverse draw order, i.e. the front-most visible - /// first, and s after their children. - /// - public IReadOnlyList HoveredDrawables => hoveredDrawables; - - /// - /// Contains all s in top-down order which are considered - /// for positional input. This list is the same as , only - /// that the return value of is not taken - /// into account. - /// - public IReadOnlyList PositionalInputQueue => positionalInputQueue; - - /// - /// Contains all s in top-down order which are considered - /// for non-positional input. - /// - public IReadOnlyList InputQueue => inputQueue; - - protected InputManager() - { - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader(permitNulls: true)] - private void load(GameHost host) - { - Host = host; - } - - /// - /// Reset current focused drawable to the top-most drawable which is . - /// - /// The source which triggered this event. - public void TriggerFocusContention(Drawable triggerSource) - { - if (FocusedDrawable == null) return; - - Logger.Log($"Focus contention triggered by {triggerSource}."); - ChangeFocus(null); - } - - /// - /// Changes the currently-focused drawable. First checks that is in a valid state to receive focus, - /// then unfocuses the current and focuses . - /// can be null to reset focus. - /// If the given drawable is already focused, nothing happens and no events are fired. - /// - /// The drawable to become focused. - /// True if the given drawable is now focused (or focus is dropped in the case of a null target). - public bool ChangeFocus(Drawable potentialFocusTarget) => ChangeFocus(potentialFocusTarget, CurrentState); - - /// - /// Changes the currently-focused drawable. First checks that is in a valid state to receive focus, - /// then unfocuses the current and focuses . - /// can be null to reset focus. - /// If the given drawable is already focused, nothing happens and no events are fired. - /// - /// The drawable to become focused. - /// The associated with the focusing event. - /// True if the given drawable is now focused (or focus is dropped in the case of a null target). - protected bool ChangeFocus(Drawable potentialFocusTarget, InputState state) - { - if (potentialFocusTarget == FocusedDrawable) - return true; - - if (potentialFocusTarget != null && (!potentialFocusTarget.IsPresent || !potentialFocusTarget.AcceptsFocus)) - return false; - - var previousFocus = FocusedDrawable; - - FocusedDrawable = null; - - if (previousFocus != null) - { - previousFocus.HasFocus = false; - previousFocus.TriggerOnFocusLost(state); - - if (FocusedDrawable != null) throw new InvalidOperationException($"Focus cannot be changed inside {nameof(OnFocusLost)}"); - } - - FocusedDrawable = potentialFocusTarget; - - Logger.Log($"Focus switched to {FocusedDrawable?.ToString() ?? "nothing"}.", LoggingTarget.Runtime, LogLevel.Debug); - - if (FocusedDrawable != null) - { - FocusedDrawable.HasFocus = true; - FocusedDrawable.TriggerOnFocus(state); - } - - return true; - } - - internal override bool BuildKeyboardInputQueue(List queue) => false; - - internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) => false; - - protected override void Update() - { - List distinctStates = createDistinctStates(GetPendingStates()).ToList(); - - //we need to make sure the code in the foreach below is run at least once even if we have no new pending states. - if (distinctStates.Count == 0) - distinctStates.Add(new InputState()); - - unfocusIfNoLongerValid(); - - foreach (InputState s in distinctStates) - HandleNewState(s); - - if (CurrentState.Mouse != null) - { - foreach (var d in positionalInputQueue) - if (d is IRequireHighFrequencyMousePosition) - if (d.TriggerOnMouseMove(CurrentState)) - break; - } - - keyboardRepeatTime -= Time.Elapsed; - - if (FocusedDrawable == null) - focusTopMostRequestingDrawable(); - - base.Update(); - } - - protected virtual void HandleNewState(InputState state) - { - bool hasNewKeyboard = state.Keyboard != null; - bool hasNewMouse = state.Mouse != null; - - var last = CurrentState; - - //avoid lingering references that would stay forever. - last.Last = null; - - CurrentState = state; - CurrentState.Last = last; - - if (CurrentState.Keyboard == null) CurrentState.Keyboard = last.Keyboard ?? new KeyboardState(); - if (CurrentState.Mouse == null) CurrentState.Mouse = last.Mouse ?? new MouseState(); - - TransformState(CurrentState); - - //move above? - updateInputQueues(CurrentState); - - // we only want to set a last state if both the new and old state are of the same type. - // this avoids giving the new state a false impression of being able to calculate delta values based on a last - // state potentially from a different input source. - if (last.Mouse != null && state.Mouse != null) - { - // only set the last state if one hasn't already been set (in addition to being the same type of state). - // a smarter InputHandler may do this internally, if they are handling input from multiple distinct devices. - if (state.Mouse.LastState == null && last.Mouse.GetType() == state.Mouse.GetType()) - state.Mouse.LastState = last.Mouse; - - if (last.Mouse.HasAnyButtonPressed) - state.Mouse.PositionMouseDown = last.Mouse.PositionMouseDown; - } - - //hover could change even when the mouse state has not. - updateHoverEvents(CurrentState); - - if (hasNewMouse) - updateMouseEvents(CurrentState); - - if (hasNewKeyboard || CurrentState.Keyboard.Keys.Any()) - updateKeyboardEvents(CurrentState); - } - - protected virtual List GetPendingStates() - { - var pendingStates = new List(); - - foreach (var h in InputHandlers) - { - if (h.IsActive && h.Enabled) - pendingStates.AddRange(h.GetPendingStates()); - else - h.GetPendingStates(); - } - - return pendingStates; - } - - protected virtual void TransformState(InputState inputState) - { - } - - private void updateInputQueues(InputState state) - { - inputQueue.Clear(); - positionalInputQueue.Clear(); - - if (state.Keyboard != null) - foreach (Drawable d in AliveInternalChildren) - d.BuildKeyboardInputQueue(inputQueue); - - if (state.Mouse != null) - foreach (Drawable d in AliveInternalChildren) - d.BuildMouseInputQueue(state.Mouse.Position, positionalInputQueue); - - // Keyboard and mouse queues were created in back-to-front order. - // We want input to first reach front-most drawables, so the queues - // need to be reversed. - inputQueue.Reverse(); - positionalInputQueue.Reverse(); - } - - protected virtual bool HandleHoverEvents => true; - - private void updateHoverEvents(InputState state) - { - Drawable lastHoverHandledDrawable = hoverHandledDrawable; - hoverHandledDrawable = null; - - lastHoveredDrawables.Clear(); - lastHoveredDrawables.AddRange(hoveredDrawables); - hoveredDrawables.Clear(); - - // New drawables shouldn't be hovered if the cursor isn't in the window - if (HandleHoverEvents) - { - // First, we need to construct hoveredDrawables for the current frame - foreach (Drawable d in positionalInputQueue) - { - hoveredDrawables.Add(d); - - // Don't need to re-hover those that are already hovered - if (d.IsHovered) - { - // Check if this drawable previously handled hover, and assume it would once more - if (d == lastHoverHandledDrawable) - { - hoverHandledDrawable = lastHoverHandledDrawable; - break; - } - - continue; - } - - d.IsHovered = true; - if (d.TriggerOnHover(state)) - { - hoverHandledDrawable = d; - break; - } - } - } - - // Unhover all previously hovered drawables which are no longer hovered. - foreach (Drawable d in lastHoveredDrawables.Except(hoveredDrawables)) - { - d.IsHovered = false; - d.TriggerOnHoverLost(state); - } - } - - private void updateKeyboardEvents(InputState state) - { - KeyboardState keyboard = (KeyboardState)state.Keyboard; - - if (!keyboard.Keys.Any()) - keyboardRepeatTime = 0; - - var last = state.Last?.Keyboard; - - if (last == null) return; - - foreach (var k in last.Keys) - { - if (!keyboard.Keys.Contains(k)) - handleKeyUp(state, k); - } - - foreach (Key k in keyboard.Keys.Distinct()) - { - bool isModifier = k == Key.LControl || k == Key.RControl - || k == Key.LAlt || k == Key.RAlt - || k == Key.LShift || k == Key.RShift - || k == Key.LWin || k == Key.RWin; - - LastActionTime = Time.Current; - - bool isRepetition = last.Keys.Contains(k); - - if (isModifier) - { - //modifiers shouldn't affect or report key repeat - if (!isRepetition) - handleKeyDown(state, k, false); - continue; - } - - if (isRepetition) - { - if (keyboardRepeatTime <= 0) - { - keyboardRepeatTime += repeat_tick_rate; - handleKeyDown(state, k, true); - } - } - else - { - keyboardRepeatTime = repeat_initial_delay; - handleKeyDown(state, k, false); - } - } - } - - private List mouseDownInputQueue; - - private void updateMouseEvents(InputState state) - { - MouseState mouse = (MouseState)state.Mouse; - - var last = state.Last?.Mouse as MouseState; - - if (last == null) return; - - if (mouse.Position != last.Position) - { - handleMouseMove(state); - if (isDragging) - handleMouseDrag(state); - } - - for (MouseButton b = 0; b < MouseButton.LastButton; b++) - { - var lastPressed = last.IsPressed(b); - - if (lastPressed != mouse.IsPressed(b)) - { - if (lastPressed) - handleMouseUp(state, b); - else - handleMouseDown(state, b); - } - } - - if (mouse.WheelDelta != 0 && Host.Window.CursorInWindow) - handleWheel(state); - - if (mouse.HasAnyButtonPressed) - { - if (!last.HasAnyButtonPressed) - { - //stuff which only happens once after the mousedown state - mouse.PositionMouseDown = state.Mouse.Position; - LastActionTime = Time.Current; - - if (mouse.IsPressed(MouseButton.Left)) - { - isValidClick = true; - - if (Time.Current - lastClickTime < double_click_time) - { - if (handleMouseDoubleClick(state)) - //when we handle a double-click we want to block a normal click from firing. - isValidClick = false; - - lastClickTime = 0; - } - else - lastClickTime = Time.Current; - } - } - - if (!isDragging && Vector2Extensions.Distance(mouse.PositionMouseDown ?? mouse.Position, mouse.Position) > click_drag_distance) - { - isDragging = true; - handleMouseDragStart(state); - } - } - else if (last.HasAnyButtonPressed) - { - if (isValidClick && (DraggedDrawable == null || Vector2Extensions.Distance(mouse.PositionMouseDown ?? mouse.Position, mouse.Position) <= click_drag_distance)) - handleMouseClick(state); - - mouseDownInputQueue = null; - mouse.PositionMouseDown = null; - isValidClick = false; - - if (isDragging) - { - isDragging = false; - handleMouseDragEnd(state); - } - } - } - - private bool handleMouseDown(InputState state, MouseButton button) - { - MouseDownEventArgs args = new MouseDownEventArgs - { - Button = button - }; - - var result = PropagateMouseDown(positionalInputQueue, state, args, out Drawable handledBy); - - mouseDownInputQueue = new List(result ? positionalInputQueue.Take(positionalInputQueue.IndexOf(handledBy) + 1) : positionalInputQueue); - - return result; - } - - private bool handleMouseUp(InputState state, MouseButton button) - { - if (mouseDownInputQueue == null) return false; - - MouseUpEventArgs args = new MouseUpEventArgs - { - Button = button - }; - - //extra check for IsAlive because we are using an outdated queue. - return PropagateMouseUp(mouseDownInputQueue.Where(target => target.IsAlive && target.IsPresent), state, args); - } - - /// - /// Triggers mouse up events on drawables in until it is handled. - /// - /// The drawables in the queue. - /// The input state. - /// The args. - /// Whether the mouse up event was handled. - protected virtual bool PropagateMouseUp(IEnumerable drawables, InputState state, MouseUpEventArgs args) - { - var handledBy = drawables.FirstOrDefault(target => target.TriggerOnMouseUp(state, args)); - - if (handledBy != null) - Logger.Log($"MouseUp ({args.Button}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handledBy != null; - } - - /// - /// Triggers mouse down events on drawables in until it is handled. - /// - /// The drawables in the queue. - /// The input state. - /// The args. - /// - /// Whether the mouse down event was handled. - protected virtual bool PropagateMouseDown(IEnumerable drawables, InputState state, MouseDownEventArgs args, out Drawable handledBy) - { - handledBy = drawables.FirstOrDefault(target => target.TriggerOnMouseDown(state, args)); - - if (handledBy != null) - Logger.Log($"MouseDown ({args.Button}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handledBy != null; - } - - private bool handleMouseMove(InputState state) - { - return positionalInputQueue.Any(target => target.TriggerOnMouseMove(state)); - } - - private Drawable clickedDrawable; - - private bool handleMouseClick(InputState state) - { - var intersectingQueue = positionalInputQueue.Intersect(mouseDownInputQueue); - - Drawable focusTarget = null; - - // click pass, triggering an OnClick on all drawables up to the first which returns true. - // an extra IsHovered check is performed because we are using an outdated queue (for valid reasons which we need to document). - clickedDrawable = intersectingQueue.FirstOrDefault(t => t.CanReceiveMouseInput && t.ReceiveMouseInputAt(state.Mouse.Position) && t.TriggerOnClick(state)); - - if (clickedDrawable != null) - { - focusTarget = clickedDrawable; - - if (!focusTarget.AcceptsFocus) - { - // search upwards from the clicked drawable until we find something to handle focus. - Drawable previousFocused = FocusedDrawable; - - while (focusTarget?.AcceptsFocus == false) - focusTarget = focusTarget.Parent; - - if (focusTarget != null && previousFocused != null) - { - // we found a focusable target above us. - // now search upwards from previousFocused to check whether focusTarget is a common parent. - Drawable search = previousFocused; - while (search != null && search != focusTarget) - search = search.Parent; - - if (focusTarget == search) - // we have a common parent, so let's keep focus on the previously focused target. - focusTarget = previousFocused; - } - } - } - - ChangeFocus(focusTarget, state); - - if (clickedDrawable != null) - Logger.Log($"MouseClick handled by {clickedDrawable}.", LoggingTarget.Runtime, LogLevel.Debug); - - return clickedDrawable != null; - } - - private bool handleMouseDoubleClick(InputState state) - { - if (clickedDrawable == null) return false; - - return clickedDrawable.ReceiveMouseInputAt(state.Mouse.Position) && clickedDrawable.TriggerOnDoubleClick(state); - } - - private bool handleMouseDrag(InputState state) - { - //Once a drawable is dragged, it remains in a dragged state until the drag is finished. - return DraggedDrawable?.TriggerOnDrag(state) ?? false; - } - - private bool handleMouseDragStart(InputState state) - { - Trace.Assert(DraggedDrawable == null, "The draggingDrawable was not set to null by handleMouseDragEnd."); - DraggedDrawable = mouseDownInputQueue?.FirstOrDefault(target => target.IsAlive && target.IsPresent && target.TriggerOnDragStart(state)); - if (DraggedDrawable != null) - { - DraggedDrawable.IsDragged = true; - Logger.Log($"MouseDragStart handled by {DraggedDrawable}.", LoggingTarget.Runtime, LogLevel.Debug); - } - - return DraggedDrawable != null; - } - - private bool handleMouseDragEnd(InputState state) - { - if (DraggedDrawable == null) - return false; - - bool result = DraggedDrawable.TriggerOnDragEnd(state); - DraggedDrawable.IsDragged = false; - DraggedDrawable = null; - - return result; - } - - private bool handleWheel(InputState state) - { - return PropagateWheel(positionalInputQueue, state); - } - - /// - /// Triggers wheel events on drawables in until it is handled. - /// - /// The drawables in the queue. - /// The input state. - /// - protected virtual bool PropagateWheel(IEnumerable drawables, InputState state) - { - var handledBy = drawables.FirstOrDefault(target => target.TriggerOnWheel(state)); - - if (handledBy != null) - Logger.Log($"Wheel ({state.Mouse.WheelDelta}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handledBy != null; - } - - private bool handleKeyDown(InputState state, Key key, bool repeat) - { - IEnumerable queue = inputQueue; - if (!unfocusIfNoLongerValid()) - queue = queue.Prepend(FocusedDrawable); - - return PropagateKeyDown(queue, state, new KeyDownEventArgs { Key = key, Repeat = repeat }); - } - - /// - /// Triggers key down events on drawables in until it is handled. - /// - /// The drawables in the queue. - /// The input state. - /// The args. - /// Whether the key down event was handled. - protected virtual bool PropagateKeyDown(IEnumerable drawables, InputState state, KeyDownEventArgs args) - { - var handledBy = drawables.FirstOrDefault(target => target.TriggerOnKeyDown(state, args)); - - if (handledBy != null) - Logger.Log($"KeyDown ({args.Key}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handledBy != null; - } - - private bool handleKeyUp(InputState state, Key key) - { - IEnumerable queue = inputQueue; - if (!unfocusIfNoLongerValid()) - queue = queue.Prepend(FocusedDrawable); - - return PropagateKeyUp(queue, state, new KeyUpEventArgs { Key = key }); - } - - /// - /// Triggers key up events on drawables in until it is handled. - /// - /// The drawables in the queue. - /// The input state. - /// The args. - /// Whether the key up event was handled. - protected virtual bool PropagateKeyUp(IEnumerable drawables, InputState state, KeyUpEventArgs args) - { - var handledBy = drawables.FirstOrDefault(target => target.TriggerOnKeyUp(state, args)); - - if (handledBy != null) - Logger.Log($"KeyUp ({args.Key}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); - - return handledBy != null; - } - - /// - /// Unfocus the current focused drawable if it is no longer in a valid state. - /// - /// true if there is no longer a focus. - private bool unfocusIfNoLongerValid() - { - if (FocusedDrawable == null) return true; - - bool stillValid = FocusedDrawable.IsPresent && FocusedDrawable.Parent != null; - - if (stillValid) - { - //ensure we are visible - CompositeDrawable d = FocusedDrawable.Parent; - while (d != null) - { - if (!d.IsPresent) - { - stillValid = false; - break; - } - - d = d.Parent; - } - } - - if (stillValid) - return false; - - ChangeFocus(null); - return true; - } - - private void focusTopMostRequestingDrawable() => ChangeFocus(inputQueue.FirstOrDefault(target => target.RequestsFocus)); - - /// - /// In order to provide a reliable event system to drawables, we want to ensure that we reprocess input queues (via the - /// main loop in after each and every button or key change. This allows - /// correct behaviour in a case where the input queues change based on triggered by a button or key. - /// - /// One ore more states which are to be converted to distinct states. - /// Processed states such that at most one attribute change occurs between any two consecutive states. - private IEnumerable createDistinctStates(IEnumerable newStates) - { - IKeyboardState lastKeyboard = CurrentState.Keyboard; - IMouseState lastMouse = CurrentState.Mouse; - - foreach (var state in newStates) - { - if (state.Mouse == null && state.Keyboard == null) - { - // we still want to return at least one state change. - yield return state; - } - - if (state.Mouse != null) - { - // first we want to create a copy of ourselves without any button changes - // this is done only for mouse handlers, as they have positional data we want to handle in a separate pass. - var iWithoutButtons = state.Mouse.Clone(); - - for (MouseButton b = 0; b < MouseButton.LastButton; b++) - iWithoutButtons.SetPressed(b, lastMouse?.IsPressed(b) ?? false); - - //we start by adding this state to the processed list... - var newState = state.Clone(); - newState.Mouse = lastMouse = iWithoutButtons; - yield return newState; - - //and then iterate over each button/key change, adding intermediate states along the way. - for (MouseButton b = 0; b < MouseButton.LastButton; b++) - { - if (state.Mouse.IsPressed(b) != (lastMouse?.IsPressed(b) ?? false)) - { - lastMouse = lastMouse?.Clone() ?? new MouseState(); - - //add our single local change - lastMouse.SetPressed(b, state.Mouse.IsPressed(b)); - - newState = state.Clone(); - newState.Mouse = lastMouse; - yield return newState; - } - } - } - - if (state.Keyboard != null) - { - if (lastKeyboard != null) - foreach (var releasedKey in lastKeyboard.Keys.Except(state.Keyboard.Keys)) - { - var newState = state.Clone(); - newState.Keyboard = lastKeyboard = new KeyboardState { Keys = lastKeyboard.Keys.Where(d => d != releasedKey).ToArray() }; - yield return newState; - } - - foreach (var pressedKey in state.Keyboard.Keys.Except(lastKeyboard?.Keys ?? new Key[] { })) - { - var newState = state.Clone(); - newState.Keyboard = lastKeyboard = new KeyboardState { Keys = lastKeyboard?.Keys.Union(new[] { pressedKey }) ?? new[] { pressedKey } }; - yield return newState; - } - } - } - } - } - - public enum ConfineMouseMode - { - Never, - Fullscreen, - Always - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Handlers; +using osu.Framework.Logging; +using osu.Framework.Platform; +using OpenTK; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public abstract class InputManager : Container, IRequireHighFrequencyMousePosition + { + /// + /// The initial delay before key repeat begins. + /// + private const int repeat_initial_delay = 250; + + /// + /// The delay between key repeats after the initial repeat. + /// + private const int repeat_tick_rate = 70; + + /// + /// The maximum time between two clicks for a double-click to be considered. + /// + private const int double_click_time = 250; + + /// + /// The distance that must be moved until a dragged click becomes invalid. + /// + private const float click_drag_distance = 10; + + /// + /// The time of the last input action. + /// + public double LastActionTime; + + protected GameHost Host; + + internal Drawable FocusedDrawable; + + protected abstract IEnumerable InputHandlers { get; } + + private double lastClickTime; + + private double keyboardRepeatTime; + + private bool isDragging; + + private bool isValidClick; + + /// + /// The last processed state. + /// + public InputState CurrentState = new InputState(); + + /// + /// The sequential list in which to handle mouse input. + /// + private readonly List positionalInputQueue = new List(); + + /// + /// The sequential list in which to handle keyboard input. + /// + private readonly List inputQueue = new List(); + + /// + /// The which is currently being dragged. null if none is. + /// + public Drawable DraggedDrawable { get; private set; } + + /// + /// Contains the previously hovered s prior to when + /// got updated. + /// + private readonly List lastHoveredDrawables = new List(); + + /// + /// Contains all hovered s in top-down order up to the first + /// which returned true in its method. + /// Top-down in this case means reverse draw order, i.e. the front-most visible + /// first, and s after their children. + /// + private readonly List hoveredDrawables = new List(); + + /// + /// The which returned true in its + /// method, or null if none did so. + /// + private Drawable hoverHandledDrawable; + + /// + /// Contains all hovered s in top-down order up to the first + /// which returned true in its method. + /// Top-down in this case means reverse draw order, i.e. the front-most visible + /// first, and s after their children. + /// + public IReadOnlyList HoveredDrawables => hoveredDrawables; + + /// + /// Contains all s in top-down order which are considered + /// for positional input. This list is the same as , only + /// that the return value of is not taken + /// into account. + /// + public IReadOnlyList PositionalInputQueue => positionalInputQueue; + + /// + /// Contains all s in top-down order which are considered + /// for non-positional input. + /// + public IReadOnlyList InputQueue => inputQueue; + + protected InputManager() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(GameHost host) + { + Host = host; + } + + /// + /// Reset current focused drawable to the top-most drawable which is . + /// + /// The source which triggered this event. + public void TriggerFocusContention(Drawable triggerSource) + { + if (FocusedDrawable == null) return; + + Logger.Log($"Focus contention triggered by {triggerSource}."); + ChangeFocus(null); + } + + /// + /// Changes the currently-focused drawable. First checks that is in a valid state to receive focus, + /// then unfocuses the current and focuses . + /// can be null to reset focus. + /// If the given drawable is already focused, nothing happens and no events are fired. + /// + /// The drawable to become focused. + /// True if the given drawable is now focused (or focus is dropped in the case of a null target). + public bool ChangeFocus(Drawable potentialFocusTarget) => ChangeFocus(potentialFocusTarget, CurrentState); + + /// + /// Changes the currently-focused drawable. First checks that is in a valid state to receive focus, + /// then unfocuses the current and focuses . + /// can be null to reset focus. + /// If the given drawable is already focused, nothing happens and no events are fired. + /// + /// The drawable to become focused. + /// The associated with the focusing event. + /// True if the given drawable is now focused (or focus is dropped in the case of a null target). + protected bool ChangeFocus(Drawable potentialFocusTarget, InputState state) + { + if (potentialFocusTarget == FocusedDrawable) + return true; + + if (potentialFocusTarget != null && (!potentialFocusTarget.IsPresent || !potentialFocusTarget.AcceptsFocus)) + return false; + + var previousFocus = FocusedDrawable; + + FocusedDrawable = null; + + if (previousFocus != null) + { + previousFocus.HasFocus = false; + previousFocus.TriggerOnFocusLost(state); + + if (FocusedDrawable != null) throw new InvalidOperationException($"Focus cannot be changed inside {nameof(OnFocusLost)}"); + } + + FocusedDrawable = potentialFocusTarget; + + Logger.Log($"Focus switched to {FocusedDrawable?.ToString() ?? "nothing"}.", LoggingTarget.Runtime, LogLevel.Debug); + + if (FocusedDrawable != null) + { + FocusedDrawable.HasFocus = true; + FocusedDrawable.TriggerOnFocus(state); + } + + return true; + } + + internal override bool BuildKeyboardInputQueue(List queue) => false; + + internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) => false; + + protected override void Update() + { + List distinctStates = createDistinctStates(GetPendingStates()).ToList(); + + //we need to make sure the code in the foreach below is run at least once even if we have no new pending states. + if (distinctStates.Count == 0) + distinctStates.Add(new InputState()); + + unfocusIfNoLongerValid(); + + foreach (InputState s in distinctStates) + HandleNewState(s); + + if (CurrentState.Mouse != null) + { + foreach (var d in positionalInputQueue) + if (d is IRequireHighFrequencyMousePosition) + if (d.TriggerOnMouseMove(CurrentState)) + break; + } + + keyboardRepeatTime -= Time.Elapsed; + + if (FocusedDrawable == null) + focusTopMostRequestingDrawable(); + + base.Update(); + } + + protected virtual void HandleNewState(InputState state) + { + bool hasNewKeyboard = state.Keyboard != null; + bool hasNewMouse = state.Mouse != null; + + var last = CurrentState; + + //avoid lingering references that would stay forever. + last.Last = null; + + CurrentState = state; + CurrentState.Last = last; + + if (CurrentState.Keyboard == null) CurrentState.Keyboard = last.Keyboard ?? new KeyboardState(); + if (CurrentState.Mouse == null) CurrentState.Mouse = last.Mouse ?? new MouseState(); + + TransformState(CurrentState); + + //move above? + updateInputQueues(CurrentState); + + // we only want to set a last state if both the new and old state are of the same type. + // this avoids giving the new state a false impression of being able to calculate delta values based on a last + // state potentially from a different input source. + if (last.Mouse != null && state.Mouse != null) + { + // only set the last state if one hasn't already been set (in addition to being the same type of state). + // a smarter InputHandler may do this internally, if they are handling input from multiple distinct devices. + if (state.Mouse.LastState == null && last.Mouse.GetType() == state.Mouse.GetType()) + state.Mouse.LastState = last.Mouse; + + if (last.Mouse.HasAnyButtonPressed) + state.Mouse.PositionMouseDown = last.Mouse.PositionMouseDown; + } + + //hover could change even when the mouse state has not. + updateHoverEvents(CurrentState); + + if (hasNewMouse) + updateMouseEvents(CurrentState); + + if (hasNewKeyboard || CurrentState.Keyboard.Keys.Any()) + updateKeyboardEvents(CurrentState); + } + + protected virtual List GetPendingStates() + { + var pendingStates = new List(); + + foreach (var h in InputHandlers) + { + if (h.IsActive && h.Enabled) + pendingStates.AddRange(h.GetPendingStates()); + else + h.GetPendingStates(); + } + + return pendingStates; + } + + protected virtual void TransformState(InputState inputState) + { + } + + private void updateInputQueues(InputState state) + { + inputQueue.Clear(); + positionalInputQueue.Clear(); + + if (state.Keyboard != null) + foreach (Drawable d in AliveInternalChildren) + d.BuildKeyboardInputQueue(inputQueue); + + if (state.Mouse != null) + foreach (Drawable d in AliveInternalChildren) + d.BuildMouseInputQueue(state.Mouse.Position, positionalInputQueue); + + // Keyboard and mouse queues were created in back-to-front order. + // We want input to first reach front-most drawables, so the queues + // need to be reversed. + inputQueue.Reverse(); + positionalInputQueue.Reverse(); + } + + protected virtual bool HandleHoverEvents => true; + + private void updateHoverEvents(InputState state) + { + Drawable lastHoverHandledDrawable = hoverHandledDrawable; + hoverHandledDrawable = null; + + lastHoveredDrawables.Clear(); + lastHoveredDrawables.AddRange(hoveredDrawables); + hoveredDrawables.Clear(); + + // New drawables shouldn't be hovered if the cursor isn't in the window + if (HandleHoverEvents) + { + // First, we need to construct hoveredDrawables for the current frame + foreach (Drawable d in positionalInputQueue) + { + hoveredDrawables.Add(d); + + // Don't need to re-hover those that are already hovered + if (d.IsHovered) + { + // Check if this drawable previously handled hover, and assume it would once more + if (d == lastHoverHandledDrawable) + { + hoverHandledDrawable = lastHoverHandledDrawable; + break; + } + + continue; + } + + d.IsHovered = true; + if (d.TriggerOnHover(state)) + { + hoverHandledDrawable = d; + break; + } + } + } + + // Unhover all previously hovered drawables which are no longer hovered. + foreach (Drawable d in lastHoveredDrawables.Except(hoveredDrawables)) + { + d.IsHovered = false; + d.TriggerOnHoverLost(state); + } + } + + private void updateKeyboardEvents(InputState state) + { + KeyboardState keyboard = (KeyboardState)state.Keyboard; + + if (!keyboard.Keys.Any()) + keyboardRepeatTime = 0; + + var last = state.Last?.Keyboard; + + if (last == null) return; + + foreach (var k in last.Keys) + { + if (!keyboard.Keys.Contains(k)) + handleKeyUp(state, k); + } + + foreach (Key k in keyboard.Keys.Distinct()) + { + bool isModifier = k == Key.LControl || k == Key.RControl + || k == Key.LAlt || k == Key.RAlt + || k == Key.LShift || k == Key.RShift + || k == Key.LWin || k == Key.RWin; + + LastActionTime = Time.Current; + + bool isRepetition = last.Keys.Contains(k); + + if (isModifier) + { + //modifiers shouldn't affect or report key repeat + if (!isRepetition) + handleKeyDown(state, k, false); + continue; + } + + if (isRepetition) + { + if (keyboardRepeatTime <= 0) + { + keyboardRepeatTime += repeat_tick_rate; + handleKeyDown(state, k, true); + } + } + else + { + keyboardRepeatTime = repeat_initial_delay; + handleKeyDown(state, k, false); + } + } + } + + private List mouseDownInputQueue; + + private void updateMouseEvents(InputState state) + { + MouseState mouse = (MouseState)state.Mouse; + + var last = state.Last?.Mouse as MouseState; + + if (last == null) return; + + if (mouse.Position != last.Position) + { + handleMouseMove(state); + if (isDragging) + handleMouseDrag(state); + } + + for (MouseButton b = 0; b < MouseButton.LastButton; b++) + { + var lastPressed = last.IsPressed(b); + + if (lastPressed != mouse.IsPressed(b)) + { + if (lastPressed) + handleMouseUp(state, b); + else + handleMouseDown(state, b); + } + } + + if (mouse.WheelDelta != 0 && Host.Window.CursorInWindow) + handleWheel(state); + + if (mouse.HasAnyButtonPressed) + { + if (!last.HasAnyButtonPressed) + { + //stuff which only happens once after the mousedown state + mouse.PositionMouseDown = state.Mouse.Position; + LastActionTime = Time.Current; + + if (mouse.IsPressed(MouseButton.Left)) + { + isValidClick = true; + + if (Time.Current - lastClickTime < double_click_time) + { + if (handleMouseDoubleClick(state)) + //when we handle a double-click we want to block a normal click from firing. + isValidClick = false; + + lastClickTime = 0; + } + else + lastClickTime = Time.Current; + } + } + + if (!isDragging && Vector2Extensions.Distance(mouse.PositionMouseDown ?? mouse.Position, mouse.Position) > click_drag_distance) + { + isDragging = true; + handleMouseDragStart(state); + } + } + else if (last.HasAnyButtonPressed) + { + if (isValidClick && (DraggedDrawable == null || Vector2Extensions.Distance(mouse.PositionMouseDown ?? mouse.Position, mouse.Position) <= click_drag_distance)) + handleMouseClick(state); + + mouseDownInputQueue = null; + mouse.PositionMouseDown = null; + isValidClick = false; + + if (isDragging) + { + isDragging = false; + handleMouseDragEnd(state); + } + } + } + + private bool handleMouseDown(InputState state, MouseButton button) + { + MouseDownEventArgs args = new MouseDownEventArgs + { + Button = button + }; + + var result = PropagateMouseDown(positionalInputQueue, state, args, out Drawable handledBy); + + mouseDownInputQueue = new List(result ? positionalInputQueue.Take(positionalInputQueue.IndexOf(handledBy) + 1) : positionalInputQueue); + + return result; + } + + private bool handleMouseUp(InputState state, MouseButton button) + { + if (mouseDownInputQueue == null) return false; + + MouseUpEventArgs args = new MouseUpEventArgs + { + Button = button + }; + + //extra check for IsAlive because we are using an outdated queue. + return PropagateMouseUp(mouseDownInputQueue.Where(target => target.IsAlive && target.IsPresent), state, args); + } + + /// + /// Triggers mouse up events on drawables in until it is handled. + /// + /// The drawables in the queue. + /// The input state. + /// The args. + /// Whether the mouse up event was handled. + protected virtual bool PropagateMouseUp(IEnumerable drawables, InputState state, MouseUpEventArgs args) + { + var handledBy = drawables.FirstOrDefault(target => target.TriggerOnMouseUp(state, args)); + + if (handledBy != null) + Logger.Log($"MouseUp ({args.Button}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handledBy != null; + } + + /// + /// Triggers mouse down events on drawables in until it is handled. + /// + /// The drawables in the queue. + /// The input state. + /// The args. + /// + /// Whether the mouse down event was handled. + protected virtual bool PropagateMouseDown(IEnumerable drawables, InputState state, MouseDownEventArgs args, out Drawable handledBy) + { + handledBy = drawables.FirstOrDefault(target => target.TriggerOnMouseDown(state, args)); + + if (handledBy != null) + Logger.Log($"MouseDown ({args.Button}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handledBy != null; + } + + private bool handleMouseMove(InputState state) + { + return positionalInputQueue.Any(target => target.TriggerOnMouseMove(state)); + } + + private Drawable clickedDrawable; + + private bool handleMouseClick(InputState state) + { + var intersectingQueue = positionalInputQueue.Intersect(mouseDownInputQueue); + + Drawable focusTarget = null; + + // click pass, triggering an OnClick on all drawables up to the first which returns true. + // an extra IsHovered check is performed because we are using an outdated queue (for valid reasons which we need to document). + clickedDrawable = intersectingQueue.FirstOrDefault(t => t.CanReceiveMouseInput && t.ReceiveMouseInputAt(state.Mouse.Position) && t.TriggerOnClick(state)); + + if (clickedDrawable != null) + { + focusTarget = clickedDrawable; + + if (!focusTarget.AcceptsFocus) + { + // search upwards from the clicked drawable until we find something to handle focus. + Drawable previousFocused = FocusedDrawable; + + while (focusTarget?.AcceptsFocus == false) + focusTarget = focusTarget.Parent; + + if (focusTarget != null && previousFocused != null) + { + // we found a focusable target above us. + // now search upwards from previousFocused to check whether focusTarget is a common parent. + Drawable search = previousFocused; + while (search != null && search != focusTarget) + search = search.Parent; + + if (focusTarget == search) + // we have a common parent, so let's keep focus on the previously focused target. + focusTarget = previousFocused; + } + } + } + + ChangeFocus(focusTarget, state); + + if (clickedDrawable != null) + Logger.Log($"MouseClick handled by {clickedDrawable}.", LoggingTarget.Runtime, LogLevel.Debug); + + return clickedDrawable != null; + } + + private bool handleMouseDoubleClick(InputState state) + { + if (clickedDrawable == null) return false; + + return clickedDrawable.ReceiveMouseInputAt(state.Mouse.Position) && clickedDrawable.TriggerOnDoubleClick(state); + } + + private bool handleMouseDrag(InputState state) + { + //Once a drawable is dragged, it remains in a dragged state until the drag is finished. + return DraggedDrawable?.TriggerOnDrag(state) ?? false; + } + + private bool handleMouseDragStart(InputState state) + { + Trace.Assert(DraggedDrawable == null, "The draggingDrawable was not set to null by handleMouseDragEnd."); + DraggedDrawable = mouseDownInputQueue?.FirstOrDefault(target => target.IsAlive && target.IsPresent && target.TriggerOnDragStart(state)); + if (DraggedDrawable != null) + { + DraggedDrawable.IsDragged = true; + Logger.Log($"MouseDragStart handled by {DraggedDrawable}.", LoggingTarget.Runtime, LogLevel.Debug); + } + + return DraggedDrawable != null; + } + + private bool handleMouseDragEnd(InputState state) + { + if (DraggedDrawable == null) + return false; + + bool result = DraggedDrawable.TriggerOnDragEnd(state); + DraggedDrawable.IsDragged = false; + DraggedDrawable = null; + + return result; + } + + private bool handleWheel(InputState state) + { + return PropagateWheel(positionalInputQueue, state); + } + + /// + /// Triggers wheel events on drawables in until it is handled. + /// + /// The drawables in the queue. + /// The input state. + /// + protected virtual bool PropagateWheel(IEnumerable drawables, InputState state) + { + var handledBy = drawables.FirstOrDefault(target => target.TriggerOnWheel(state)); + + if (handledBy != null) + Logger.Log($"Wheel ({state.Mouse.WheelDelta}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handledBy != null; + } + + private bool handleKeyDown(InputState state, Key key, bool repeat) + { + IEnumerable queue = inputQueue; + if (!unfocusIfNoLongerValid()) + queue = queue.Prepend(FocusedDrawable); + + return PropagateKeyDown(queue, state, new KeyDownEventArgs { Key = key, Repeat = repeat }); + } + + /// + /// Triggers key down events on drawables in until it is handled. + /// + /// The drawables in the queue. + /// The input state. + /// The args. + /// Whether the key down event was handled. + protected virtual bool PropagateKeyDown(IEnumerable drawables, InputState state, KeyDownEventArgs args) + { + var handledBy = drawables.FirstOrDefault(target => target.TriggerOnKeyDown(state, args)); + + if (handledBy != null) + Logger.Log($"KeyDown ({args.Key}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handledBy != null; + } + + private bool handleKeyUp(InputState state, Key key) + { + IEnumerable queue = inputQueue; + if (!unfocusIfNoLongerValid()) + queue = queue.Prepend(FocusedDrawable); + + return PropagateKeyUp(queue, state, new KeyUpEventArgs { Key = key }); + } + + /// + /// Triggers key up events on drawables in until it is handled. + /// + /// The drawables in the queue. + /// The input state. + /// The args. + /// Whether the key up event was handled. + protected virtual bool PropagateKeyUp(IEnumerable drawables, InputState state, KeyUpEventArgs args) + { + var handledBy = drawables.FirstOrDefault(target => target.TriggerOnKeyUp(state, args)); + + if (handledBy != null) + Logger.Log($"KeyUp ({args.Key}) handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug); + + return handledBy != null; + } + + /// + /// Unfocus the current focused drawable if it is no longer in a valid state. + /// + /// true if there is no longer a focus. + private bool unfocusIfNoLongerValid() + { + if (FocusedDrawable == null) return true; + + bool stillValid = FocusedDrawable.IsPresent && FocusedDrawable.Parent != null; + + if (stillValid) + { + //ensure we are visible + CompositeDrawable d = FocusedDrawable.Parent; + while (d != null) + { + if (!d.IsPresent) + { + stillValid = false; + break; + } + + d = d.Parent; + } + } + + if (stillValid) + return false; + + ChangeFocus(null); + return true; + } + + private void focusTopMostRequestingDrawable() => ChangeFocus(inputQueue.FirstOrDefault(target => target.RequestsFocus)); + + /// + /// In order to provide a reliable event system to drawables, we want to ensure that we reprocess input queues (via the + /// main loop in after each and every button or key change. This allows + /// correct behaviour in a case where the input queues change based on triggered by a button or key. + /// + /// One ore more states which are to be converted to distinct states. + /// Processed states such that at most one attribute change occurs between any two consecutive states. + private IEnumerable createDistinctStates(IEnumerable newStates) + { + IKeyboardState lastKeyboard = CurrentState.Keyboard; + IMouseState lastMouse = CurrentState.Mouse; + + foreach (var state in newStates) + { + if (state.Mouse == null && state.Keyboard == null) + { + // we still want to return at least one state change. + yield return state; + } + + if (state.Mouse != null) + { + // first we want to create a copy of ourselves without any button changes + // this is done only for mouse handlers, as they have positional data we want to handle in a separate pass. + var iWithoutButtons = state.Mouse.Clone(); + + for (MouseButton b = 0; b < MouseButton.LastButton; b++) + iWithoutButtons.SetPressed(b, lastMouse?.IsPressed(b) ?? false); + + //we start by adding this state to the processed list... + var newState = state.Clone(); + newState.Mouse = lastMouse = iWithoutButtons; + yield return newState; + + //and then iterate over each button/key change, adding intermediate states along the way. + for (MouseButton b = 0; b < MouseButton.LastButton; b++) + { + if (state.Mouse.IsPressed(b) != (lastMouse?.IsPressed(b) ?? false)) + { + lastMouse = lastMouse?.Clone() ?? new MouseState(); + + //add our single local change + lastMouse.SetPressed(b, state.Mouse.IsPressed(b)); + + newState = state.Clone(); + newState.Mouse = lastMouse; + yield return newState; + } + } + } + + if (state.Keyboard != null) + { + if (lastKeyboard != null) + foreach (var releasedKey in lastKeyboard.Keys.Except(state.Keyboard.Keys)) + { + var newState = state.Clone(); + newState.Keyboard = lastKeyboard = new KeyboardState { Keys = lastKeyboard.Keys.Where(d => d != releasedKey).ToArray() }; + yield return newState; + } + + foreach (var pressedKey in state.Keyboard.Keys.Except(lastKeyboard?.Keys ?? new Key[] { })) + { + var newState = state.Clone(); + newState.Keyboard = lastKeyboard = new KeyboardState { Keys = lastKeyboard?.Keys.Union(new[] { pressedKey }) ?? new[] { pressedKey } }; + yield return newState; + } + } + } + } + } + + public enum ConfineMouseMode + { + Never, + Fullscreen, + Always + } +} diff --git a/osu.Framework/Input/InputResampler.cs b/osu.Framework/Input/InputResampler.cs index 6b1fae433..22a701603 100644 --- a/osu.Framework/Input/InputResampler.cs +++ b/osu.Framework/Input/InputResampler.cs @@ -1,79 +1,79 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; - -namespace osu.Framework.Input -{ - /// - /// Reduces cursor input to relevant nodes and corners that noticably affect the cursor path. - /// If the input is a raw/HD input this won't omit any input nodes. - /// Set SmoothRawInput to true to keep behaviour for HD inputs. - /// - public class InputResampler - { - private Vector2? lastRelevantPosition; - - private Vector2? lastActualPosition; - - private bool isRawInput; - - /// - /// true if AddPosition should treat raw input (input with a decimal fraction) the same - /// as normal input. If false, AddPosition will always just return the position argument - /// passed to the function without modification. - /// - public bool ResampleRawInput { get; set; } - - /// - /// Function that takes in a and returns a list of positions - /// that can be used by the caller to make the input path smoother or reduce it. - /// The current implementation always returns only none or exactly one vector which - /// reduces the input to the corner nodes. - /// - public IEnumerable AddPosition(Vector2 position) - { - if (!ResampleRawInput) - { - if (isRawInput) - { - lastRelevantPosition = position; - lastActualPosition = position; - return new[] { position }; - } - - // HD if it has fractions - if (position.X - (float)Math.Truncate(position.X) != 0) - isRawInput = true; - } - - if (lastRelevantPosition == null || lastActualPosition == null) - { - lastRelevantPosition = position; - lastActualPosition = position; - return new[] { position }; - } - - Vector2 diff = position - lastRelevantPosition.Value; - float distance = diff.Length; - Vector2 direction = diff / distance; - - Vector2 realDiff = position - lastActualPosition.Value; - float realMovementDistance = realDiff.Length; - if (realMovementDistance < 1) - return Array.Empty(); - lastActualPosition = position; - - // don't update when it moved less than 10 pixels from the last position in a straight fashion - // but never update when its less than 2 pixels - if (distance < 10 && Vector2.Dot(direction, realDiff / realMovementDistance) > 0.7 || distance < 2) - return Array.Empty(); - - lastRelevantPosition = position; - - return new[] { position }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using OpenTK; + +namespace osu.Framework.Input +{ + /// + /// Reduces cursor input to relevant nodes and corners that noticably affect the cursor path. + /// If the input is a raw/HD input this won't omit any input nodes. + /// Set SmoothRawInput to true to keep behaviour for HD inputs. + /// + public class InputResampler + { + private Vector2? lastRelevantPosition; + + private Vector2? lastActualPosition; + + private bool isRawInput; + + /// + /// true if AddPosition should treat raw input (input with a decimal fraction) the same + /// as normal input. If false, AddPosition will always just return the position argument + /// passed to the function without modification. + /// + public bool ResampleRawInput { get; set; } + + /// + /// Function that takes in a and returns a list of positions + /// that can be used by the caller to make the input path smoother or reduce it. + /// The current implementation always returns only none or exactly one vector which + /// reduces the input to the corner nodes. + /// + public IEnumerable AddPosition(Vector2 position) + { + if (!ResampleRawInput) + { + if (isRawInput) + { + lastRelevantPosition = position; + lastActualPosition = position; + return new[] { position }; + } + + // HD if it has fractions + if (position.X - (float)Math.Truncate(position.X) != 0) + isRawInput = true; + } + + if (lastRelevantPosition == null || lastActualPosition == null) + { + lastRelevantPosition = position; + lastActualPosition = position; + return new[] { position }; + } + + Vector2 diff = position - lastRelevantPosition.Value; + float distance = diff.Length; + Vector2 direction = diff / distance; + + Vector2 realDiff = position - lastActualPosition.Value; + float realMovementDistance = realDiff.Length; + if (realMovementDistance < 1) + return Array.Empty(); + lastActualPosition = position; + + // don't update when it moved less than 10 pixels from the last position in a straight fashion + // but never update when its less than 2 pixels + if (distance < 10 && Vector2.Dot(direction, realDiff / realMovementDistance) > 0.7 || distance < 2) + return Array.Empty(); + + lastRelevantPosition = position; + + return new[] { position }; + } + } +} diff --git a/osu.Framework/Input/InputState.cs b/osu.Framework/Input/InputState.cs index e38b11b09..9423891dc 100644 --- a/osu.Framework/Input/InputState.cs +++ b/osu.Framework/Input/InputState.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Input -{ - public class InputState : EventArgs - { - public IKeyboardState Keyboard; - public IMouseState Mouse; - public InputState Last; - - public virtual InputState Clone() - { - var clone = (InputState)MemberwiseClone(); - clone.Keyboard = Keyboard?.Clone(); - clone.Mouse = Mouse?.Clone(); - clone.Last = Last; - - return clone; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Input +{ + public class InputState : EventArgs + { + public IKeyboardState Keyboard; + public IMouseState Mouse; + public InputState Last; + + public virtual InputState Clone() + { + var clone = (InputState)MemberwiseClone(); + clone.Keyboard = Keyboard?.Clone(); + clone.Mouse = Mouse?.Clone(); + clone.Last = Last; + + return clone; + } + } +} diff --git a/osu.Framework/Input/KeyDownEventArgs.cs b/osu.Framework/Input/KeyDownEventArgs.cs index 2ea596542..65f17951b 100644 --- a/osu.Framework/Input/KeyDownEventArgs.cs +++ b/osu.Framework/Input/KeyDownEventArgs.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public class KeyDownEventArgs : EventArgs - { - public Key Key; - public bool Repeat; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public class KeyDownEventArgs : EventArgs + { + public Key Key; + public bool Repeat; + } +} diff --git a/osu.Framework/Input/KeyUpEventArgs.cs b/osu.Framework/Input/KeyUpEventArgs.cs index 17adaed0d..c8f712026 100644 --- a/osu.Framework/Input/KeyUpEventArgs.cs +++ b/osu.Framework/Input/KeyUpEventArgs.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Input; -using System; - -namespace osu.Framework.Input -{ - public class KeyUpEventArgs : EventArgs - { - public Key Key; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Input; +using System; + +namespace osu.Framework.Input +{ + public class KeyUpEventArgs : EventArgs + { + public Key Key; + } +} diff --git a/osu.Framework/Input/KeyboardState.cs b/osu.Framework/Input/KeyboardState.cs index 21a69c1e2..c179852d7 100644 --- a/osu.Framework/Input/KeyboardState.cs +++ b/osu.Framework/Input/KeyboardState.cs @@ -1,30 +1,30 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public class KeyboardState : IKeyboardState - { - public IEnumerable Keys { get; set; } = new Key[] { }; - - public bool ControlPressed => Keys.Any(k => k == Key.LControl || k == Key.RControl); - public bool AltPressed => Keys.Any(k => k == Key.LAlt || k == Key.RAlt); - public bool ShiftPressed => Keys.Any(k => k == Key.LShift || k == Key.RShift); - - /// - /// Win key on Windows, or Command key on Mac. - /// - public bool SuperPressed => Keys.Any(k => k == Key.LWin || k == Key.RWin); - - public IKeyboardState Clone() - { - var clone = (KeyboardState)MemberwiseClone(); - clone.Keys = new List(Keys); - return clone; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public class KeyboardState : IKeyboardState + { + public IEnumerable Keys { get; set; } = new Key[] { }; + + public bool ControlPressed => Keys.Any(k => k == Key.LControl || k == Key.RControl); + public bool AltPressed => Keys.Any(k => k == Key.LAlt || k == Key.RAlt); + public bool ShiftPressed => Keys.Any(k => k == Key.LShift || k == Key.RShift); + + /// + /// Win key on Windows, or Command key on Mac. + /// + public bool SuperPressed => Keys.Any(k => k == Key.LWin || k == Key.RWin); + + public IKeyboardState Clone() + { + var clone = (KeyboardState)MemberwiseClone(); + clone.Keys = new List(Keys); + return clone; + } + } +} diff --git a/osu.Framework/Input/MouseDownEventArgs.cs b/osu.Framework/Input/MouseDownEventArgs.cs index b52cc9aca..9eb24c278 100644 --- a/osu.Framework/Input/MouseDownEventArgs.cs +++ b/osu.Framework/Input/MouseDownEventArgs.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Input -{ - public class MouseDownEventArgs : MouseEventArgs - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Input +{ + public class MouseDownEventArgs : MouseEventArgs + { + } +} diff --git a/osu.Framework/Input/MouseEventArgs.cs b/osu.Framework/Input/MouseEventArgs.cs index fe5f5bfe3..81f3ec2a3 100644 --- a/osu.Framework/Input/MouseEventArgs.cs +++ b/osu.Framework/Input/MouseEventArgs.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK.Input; - -namespace osu.Framework.Input -{ - public class MouseEventArgs : EventArgs - { - public MouseButton Button; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK.Input; + +namespace osu.Framework.Input +{ + public class MouseEventArgs : EventArgs + { + public MouseButton Button; + } +} diff --git a/osu.Framework/Input/MouseState.cs b/osu.Framework/Input/MouseState.cs index 1d066a4aa..cab4c1821 100644 --- a/osu.Framework/Input/MouseState.cs +++ b/osu.Framework/Input/MouseState.cs @@ -1,76 +1,76 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using OpenTK; -using OpenTK.Input; -using System.Linq; - -namespace osu.Framework.Input -{ - public class MouseState : IMouseState - { - private IMouseState lastState; - - public IReadOnlyList Buttons - { - get { return buttons; } - set - { - buttons.Clear(); - buttons.AddRange(value); - } - } - - private List buttons { get; set; } = new List(); - - public IMouseState NativeState => this; - - public IMouseState LastState - { - get { return lastState; } - set - { - lastState = value; - if (lastState != null) lastState.LastState = null; - } - } - - public virtual int WheelDelta => Wheel - LastState?.Wheel ?? 0; - - public int Wheel { get; set; } - - public bool HasMainButtonPressed => IsPressed(MouseButton.Left) || IsPressed(MouseButton.Right); - - public bool HasAnyButtonPressed => buttons.Any(); - - public Vector2 Delta => Position - LastPosition; - - public Vector2 Position { get; set; } - - public Vector2 LastPosition => LastState?.Position ?? Position; - - public Vector2? PositionMouseDown { get; set; } - - public IMouseState Clone() - { - var clone = (MouseState)MemberwiseClone(); - clone.buttons = new List(buttons); - clone.LastState = LastState; - return clone; - } - - public bool IsPressed(MouseButton button) => buttons.Contains(button); - - public void SetPressed(MouseButton button, bool pressed) - { - if (buttons.Contains(button) == pressed) - return; - - if (pressed) - buttons.Add(button); - else - buttons.Remove(button); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using OpenTK; +using OpenTK.Input; +using System.Linq; + +namespace osu.Framework.Input +{ + public class MouseState : IMouseState + { + private IMouseState lastState; + + public IReadOnlyList Buttons + { + get { return buttons; } + set + { + buttons.Clear(); + buttons.AddRange(value); + } + } + + private List buttons { get; set; } = new List(); + + public IMouseState NativeState => this; + + public IMouseState LastState + { + get { return lastState; } + set + { + lastState = value; + if (lastState != null) lastState.LastState = null; + } + } + + public virtual int WheelDelta => Wheel - LastState?.Wheel ?? 0; + + public int Wheel { get; set; } + + public bool HasMainButtonPressed => IsPressed(MouseButton.Left) || IsPressed(MouseButton.Right); + + public bool HasAnyButtonPressed => buttons.Any(); + + public Vector2 Delta => Position - LastPosition; + + public Vector2 Position { get; set; } + + public Vector2 LastPosition => LastState?.Position ?? Position; + + public Vector2? PositionMouseDown { get; set; } + + public IMouseState Clone() + { + var clone = (MouseState)MemberwiseClone(); + clone.buttons = new List(buttons); + clone.LastState = LastState; + return clone; + } + + public bool IsPressed(MouseButton button) => buttons.Contains(button); + + public void SetPressed(MouseButton button, bool pressed) + { + if (buttons.Contains(button) == pressed) + return; + + if (pressed) + buttons.Add(button); + else + buttons.Remove(button); + } + } +} diff --git a/osu.Framework/Input/MouseUpEventArgs.cs b/osu.Framework/Input/MouseUpEventArgs.cs index 96f8f18ab..75ddf4516 100644 --- a/osu.Framework/Input/MouseUpEventArgs.cs +++ b/osu.Framework/Input/MouseUpEventArgs.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Input -{ - public class MouseUpEventArgs : MouseEventArgs - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Input +{ + public class MouseUpEventArgs : MouseEventArgs + { + } +} diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 3bb9ff9a6..cb2dd94f3 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics; -using OpenTK; - -namespace osu.Framework.Input -{ - public class PassThroughInputManager : CustomInputManager - { - /// - /// If there's an InputManager above us, decide whether we should use their available state. - /// - public bool UseParentState = true; - - internal override bool BuildKeyboardInputQueue(List queue) - { - if (!CanReceiveKeyboardInput) return false; - - if (UseParentState) - queue.Add(this); - return false; - } - - internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) - { - if (!CanReceiveMouseInput) return false; - - if (UseParentState) - queue.Add(this); - return false; - } - - protected override List GetPendingStates() - { - //we still want to call the base method to clear any pending states that may build up. - var pendingStates = base.GetPendingStates(); - - if (!UseParentState) - return pendingStates; - - pendingStates.Clear(); - - foreach (var s in pendingParentStates) - pendingStates.Add(new PassThroughInputState(s)); - - pendingParentStates.Clear(); - - return pendingStates; - } - - private readonly List pendingParentStates = new List(); - - private bool acceptState(InputState state) - { - if (UseParentState) - pendingParentStates.Add(state); - return false; - } - - protected override bool OnMouseMove(InputState state) => acceptState(state); - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => acceptState(state); - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => acceptState(state); - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => acceptState(state); - - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => acceptState(state); - - /// - /// An input state which allows for transformations to state which don't affect the source state. - /// - public class PassThroughInputState : InputState - { - public PassThroughInputState(InputState state) - { - Mouse = (state.Mouse.NativeState as MouseState)?.Clone(); - Keyboard = (state.Keyboard as KeyboardState)?.Clone(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics; +using OpenTK; + +namespace osu.Framework.Input +{ + public class PassThroughInputManager : CustomInputManager + { + /// + /// If there's an InputManager above us, decide whether we should use their available state. + /// + public bool UseParentState = true; + + internal override bool BuildKeyboardInputQueue(List queue) + { + if (!CanReceiveKeyboardInput) return false; + + if (UseParentState) + queue.Add(this); + return false; + } + + internal override bool BuildMouseInputQueue(Vector2 screenSpaceMousePos, List queue) + { + if (!CanReceiveMouseInput) return false; + + if (UseParentState) + queue.Add(this); + return false; + } + + protected override List GetPendingStates() + { + //we still want to call the base method to clear any pending states that may build up. + var pendingStates = base.GetPendingStates(); + + if (!UseParentState) + return pendingStates; + + pendingStates.Clear(); + + foreach (var s in pendingParentStates) + pendingStates.Add(new PassThroughInputState(s)); + + pendingParentStates.Clear(); + + return pendingStates; + } + + private readonly List pendingParentStates = new List(); + + private bool acceptState(InputState state) + { + if (UseParentState) + pendingParentStates.Add(state); + return false; + } + + protected override bool OnMouseMove(InputState state) => acceptState(state); + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => acceptState(state); + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => acceptState(state); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => acceptState(state); + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => acceptState(state); + + /// + /// An input state which allows for transformations to state which don't affect the source state. + /// + public class PassThroughInputState : InputState + { + public PassThroughInputState(InputState state) + { + Mouse = (state.Mouse.NativeState as MouseState)?.Clone(); + Keyboard = (state.Keyboard as KeyboardState)?.Clone(); + } + } + } +} diff --git a/osu.Framework/Input/PlatformActionContainer.cs b/osu.Framework/Input/PlatformActionContainer.cs index af904d4d8..ca61252c2 100644 --- a/osu.Framework/Input/PlatformActionContainer.cs +++ b/osu.Framework/Input/PlatformActionContainer.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Input.Bindings; -using osu.Framework.Platform; - -namespace osu.Framework.Input -{ - /// - /// Provides actions that are expected to have different key bindings per platform. - /// The framework will always contain one top-level instance of this class, but extra instances - /// can be created to handle events that should trigger specifically on a focused drawable. - /// Will send repeat events by default. - /// - public class PlatformActionContainer : KeyBindingContainer, IHandleGlobalInput - { - private GameHost host; - - [BackgroundDependencyLoader] - private void load(GameHost host) - { - this.host = host; - } - - public override IEnumerable DefaultKeyBindings => host.PlatformKeyBindings; - - protected override bool Prioritised => true; - - protected override bool SendRepeats => true; - } - - public struct PlatformAction - { - public PlatformActionType ActionType; - public PlatformActionMethod? ActionMethod; - - public PlatformAction(PlatformActionType actionType, PlatformActionMethod? actionMethod = null) - { - ActionType = actionType; - ActionMethod = actionMethod; - } - } - - public enum PlatformActionType - { - Cut, - Copy, - Paste, - SelectAll, - CharPrevious, - CharNext, - WordPrevious, - WordNext, - LineStart, - LineEnd - } - - public enum PlatformActionMethod - { - Move, - Select, - Delete - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; + +namespace osu.Framework.Input +{ + /// + /// Provides actions that are expected to have different key bindings per platform. + /// The framework will always contain one top-level instance of this class, but extra instances + /// can be created to handle events that should trigger specifically on a focused drawable. + /// Will send repeat events by default. + /// + public class PlatformActionContainer : KeyBindingContainer, IHandleGlobalInput + { + private GameHost host; + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + this.host = host; + } + + public override IEnumerable DefaultKeyBindings => host.PlatformKeyBindings; + + protected override bool Prioritised => true; + + protected override bool SendRepeats => true; + } + + public struct PlatformAction + { + public PlatformActionType ActionType; + public PlatformActionMethod? ActionMethod; + + public PlatformAction(PlatformActionType actionType, PlatformActionMethod? actionMethod = null) + { + ActionType = actionType; + ActionMethod = actionMethod; + } + } + + public enum PlatformActionType + { + Cut, + Copy, + Paste, + SelectAll, + CharPrevious, + CharNext, + WordPrevious, + WordNext, + LineStart, + LineEnd + } + + public enum PlatformActionMethod + { + Move, + Select, + Delete + } +} diff --git a/osu.Framework/Input/UserInputManager.cs b/osu.Framework/Input/UserInputManager.cs index 8d1a2755f..46b52c540 100644 --- a/osu.Framework/Input/UserInputManager.cs +++ b/osu.Framework/Input/UserInputManager.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Input.Handlers; - -namespace osu.Framework.Input -{ - public class UserInputManager : PassThroughInputManager - { - protected override IEnumerable InputHandlers => Host.AvailableInputHandlers; - - protected override bool HandleHoverEvents => Host.Window?.CursorInWindow ?? true; - - public UserInputManager() - { - UseParentState = false; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input.Handlers; + +namespace osu.Framework.Input +{ + public class UserInputManager : PassThroughInputManager + { + protected override IEnumerable InputHandlers => Host.AvailableInputHandlers; + + protected override bool HandleHoverEvents => Host.Window?.CursorInWindow ?? true; + + public UserInputManager() + { + UseParentState = false; + } + } +} diff --git a/osu.Framework/Lists/LazyList.cs b/osu.Framework/Lists/LazyList.cs index 0f138106f..8c372f4dd 100644 --- a/osu.Framework/Lists/LazyList.cs +++ b/osu.Framework/Lists/LazyList.cs @@ -1,47 +1,47 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Framework.Lists -{ - /// - /// A list that lazily applies a transformation to elements of a source list to a target type when its indexed or iterated. - /// - /// The type of the source elements. - /// The type of the target elements. - public class LazyList : IReadOnlyList - { - private readonly IReadOnlyList source; - private readonly Func map; - - /// - /// Gets the element at the specified index from source, applies the transformation to it and returns the transformed element. - /// - /// The index of the element. - /// The transformed element at the specified index. - public TTarget this[int index] => map(source[index]); - - /// - /// The number of elements in this lazy list. - /// - public int Count => source.Count; - - /// - /// Constructs a new lazy list from the given source list and with the given transformation. - /// - /// The source list to get elements from. - /// - public LazyList(IReadOnlyList source, Func map) - { - this.source = source; - this.map = map; - } - - IEnumerator IEnumerable.GetEnumerator() => source.Select(s => map(s)).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Lists +{ + /// + /// A list that lazily applies a transformation to elements of a source list to a target type when its indexed or iterated. + /// + /// The type of the source elements. + /// The type of the target elements. + public class LazyList : IReadOnlyList + { + private readonly IReadOnlyList source; + private readonly Func map; + + /// + /// Gets the element at the specified index from source, applies the transformation to it and returns the transformed element. + /// + /// The index of the element. + /// The transformed element at the specified index. + public TTarget this[int index] => map(source[index]); + + /// + /// The number of elements in this lazy list. + /// + public int Count => source.Count; + + /// + /// Constructs a new lazy list from the given source list and with the given transformation. + /// + /// The source list to get elements from. + /// + public LazyList(IReadOnlyList source, Func map) + { + this.source = source; + this.map = map; + } + + IEnumerator IEnumerable.GetEnumerator() => source.Select(s => map(s)).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + } +} diff --git a/osu.Framework/Lists/SortedList.cs b/osu.Framework/Lists/SortedList.cs index 60d6244f7..047c9ffb3 100644 --- a/osu.Framework/Lists/SortedList.cs +++ b/osu.Framework/Lists/SortedList.cs @@ -1,177 +1,177 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions.TypeExtensions; -using System; -using System.Collections; -using System.Collections.Generic; -using Newtonsoft.Json; -using osu.Framework.IO.Serialization; - -namespace osu.Framework.Lists -{ - public class SortedList : ICollection, IReadOnlyList, ISortedList - { - private readonly List list; - - public IComparer Comparer { get; } - - public int Count => list.Count; - - bool ICollection.IsReadOnly => ((ICollection)list).IsReadOnly; - - public T this[int index] - { - get { return list[index]; } - set { list[index] = value; } - } - - /// - /// Constructs a new with the default comparer. - /// - public SortedList() - : this(Comparer.Default) - { - } - - /// - /// Constructs a new with a custom comparison function. - /// - /// The comparison function. - public SortedList(Func comparer) - : this(new ComparisonComparer(comparer)) - { - } - - /// - /// Constructs a new with a custom . - /// - /// The comparer to use. - public SortedList(IComparer comparer) - { - list = new List(); - Comparer = comparer; - } - - public void AddRange(IEnumerable collection) - { - foreach (var i in collection) - Add(i); - } - - public virtual void RemoveRange(int index, int count) => list.RemoveRange(index, count); - - public virtual int Add(T value) => addInternal(value); - - /// - /// Adds the specified item internally without the interference of a possible derived class. - /// - /// The item to add. - /// The index of the item within this list. - private int addInternal(T value) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - int index = list.BinarySearch(value, Comparer); - if (index < 0) - index = ~index; - - list.Insert(index, value); - - return index; - } - - public bool Remove(T item) - { - int index = IndexOf(item); - if (index < 0) - return false; - RemoveAt(index); - return true; - } - - public virtual void RemoveAt(int index) => list.RemoveAt(index); - - public int RemoveAll(Predicate match) - { - List found = (List)FindAll(match); - - foreach (var i in found) - Remove(i); - - return found.Count; - } - - public virtual void Clear() => list.Clear(); - - public bool Contains(T item) => IndexOf(item) >= 0; - - public int BinarySearch(T value) => list.BinarySearch(value, Comparer); - - public int IndexOf(T value) - { - int index = list.BinarySearch(value, Comparer); - return index >= 0 && list[index].Equals(value) ? index : -1; - } - - public void CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); - - public T Find(Predicate match) => list.Find(match); - - public IEnumerable FindAll(Predicate match) => list.FindAll(match); - - public T FindLast(Predicate match) => list.FindLast(match); - - public int FindIndex(Predicate match) => list.FindIndex(match); - - public override string ToString() => $@"{GetType().ReadableName()} ({Count} items)"; - - #region ICollection Implementation - - void ICollection.Add(T item) => Add(item); - - public IEnumerator GetEnumerator() => list.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); - - public void SerializeTo(JsonWriter writer, JsonSerializer serializer) - { - serializer.Serialize(writer, list); - } - - public void DeserializeFrom(JsonReader reader, JsonSerializer serializer) - { - serializer.Populate(reader, list); - list.Sort(Comparer); - } - - #endregion - - private class ComparisonComparer : IComparer - { - private readonly Comparison comparison; - - public ComparisonComparer(Func compare) - { - if (compare == null) - { - throw new ArgumentNullException(nameof(compare)); - } - comparison = new Comparison(compare); - } - - public int Compare(TComparison x, TComparison y) - { - return comparison(x, y); - } - } - } - - [JsonConverter(typeof(SortedListConverter))] - internal interface ISortedList - { - void SerializeTo(JsonWriter writer, JsonSerializer serializer); - void DeserializeFrom(JsonReader reader, JsonSerializer serializer); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions.TypeExtensions; +using System; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Framework.IO.Serialization; + +namespace osu.Framework.Lists +{ + public class SortedList : ICollection, IReadOnlyList, ISortedList + { + private readonly List list; + + public IComparer Comparer { get; } + + public int Count => list.Count; + + bool ICollection.IsReadOnly => ((ICollection)list).IsReadOnly; + + public T this[int index] + { + get { return list[index]; } + set { list[index] = value; } + } + + /// + /// Constructs a new with the default comparer. + /// + public SortedList() + : this(Comparer.Default) + { + } + + /// + /// Constructs a new with a custom comparison function. + /// + /// The comparison function. + public SortedList(Func comparer) + : this(new ComparisonComparer(comparer)) + { + } + + /// + /// Constructs a new with a custom . + /// + /// The comparer to use. + public SortedList(IComparer comparer) + { + list = new List(); + Comparer = comparer; + } + + public void AddRange(IEnumerable collection) + { + foreach (var i in collection) + Add(i); + } + + public virtual void RemoveRange(int index, int count) => list.RemoveRange(index, count); + + public virtual int Add(T value) => addInternal(value); + + /// + /// Adds the specified item internally without the interference of a possible derived class. + /// + /// The item to add. + /// The index of the item within this list. + private int addInternal(T value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + int index = list.BinarySearch(value, Comparer); + if (index < 0) + index = ~index; + + list.Insert(index, value); + + return index; + } + + public bool Remove(T item) + { + int index = IndexOf(item); + if (index < 0) + return false; + RemoveAt(index); + return true; + } + + public virtual void RemoveAt(int index) => list.RemoveAt(index); + + public int RemoveAll(Predicate match) + { + List found = (List)FindAll(match); + + foreach (var i in found) + Remove(i); + + return found.Count; + } + + public virtual void Clear() => list.Clear(); + + public bool Contains(T item) => IndexOf(item) >= 0; + + public int BinarySearch(T value) => list.BinarySearch(value, Comparer); + + public int IndexOf(T value) + { + int index = list.BinarySearch(value, Comparer); + return index >= 0 && list[index].Equals(value) ? index : -1; + } + + public void CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); + + public T Find(Predicate match) => list.Find(match); + + public IEnumerable FindAll(Predicate match) => list.FindAll(match); + + public T FindLast(Predicate match) => list.FindLast(match); + + public int FindIndex(Predicate match) => list.FindIndex(match); + + public override string ToString() => $@"{GetType().ReadableName()} ({Count} items)"; + + #region ICollection Implementation + + void ICollection.Add(T item) => Add(item); + + public IEnumerator GetEnumerator() => list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); + + public void SerializeTo(JsonWriter writer, JsonSerializer serializer) + { + serializer.Serialize(writer, list); + } + + public void DeserializeFrom(JsonReader reader, JsonSerializer serializer) + { + serializer.Populate(reader, list); + list.Sort(Comparer); + } + + #endregion + + private class ComparisonComparer : IComparer + { + private readonly Comparison comparison; + + public ComparisonComparer(Func compare) + { + if (compare == null) + { + throw new ArgumentNullException(nameof(compare)); + } + comparison = new Comparison(compare); + } + + public int Compare(TComparison x, TComparison y) + { + return comparison(x, y); + } + } + } + + [JsonConverter(typeof(SortedListConverter))] + internal interface ISortedList + { + void SerializeTo(JsonWriter writer, JsonSerializer serializer); + void DeserializeFrom(JsonReader reader, JsonSerializer serializer); + } +} diff --git a/osu.Framework/Lists/WeakList.cs b/osu.Framework/Lists/WeakList.cs index cd39cf34a..394c78b77 100644 --- a/osu.Framework/Lists/WeakList.cs +++ b/osu.Framework/Lists/WeakList.cs @@ -1,36 +1,36 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Lists -{ - /// - /// A list maintaining weak reference of objects. - /// - /// Type of items tracked by weak reference. - public class WeakList : List> - where T : class - { - public void Add(T obj) => Add(new WeakReference(obj)); - - /// - /// Iterate on alive items, and remove non-alive references. - /// - public void ForEachAlive(Action action) - { - int index = 0; - while (index < Count) - { - if (this[index].TryGetTarget(out T obj)) - { - action(obj); - index++; - } - else - RemoveAt(index); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Lists +{ + /// + /// A list maintaining weak reference of objects. + /// + /// Type of items tracked by weak reference. + public class WeakList : List> + where T : class + { + public void Add(T obj) => Add(new WeakReference(obj)); + + /// + /// Iterate on alive items, and remove non-alive references. + /// + public void ForEachAlive(Action action) + { + int index = 0; + while (index < Count) + { + if (this[index].TryGetTarget(out T obj)) + { + action(obj); + index++; + } + else + RemoveAt(index); + } + } + } +} diff --git a/osu.Framework/Localisation/FormatString.cs b/osu.Framework/Localisation/FormatString.cs index 7d28c5c8e..79093c68e 100644 --- a/osu.Framework/Localisation/FormatString.cs +++ b/osu.Framework/Localisation/FormatString.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Configuration; - -namespace osu.Framework.Localisation -{ - /// - /// A bindable string constructed from . - /// - public class FormatString : Bindable - { - private readonly FormattableString formattable; - - public FormatString(FormattableString formattable) - { - this.formattable = formattable; - Update(); - } - - public void Update() - { - Value = formattable.ToString(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Configuration; + +namespace osu.Framework.Localisation +{ + /// + /// A bindable string constructed from . + /// + public class FormatString : Bindable + { + private readonly FormattableString formattable; + + public FormatString(FormattableString formattable) + { + this.formattable = formattable; + Update(); + } + + public void Update() + { + Value = formattable.ToString(); + } + } +} diff --git a/osu.Framework/Localisation/LocalisationEngine.cs b/osu.Framework/Localisation/LocalisationEngine.cs index 74bcbac77..049bf0a05 100644 --- a/osu.Framework/Localisation/LocalisationEngine.cs +++ b/osu.Framework/Localisation/LocalisationEngine.cs @@ -1,133 +1,133 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.IO.Stores; -using osu.Framework.Lists; - -namespace osu.Framework.Localisation -{ - public class LocalisationEngine - { - private readonly Bindable preferUnicode; - private readonly Bindable locale; - private readonly Dictionary> storages = new Dictionary>(); - private IResourceStore current; - - public virtual IEnumerable SupportedLocales => storages.Keys; - public IEnumerable> SupportedLanguageNames => SupportedLocales.Select(x => new KeyValuePair(x, new CultureInfo(x).NativeName)); - - public LocalisationEngine(FrameworkConfigManager config) - { - preferUnicode = config.GetBindable(FrameworkSetting.ShowUnicode); - preferUnicode.ValueChanged += newValue => - { - lock (unicodeBindings) - unicodeBindings.ForEachAlive(b => b.PreferUnicode = newValue); - }; - - locale = config.GetBindable(FrameworkSetting.Locale); - locale.ValueChanged += checkLocale; - } - - private readonly WeakList unicodeBindings = new WeakList(); - private readonly WeakList localisedBindings = new WeakList(); - private readonly WeakList formattableBindings = new WeakList(); - - public void AddLanguage(string language, IResourceStore storage) - { - storages.Add(language, storage); - locale.TriggerChange(); - } - - public UnicodeBindableString GetUnicodePreference(string unicode, string nonUnicode) - { - var bindable = new UnicodeBindableString(unicode, nonUnicode) - { - PreferUnicode = preferUnicode.Value - }; - - lock (unicodeBindings) - unicodeBindings.Add(bindable); - - return bindable; - } - - public LocalisedString GetLocalisedString(string key) - { - var bindable = new LocalisedString(key) - { - Value = GetLocalised(key) - }; - - lock (localisedBindings) - localisedBindings.Add(bindable); - - return bindable; - } - - public FormatString Format(FormattableString formattable) - { - var bindable = new FormatString(formattable); - - lock (formattableBindings) - formattableBindings.Add(bindable); - - return bindable; - } - - public FormatString FormatVariant(string formatKey, params object[] objects) - { - var bindable = new FormatString(new LocalisedFormatString(GetLocalisedString(formatKey), objects)); - - lock (formattableBindings) - formattableBindings.Add(bindable); - - return bindable; - } - - protected virtual string GetLocalised(string key) => current.Get(key); - - private void checkLocale(string newValue) - { - var locales = SupportedLocales.ToList(); - string validLocale = null; - - if (locales.Contains(newValue)) - validLocale = newValue; - else - { - var culture = string.IsNullOrEmpty(newValue) ? CultureInfo.CurrentCulture : new CultureInfo(newValue); - - for (var c = culture; !c.Equals(CultureInfo.InvariantCulture); c = c.Parent) - if (locales.Contains(c.Name)) - { - validLocale = c.Name; - break; - } - - if (validLocale == null) - validLocale = locales[0]; - } - - if (validLocale != newValue) - locale.Value = validLocale; - else - { - var culture = new CultureInfo(validLocale); - CultureInfo.DefaultThreadCurrentCulture = culture; - CultureInfo.DefaultThreadCurrentUICulture = culture; - ChangeLocale(validLocale); - - lock (localisedBindings) localisedBindings.ForEachAlive(b => b.Value = GetLocalised(b.Key)); - lock (formattableBindings) formattableBindings.ForEachAlive(b => b.Update()); - } - } - - protected virtual void ChangeLocale(string locale) => current = storages[locale]; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.IO.Stores; +using osu.Framework.Lists; + +namespace osu.Framework.Localisation +{ + public class LocalisationEngine + { + private readonly Bindable preferUnicode; + private readonly Bindable locale; + private readonly Dictionary> storages = new Dictionary>(); + private IResourceStore current; + + public virtual IEnumerable SupportedLocales => storages.Keys; + public IEnumerable> SupportedLanguageNames => SupportedLocales.Select(x => new KeyValuePair(x, new CultureInfo(x).NativeName)); + + public LocalisationEngine(FrameworkConfigManager config) + { + preferUnicode = config.GetBindable(FrameworkSetting.ShowUnicode); + preferUnicode.ValueChanged += newValue => + { + lock (unicodeBindings) + unicodeBindings.ForEachAlive(b => b.PreferUnicode = newValue); + }; + + locale = config.GetBindable(FrameworkSetting.Locale); + locale.ValueChanged += checkLocale; + } + + private readonly WeakList unicodeBindings = new WeakList(); + private readonly WeakList localisedBindings = new WeakList(); + private readonly WeakList formattableBindings = new WeakList(); + + public void AddLanguage(string language, IResourceStore storage) + { + storages.Add(language, storage); + locale.TriggerChange(); + } + + public UnicodeBindableString GetUnicodePreference(string unicode, string nonUnicode) + { + var bindable = new UnicodeBindableString(unicode, nonUnicode) + { + PreferUnicode = preferUnicode.Value + }; + + lock (unicodeBindings) + unicodeBindings.Add(bindable); + + return bindable; + } + + public LocalisedString GetLocalisedString(string key) + { + var bindable = new LocalisedString(key) + { + Value = GetLocalised(key) + }; + + lock (localisedBindings) + localisedBindings.Add(bindable); + + return bindable; + } + + public FormatString Format(FormattableString formattable) + { + var bindable = new FormatString(formattable); + + lock (formattableBindings) + formattableBindings.Add(bindable); + + return bindable; + } + + public FormatString FormatVariant(string formatKey, params object[] objects) + { + var bindable = new FormatString(new LocalisedFormatString(GetLocalisedString(formatKey), objects)); + + lock (formattableBindings) + formattableBindings.Add(bindable); + + return bindable; + } + + protected virtual string GetLocalised(string key) => current.Get(key); + + private void checkLocale(string newValue) + { + var locales = SupportedLocales.ToList(); + string validLocale = null; + + if (locales.Contains(newValue)) + validLocale = newValue; + else + { + var culture = string.IsNullOrEmpty(newValue) ? CultureInfo.CurrentCulture : new CultureInfo(newValue); + + for (var c = culture; !c.Equals(CultureInfo.InvariantCulture); c = c.Parent) + if (locales.Contains(c.Name)) + { + validLocale = c.Name; + break; + } + + if (validLocale == null) + validLocale = locales[0]; + } + + if (validLocale != newValue) + locale.Value = validLocale; + else + { + var culture = new CultureInfo(validLocale); + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + ChangeLocale(validLocale); + + lock (localisedBindings) localisedBindings.ForEachAlive(b => b.Value = GetLocalised(b.Key)); + lock (formattableBindings) formattableBindings.ForEachAlive(b => b.Update()); + } + } + + protected virtual void ChangeLocale(string locale) => current = storages[locale]; + } +} diff --git a/osu.Framework/Localisation/LocalisedFormatString.cs b/osu.Framework/Localisation/LocalisedFormatString.cs index 0cc988e57..1dcf57e11 100644 --- a/osu.Framework/Localisation/LocalisedFormatString.cs +++ b/osu.Framework/Localisation/LocalisedFormatString.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Configuration; - -namespace osu.Framework.Localisation -{ - /// - /// A bindable string constructed from , and changable. - /// - public class LocalisedFormatString : FormattableString - { - private readonly Bindable formatSource; - private readonly object[] objects; - - public LocalisedFormatString(Bindable formatSource, params object[] objects) - { - this.formatSource = formatSource; - this.objects = objects; - } - - public override string Format => formatSource.Value; - - public override int ArgumentCount => objects.Length; - - public override object GetArgument(int index) => objects[index]; - - public override object[] GetArguments() => objects; - - public override string ToString(IFormatProvider formatProvider) => string.Format(formatProvider, formatSource.Value, objects); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Configuration; + +namespace osu.Framework.Localisation +{ + /// + /// A bindable string constructed from , and changable. + /// + public class LocalisedFormatString : FormattableString + { + private readonly Bindable formatSource; + private readonly object[] objects; + + public LocalisedFormatString(Bindable formatSource, params object[] objects) + { + this.formatSource = formatSource; + this.objects = objects; + } + + public override string Format => formatSource.Value; + + public override int ArgumentCount => objects.Length; + + public override object GetArgument(int index) => objects[index]; + + public override object[] GetArguments() => objects; + + public override string ToString(IFormatProvider formatProvider) => string.Format(formatProvider, formatSource.Value, objects); + } +} diff --git a/osu.Framework/Localisation/LocalisedString.cs b/osu.Framework/Localisation/LocalisedString.cs index daf861f95..1e934c9ff 100644 --- a/osu.Framework/Localisation/LocalisedString.cs +++ b/osu.Framework/Localisation/LocalisedString.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; - -namespace osu.Framework.Localisation -{ - /// - /// A Bindable string which stays up-to-date with the current locale choice for the specified key. - /// - public class LocalisedString : Bindable - { - public readonly string Key; - public LocalisedString(string key) - { - Key = key; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Framework.Localisation +{ + /// + /// A Bindable string which stays up-to-date with the current locale choice for the specified key. + /// + public class LocalisedString : Bindable + { + public readonly string Key; + public LocalisedString(string key) + { + Key = key; + } + } +} diff --git a/osu.Framework/Localisation/UnicodeBindableString.cs b/osu.Framework/Localisation/UnicodeBindableString.cs index 4d8d34bd2..6b8dcb609 100644 --- a/osu.Framework/Localisation/UnicodeBindableString.cs +++ b/osu.Framework/Localisation/UnicodeBindableString.cs @@ -1,34 +1,34 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; - -namespace osu.Framework.Localisation -{ - /// - /// A Bindable string which takes a unicode and non-unicode (usually romanised) version of the contained text - /// and provides automatic switching behaviour should the user change their preference. - /// - public class UnicodeBindableString : Bindable - { - public readonly string Unicode; - public readonly string NonUnicode; - - public UnicodeBindableString(string unicode, string nonUnicode) : base(nonUnicode) - { - Unicode = unicode; - NonUnicode = nonUnicode; - - if (Unicode == null) - Unicode = NonUnicode; - if (NonUnicode == null) - NonUnicode = Unicode; - } - - public bool PreferUnicode - { - get { return Value == Unicode; } - set { Value = value ? Unicode : NonUnicode; } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Framework.Localisation +{ + /// + /// A Bindable string which takes a unicode and non-unicode (usually romanised) version of the contained text + /// and provides automatic switching behaviour should the user change their preference. + /// + public class UnicodeBindableString : Bindable + { + public readonly string Unicode; + public readonly string NonUnicode; + + public UnicodeBindableString(string unicode, string nonUnicode) : base(nonUnicode) + { + Unicode = unicode; + NonUnicode = nonUnicode; + + if (Unicode == null) + Unicode = NonUnicode; + if (NonUnicode == null) + NonUnicode = Unicode; + } + + public bool PreferUnicode + { + get { return Value == Unicode; } + set { Value = value ? Unicode : NonUnicode; } + } + } +} diff --git a/osu.Framework/Logging/Logger.cs b/osu.Framework/Logging/Logger.cs index 2e16ab728..07dcee0c8 100644 --- a/osu.Framework/Logging/Logger.cs +++ b/osu.Framework/Logging/Logger.cs @@ -1,514 +1,514 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using osu.Framework.Platform; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Threading; - -namespace osu.Framework.Logging -{ - /// - /// This class allows statically (globally) configuring and using logging functionality. - /// - public class Logger - { - private static readonly object static_sync_lock = new object(); - - // separate locking object for flushing so that we don't lock too long on the staticSyncLock object, since we have to - // hold this lock for the entire duration of the flush (waiting for I/O etc) before we can resume scheduling logs - // but other operations like GetLogger(), ApplyFilters() etc. can still be executed while a flush is happening. - private static readonly object flush_sync_lock = new object(); - - /// - /// Whether logging is enabled. Setting this to false will disable all logging. - /// - public static bool Enabled = true; - - /// - /// The minimum log-level a logged message needs to have to be logged. Default is . Please note that setting this to will log input events, including keypresses when entering a password. - /// - public static LogLevel Level = LogLevel.Verbose; - - /// - /// An identifier used in log file headers to figure where the log file came from. - /// - public static string UserIdentifier = Environment.UserName; - - /// - /// An identifier for the game written to log file headers to indicate where the log file came from. - /// - public static string GameIdentifier = @"game"; - - /// - /// An identifier for the version written to log file headers to indicate where the log file came from. - /// - public static string VersionIdentifier = @"unknown"; - - private static Storage storage; - - /// - /// The storage to place logs inside. - /// - public static Storage Storage - { - private get { return storage; } - set { storage = value ?? throw new ArgumentNullException(nameof(value)); } - } - - /// - /// Add a plain-text phrase which should always be filtered from logs. The filtered phrase will be replaced with asterisks (*). - /// Useful for avoiding logging of credentials. - /// See also . - /// - public static void AddFilteredText(string text) - { - if (string.IsNullOrEmpty(text)) return; - - lock (static_sync_lock) - filters.Add(text); - } - - /// - /// Removes phrases which should be filtered from logs. - /// Useful for avoiding logging of credentials. - /// See also . - /// - public static string ApplyFilters(string message) - { - lock (static_sync_lock) - { - foreach (string f in filters) - message = message.Replace(f, string.Empty.PadRight(f.Length, '*')); - } - - return message; - } - - /// - /// Logs the given exception with the given description to the specified logging target. - /// - /// The exception that should be logged. - /// The description of the error that should be logged with the exception. - /// The logging target (file). - /// Whether the inner exceptions of the given exception should be logged recursively. - public static void Error(Exception e, string description, LoggingTarget target = LoggingTarget.Runtime, bool recursive = false) - { - error(e, description, target, null, recursive); - } - - /// - /// Logs the given exception with the given description to the logger with the given name. - /// - /// The exception that should be logged. - /// The description of the error that should be logged with the exception. - /// The logger name (file). - /// Whether the inner exceptions of the given exception should be logged recursively. - public static void Error(Exception e, string description, string name, bool recursive = false) - { - error(e, description, null, name, recursive); - } - - private static void error(Exception e, string description, LoggingTarget? target, string name, bool recursive) - { - log($@"{description}", target, name, LogLevel.Error); - log(e.ToString(), target, name, LogLevel.Important); - - if (recursive) - for (Exception inner = e.InnerException; inner != null; inner = inner.InnerException) - log(inner.ToString(), target, name, LogLevel.Important); - } - - /// - /// Log an arbitrary string to the specified logging target. - /// - /// The message to log. Can include newline (\n) characters to split into multiple lines. - /// The logging target (file). - /// The verbosity level. - public static void Log(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose) - { - log(message, target, null, level); - } - - /// - /// Log an arbitrary string to the logger with the given name. - /// - /// The message to log. Can include newline (\n) characters to split into multiple lines. - /// The logger name (file). - /// The verbosity level. - public static void Log(string message, string name, LogLevel level = LogLevel.Verbose) - { - log(message, null, name, level); - } - - private static void log(string message, LoggingTarget? target, string loggerName, LogLevel level) - { - try - { - if (target.HasValue) - GetLogger(target.Value).Add(message, level); - else - GetLogger(loggerName).Add(message, level); - } - catch - { - } - } - - /// - /// Logs a message to the specified logging target and also displays a print statement. - /// - /// The message to log. Can include newline (\n) characters to split into multiple lines. - /// The logging target (file). - /// The verbosity level. - public static void LogPrint(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose) - { -#if DEBUG - if (Enabled) - System.Diagnostics.Debug.Print(message); -#endif - Log(message, target, level); - } - - /// - /// Logs a message to the logger with the given name and also displays a print statement. - /// - /// The message to log. Can include newline (\n) characters to split into multiple lines. - /// The logger name (file). - /// The verbosity level. - public static void LogPrint(string message, string name, LogLevel level = LogLevel.Verbose) - { -#if DEBUG - if (Enabled) - System.Diagnostics.Debug.Print(message); -#endif - Log(message, name, level); - } - - /// - /// For classes that regularly log to the same target, this method may be preferred over the static Log method. - /// - /// The logging target. - /// The logger responsible for the given logging target. - public static Logger GetLogger(LoggingTarget target = LoggingTarget.Runtime) - { - // there can be no name conflicts between LoggingTarget-based Loggers and named loggers because - // every name that would coincide with a LoggingTarget-value is reserved and cannot be used (see ctor). - return GetLogger(target.ToString()); - } - - /// - /// For classes that regularly log to the same target, this method may be preferred over the static Log method. - /// - /// The name of the custom logger. - /// The logger responsible for the given logging target. - public static Logger GetLogger(string name) - { - lock (static_sync_lock) - { - var nameLower = name.ToLower(); - if (!static_loggers.TryGetValue(nameLower, out Logger l)) - { - static_loggers[nameLower] = l = Enum.TryParse(name, true, out LoggingTarget target) ? new Logger(target) : new Logger(name); - l.clear(); - } - - return l; - } - } - - /// - /// The target for which this logger logs information. This will only be null if the logger has a name. - /// - public LoggingTarget? Target { get; } - - /// - /// The name of the logger. This will only have a value if is null. - /// - public string Name { get; } - - /// - /// Gets the name of the file that this logger is logging to. - /// - public string Filename => $@"{(Target?.ToString() ?? Name).ToLower()}.log"; - - private Logger(LoggingTarget target = LoggingTarget.Runtime) - { - Target = target; - } - - private static readonly HashSet reserved_names = new HashSet(Enum.GetNames(typeof(LoggingTarget)).Select(n => n.ToLower())); - - private Logger(string name) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("The name of a logger must be non-null and may not contain only white space.", nameof(name)); - - if (reserved_names.Contains(name.ToLower())) - throw new ArgumentException($"The name \"{name}\" is reserved. Please use the {nameof(LoggingTarget)}-value corresponding to the name instead."); - - Name = name; - } - - /// - /// Logs a new message with the and will only be logged if your project is built in the Debug configuration. Please note that the default setting for is so unless you increase the to messages printed with this method will not appear in the output. - /// - /// The message that should be logged. - [Conditional("DEBUG")] - public void Debug(string message = @"") - { - Add(message, LogLevel.Debug); - } - - /// - /// Log an arbitrary string to current log. - /// - /// The message to log. Can include newline (\n) characters to split into multiple lines. - /// The verbosity level. - public void Add(string message = @"", LogLevel level = LogLevel.Verbose) => - add(message, level, OutputToListeners); - - private void add(string message = @"", LogLevel level = LogLevel.Verbose, bool outputToListeners = true) - { - if (!Enabled || level < Level) - return; - - ensureHeader(); - -#if DEBUG - if (outputToListeners) - { - var debugLine = $"[{Target?.ToString().ToLower() ?? Name}:{level.ToString().ToLower()}] {message}"; - - // fire to all debug listeners (like visual studio's output window) - System.Diagnostics.Debug.Print(debugLine); - - // fire for console displays (appveyor/CI). - Console.WriteLine(debugLine); - } -#endif - -#if Public - if (level < LogLevel.Important) return; -#endif - -#if !DEBUG - if (level <= LogLevel.Debug) return; -#endif - - message = ApplyFilters(message); - - //split each line up. - string[] lines = message.Replace(@"\r\n", @"\n").Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - string s = lines[i]; - lines[i] = $@"{DateTime.UtcNow.ToString(NumberFormatInfo.InvariantInfo)}: {s.Trim()}"; - } - - if (outputToListeners) - NewEntry?.Invoke(new LogEntry - { - Level = level, - Target = Target, - LoggerName = Name, - Message = message - }); - - if (Target == LoggingTarget.Information) - // don't want to log this to a file - return; - - lock (flush_sync_lock) - { - // we need to check if the logger is still enabled here, since we may have been waiting for a - // flush and while the flush was happening, the logger might have been disabled. In that case - // we want to make sure that we don't accidentally write anything to a file after that flush. - if (!Enabled) - return; - - scheduler.Add(delegate - { - try - { - using (var stream = Storage.GetStream(Filename, FileAccess.Write, FileMode.Append)) - using (var writer = new StreamWriter(stream)) - foreach (var line in lines) - writer.WriteLine(line); - } - catch - { - } - }); - - writer_idle.Reset(); - } - } - - /// - /// Whether the output of this logger should be sent to listeners of and . - /// Defaults to true. - /// - public bool OutputToListeners { get; set; } = true; - - /// - /// Fires whenever any logger tries to log a new entry, but before the entry is actually written to the logfile. - /// - public static event Action NewEntry; - - /// - /// Deletes log file from disk. - /// - private void clear() - { - lock (flush_sync_lock) - { - scheduler.Add(() => Storage.Delete(Filename)); - writer_idle.Reset(); - } - } - - private bool headerAdded; - - private void ensureHeader() - { - if (headerAdded) return; - headerAdded = true; - - add("----------------------------------------------------------", outputToListeners: false); - add($"{Target} Log for {UserIdentifier}", outputToListeners: false); - add($"{GameIdentifier} version {VersionIdentifier}", outputToListeners: false); - add($"Running on {Environment.OSVersion}, {Environment.ProcessorCount} cores", outputToListeners: false); - add("----------------------------------------------------------", outputToListeners: false); - } - - private static readonly List filters = new List(); - private static readonly Dictionary static_loggers = new Dictionary(); - - private static readonly Scheduler scheduler = new Scheduler(); - - private static readonly ManualResetEvent writer_idle = new ManualResetEvent(true); - - static Logger() - { - Task.Factory.StartNew(() => - { - while (true) - { - if ((Storage != null ? scheduler.Update() : 0) == 0) - writer_idle.Set(); - Thread.Sleep(50); - } - - // ReSharper disable once FunctionNeverReturns - }, TaskCreationOptions.LongRunning); - } - - /// - /// Pause execution until all logger writes have completed and file handles have been closed. - /// This will also unbind all handlers bound to . - /// - public static void Flush() - { - lock (flush_sync_lock) - { - writer_idle.WaitOne(500); - NewEntry = null; - } - } - } - - /// - /// Captures information about a logged message. - /// - public class LogEntry - { - /// - /// The level for which the message was logged. - /// - public LogLevel Level; - - /// - /// The target to which this message is being logged, or null if it is being logged to a custom named logger. - /// - public LoggingTarget? Target; - - /// - /// The name of the logger to which this message is being logged, or null if it is being logged to a specific . - /// - public string LoggerName; - - /// - /// The message that was logged. - /// - public string Message; - } - - /// - /// The level on which a log-message is logged. - /// - public enum LogLevel - { - /// - /// Log-level for debugging-related log-messages. This is the lowest level (highest verbosity). Please note that this will log input events, including keypresses when entering a password. - /// - Debug, - - /// - /// Log-level for most log-messages. This is the second-lowest level (second-highest verbosity). - /// - Verbose, - - /// - /// Log-level for important log-messages. This is the second-highest level (second-lowest verbosity). - /// - Important, - - /// - /// Log-level for error messages. This is the highest level (lowest verbosity). - /// - Error - } - - /// - /// The target for logging. Different targets can have different logfiles, are displayed differently in the LogOverlay and are generally useful for organizing logs into groups. - /// - public enum LoggingTarget - { - /// - /// Logging target for general information. Everything logged with this target will not be written to a logfile. - /// - Information, - - /// - /// Logging target for information about the runtime. - /// - Runtime, - - /// - /// Logging target for network-related events. - /// - Network, - - /// - /// Logging target for performance-related information. - /// - Performance, - - /// - /// Logging target for information relevant to debugging. - /// - Debug, - - /// - /// Logging target for database-related events. - /// - Database - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using osu.Framework.Platform; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Threading; + +namespace osu.Framework.Logging +{ + /// + /// This class allows statically (globally) configuring and using logging functionality. + /// + public class Logger + { + private static readonly object static_sync_lock = new object(); + + // separate locking object for flushing so that we don't lock too long on the staticSyncLock object, since we have to + // hold this lock for the entire duration of the flush (waiting for I/O etc) before we can resume scheduling logs + // but other operations like GetLogger(), ApplyFilters() etc. can still be executed while a flush is happening. + private static readonly object flush_sync_lock = new object(); + + /// + /// Whether logging is enabled. Setting this to false will disable all logging. + /// + public static bool Enabled = true; + + /// + /// The minimum log-level a logged message needs to have to be logged. Default is . Please note that setting this to will log input events, including keypresses when entering a password. + /// + public static LogLevel Level = LogLevel.Verbose; + + /// + /// An identifier used in log file headers to figure where the log file came from. + /// + public static string UserIdentifier = Environment.UserName; + + /// + /// An identifier for the game written to log file headers to indicate where the log file came from. + /// + public static string GameIdentifier = @"game"; + + /// + /// An identifier for the version written to log file headers to indicate where the log file came from. + /// + public static string VersionIdentifier = @"unknown"; + + private static Storage storage; + + /// + /// The storage to place logs inside. + /// + public static Storage Storage + { + private get { return storage; } + set { storage = value ?? throw new ArgumentNullException(nameof(value)); } + } + + /// + /// Add a plain-text phrase which should always be filtered from logs. The filtered phrase will be replaced with asterisks (*). + /// Useful for avoiding logging of credentials. + /// See also . + /// + public static void AddFilteredText(string text) + { + if (string.IsNullOrEmpty(text)) return; + + lock (static_sync_lock) + filters.Add(text); + } + + /// + /// Removes phrases which should be filtered from logs. + /// Useful for avoiding logging of credentials. + /// See also . + /// + public static string ApplyFilters(string message) + { + lock (static_sync_lock) + { + foreach (string f in filters) + message = message.Replace(f, string.Empty.PadRight(f.Length, '*')); + } + + return message; + } + + /// + /// Logs the given exception with the given description to the specified logging target. + /// + /// The exception that should be logged. + /// The description of the error that should be logged with the exception. + /// The logging target (file). + /// Whether the inner exceptions of the given exception should be logged recursively. + public static void Error(Exception e, string description, LoggingTarget target = LoggingTarget.Runtime, bool recursive = false) + { + error(e, description, target, null, recursive); + } + + /// + /// Logs the given exception with the given description to the logger with the given name. + /// + /// The exception that should be logged. + /// The description of the error that should be logged with the exception. + /// The logger name (file). + /// Whether the inner exceptions of the given exception should be logged recursively. + public static void Error(Exception e, string description, string name, bool recursive = false) + { + error(e, description, null, name, recursive); + } + + private static void error(Exception e, string description, LoggingTarget? target, string name, bool recursive) + { + log($@"{description}", target, name, LogLevel.Error); + log(e.ToString(), target, name, LogLevel.Important); + + if (recursive) + for (Exception inner = e.InnerException; inner != null; inner = inner.InnerException) + log(inner.ToString(), target, name, LogLevel.Important); + } + + /// + /// Log an arbitrary string to the specified logging target. + /// + /// The message to log. Can include newline (\n) characters to split into multiple lines. + /// The logging target (file). + /// The verbosity level. + public static void Log(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose) + { + log(message, target, null, level); + } + + /// + /// Log an arbitrary string to the logger with the given name. + /// + /// The message to log. Can include newline (\n) characters to split into multiple lines. + /// The logger name (file). + /// The verbosity level. + public static void Log(string message, string name, LogLevel level = LogLevel.Verbose) + { + log(message, null, name, level); + } + + private static void log(string message, LoggingTarget? target, string loggerName, LogLevel level) + { + try + { + if (target.HasValue) + GetLogger(target.Value).Add(message, level); + else + GetLogger(loggerName).Add(message, level); + } + catch + { + } + } + + /// + /// Logs a message to the specified logging target and also displays a print statement. + /// + /// The message to log. Can include newline (\n) characters to split into multiple lines. + /// The logging target (file). + /// The verbosity level. + public static void LogPrint(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose) + { +#if DEBUG + if (Enabled) + System.Diagnostics.Debug.Print(message); +#endif + Log(message, target, level); + } + + /// + /// Logs a message to the logger with the given name and also displays a print statement. + /// + /// The message to log. Can include newline (\n) characters to split into multiple lines. + /// The logger name (file). + /// The verbosity level. + public static void LogPrint(string message, string name, LogLevel level = LogLevel.Verbose) + { +#if DEBUG + if (Enabled) + System.Diagnostics.Debug.Print(message); +#endif + Log(message, name, level); + } + + /// + /// For classes that regularly log to the same target, this method may be preferred over the static Log method. + /// + /// The logging target. + /// The logger responsible for the given logging target. + public static Logger GetLogger(LoggingTarget target = LoggingTarget.Runtime) + { + // there can be no name conflicts between LoggingTarget-based Loggers and named loggers because + // every name that would coincide with a LoggingTarget-value is reserved and cannot be used (see ctor). + return GetLogger(target.ToString()); + } + + /// + /// For classes that regularly log to the same target, this method may be preferred over the static Log method. + /// + /// The name of the custom logger. + /// The logger responsible for the given logging target. + public static Logger GetLogger(string name) + { + lock (static_sync_lock) + { + var nameLower = name.ToLower(); + if (!static_loggers.TryGetValue(nameLower, out Logger l)) + { + static_loggers[nameLower] = l = Enum.TryParse(name, true, out LoggingTarget target) ? new Logger(target) : new Logger(name); + l.clear(); + } + + return l; + } + } + + /// + /// The target for which this logger logs information. This will only be null if the logger has a name. + /// + public LoggingTarget? Target { get; } + + /// + /// The name of the logger. This will only have a value if is null. + /// + public string Name { get; } + + /// + /// Gets the name of the file that this logger is logging to. + /// + public string Filename => $@"{(Target?.ToString() ?? Name).ToLower()}.log"; + + private Logger(LoggingTarget target = LoggingTarget.Runtime) + { + Target = target; + } + + private static readonly HashSet reserved_names = new HashSet(Enum.GetNames(typeof(LoggingTarget)).Select(n => n.ToLower())); + + private Logger(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("The name of a logger must be non-null and may not contain only white space.", nameof(name)); + + if (reserved_names.Contains(name.ToLower())) + throw new ArgumentException($"The name \"{name}\" is reserved. Please use the {nameof(LoggingTarget)}-value corresponding to the name instead."); + + Name = name; + } + + /// + /// Logs a new message with the and will only be logged if your project is built in the Debug configuration. Please note that the default setting for is so unless you increase the to messages printed with this method will not appear in the output. + /// + /// The message that should be logged. + [Conditional("DEBUG")] + public void Debug(string message = @"") + { + Add(message, LogLevel.Debug); + } + + /// + /// Log an arbitrary string to current log. + /// + /// The message to log. Can include newline (\n) characters to split into multiple lines. + /// The verbosity level. + public void Add(string message = @"", LogLevel level = LogLevel.Verbose) => + add(message, level, OutputToListeners); + + private void add(string message = @"", LogLevel level = LogLevel.Verbose, bool outputToListeners = true) + { + if (!Enabled || level < Level) + return; + + ensureHeader(); + +#if DEBUG + if (outputToListeners) + { + var debugLine = $"[{Target?.ToString().ToLower() ?? Name}:{level.ToString().ToLower()}] {message}"; + + // fire to all debug listeners (like visual studio's output window) + System.Diagnostics.Debug.Print(debugLine); + + // fire for console displays (appveyor/CI). + Console.WriteLine(debugLine); + } +#endif + +#if Public + if (level < LogLevel.Important) return; +#endif + +#if !DEBUG + if (level <= LogLevel.Debug) return; +#endif + + message = ApplyFilters(message); + + //split each line up. + string[] lines = message.Replace(@"\r\n", @"\n").Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + string s = lines[i]; + lines[i] = $@"{DateTime.UtcNow.ToString(NumberFormatInfo.InvariantInfo)}: {s.Trim()}"; + } + + if (outputToListeners) + NewEntry?.Invoke(new LogEntry + { + Level = level, + Target = Target, + LoggerName = Name, + Message = message + }); + + if (Target == LoggingTarget.Information) + // don't want to log this to a file + return; + + lock (flush_sync_lock) + { + // we need to check if the logger is still enabled here, since we may have been waiting for a + // flush and while the flush was happening, the logger might have been disabled. In that case + // we want to make sure that we don't accidentally write anything to a file after that flush. + if (!Enabled) + return; + + scheduler.Add(delegate + { + try + { + using (var stream = Storage.GetStream(Filename, FileAccess.Write, FileMode.Append)) + using (var writer = new StreamWriter(stream)) + foreach (var line in lines) + writer.WriteLine(line); + } + catch + { + } + }); + + writer_idle.Reset(); + } + } + + /// + /// Whether the output of this logger should be sent to listeners of and . + /// Defaults to true. + /// + public bool OutputToListeners { get; set; } = true; + + /// + /// Fires whenever any logger tries to log a new entry, but before the entry is actually written to the logfile. + /// + public static event Action NewEntry; + + /// + /// Deletes log file from disk. + /// + private void clear() + { + lock (flush_sync_lock) + { + scheduler.Add(() => Storage.Delete(Filename)); + writer_idle.Reset(); + } + } + + private bool headerAdded; + + private void ensureHeader() + { + if (headerAdded) return; + headerAdded = true; + + add("----------------------------------------------------------", outputToListeners: false); + add($"{Target} Log for {UserIdentifier}", outputToListeners: false); + add($"{GameIdentifier} version {VersionIdentifier}", outputToListeners: false); + add($"Running on {Environment.OSVersion}, {Environment.ProcessorCount} cores", outputToListeners: false); + add("----------------------------------------------------------", outputToListeners: false); + } + + private static readonly List filters = new List(); + private static readonly Dictionary static_loggers = new Dictionary(); + + private static readonly Scheduler scheduler = new Scheduler(); + + private static readonly ManualResetEvent writer_idle = new ManualResetEvent(true); + + static Logger() + { + Task.Factory.StartNew(() => + { + while (true) + { + if ((Storage != null ? scheduler.Update() : 0) == 0) + writer_idle.Set(); + Thread.Sleep(50); + } + + // ReSharper disable once FunctionNeverReturns + }, TaskCreationOptions.LongRunning); + } + + /// + /// Pause execution until all logger writes have completed and file handles have been closed. + /// This will also unbind all handlers bound to . + /// + public static void Flush() + { + lock (flush_sync_lock) + { + writer_idle.WaitOne(500); + NewEntry = null; + } + } + } + + /// + /// Captures information about a logged message. + /// + public class LogEntry + { + /// + /// The level for which the message was logged. + /// + public LogLevel Level; + + /// + /// The target to which this message is being logged, or null if it is being logged to a custom named logger. + /// + public LoggingTarget? Target; + + /// + /// The name of the logger to which this message is being logged, or null if it is being logged to a specific . + /// + public string LoggerName; + + /// + /// The message that was logged. + /// + public string Message; + } + + /// + /// The level on which a log-message is logged. + /// + public enum LogLevel + { + /// + /// Log-level for debugging-related log-messages. This is the lowest level (highest verbosity). Please note that this will log input events, including keypresses when entering a password. + /// + Debug, + + /// + /// Log-level for most log-messages. This is the second-lowest level (second-highest verbosity). + /// + Verbose, + + /// + /// Log-level for important log-messages. This is the second-highest level (second-lowest verbosity). + /// + Important, + + /// + /// Log-level for error messages. This is the highest level (lowest verbosity). + /// + Error + } + + /// + /// The target for logging. Different targets can have different logfiles, are displayed differently in the LogOverlay and are generally useful for organizing logs into groups. + /// + public enum LoggingTarget + { + /// + /// Logging target for general information. Everything logged with this target will not be written to a logfile. + /// + Information, + + /// + /// Logging target for information about the runtime. + /// + Runtime, + + /// + /// Logging target for network-related events. + /// + Network, + + /// + /// Logging target for performance-related information. + /// + Performance, + + /// + /// Logging target for information relevant to debugging. + /// + Debug, + + /// + /// Logging target for database-related events. + /// + Database + } +} diff --git a/osu.Framework/MathUtils/Blur.cs b/osu.Framework/MathUtils/Blur.cs index 2665a0842..f41370290 100644 --- a/osu.Framework/MathUtils/Blur.cs +++ b/osu.Framework/MathUtils/Blur.cs @@ -1,48 +1,48 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.MathUtils -{ - /// - /// Helper methods for blur calculations. - /// - public static class Blur - { - /// - /// Evaluates a 1-D gaussian distribution with 0 mean and sigma standard deviation at position x. - /// - /// The position to evaluate the distribution at. - /// Standard deviation of the distribution. - /// The probability density of the distribution at x. - public static double EvalGaussian(float x, float sigma) - { - const double inv_sqrt_2_pi = 0.39894; - return inv_sqrt_2_pi * Math.Exp(-0.5 * x * x / (sigma * sigma)) / sigma; - } - - /// - /// Finds the guassian blur kernel size where the magnitude of the gaussian distribution within the kernel - /// is greater than or equal to cutoffThreshold times the maximum of the distribution. - /// - /// Standard deviation of the distribution. - /// The threshold that defines the kernel size. - /// The size of the kernel satisfying the requested threshold. - public static int KernelSize(float sigma, float cutoffThreshold = 0.1f) - { - if (sigma == 0) - return 0; - - const int max_radius = 200; - - double center = EvalGaussian(0, sigma); - double threshold = cutoffThreshold * center; - for (int i = 0; i < max_radius; ++i) - if (EvalGaussian(i, sigma) < threshold) - return Math.Max(i - 1, 0); - - return max_radius; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.MathUtils +{ + /// + /// Helper methods for blur calculations. + /// + public static class Blur + { + /// + /// Evaluates a 1-D gaussian distribution with 0 mean and sigma standard deviation at position x. + /// + /// The position to evaluate the distribution at. + /// Standard deviation of the distribution. + /// The probability density of the distribution at x. + public static double EvalGaussian(float x, float sigma) + { + const double inv_sqrt_2_pi = 0.39894; + return inv_sqrt_2_pi * Math.Exp(-0.5 * x * x / (sigma * sigma)) / sigma; + } + + /// + /// Finds the guassian blur kernel size where the magnitude of the gaussian distribution within the kernel + /// is greater than or equal to cutoffThreshold times the maximum of the distribution. + /// + /// Standard deviation of the distribution. + /// The threshold that defines the kernel size. + /// The size of the kernel satisfying the requested threshold. + public static int KernelSize(float sigma, float cutoffThreshold = 0.1f) + { + if (sigma == 0) + return 0; + + const int max_radius = 200; + + double center = EvalGaussian(0, sigma); + double threshold = cutoffThreshold * center; + for (int i = 0; i < max_radius; ++i) + if (EvalGaussian(i, sigma) < threshold) + return Math.Max(i - 1, 0); + + return max_radius; + } + } +} diff --git a/osu.Framework/MathUtils/Interpolation.cs b/osu.Framework/MathUtils/Interpolation.cs index e6f9d3972..f45296840 100644 --- a/osu.Framework/MathUtils/Interpolation.cs +++ b/osu.Framework/MathUtils/Interpolation.cs @@ -1,281 +1,281 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.MathUtils -{ - public static class Interpolation - { - public static double Lerp(double start, double final, double amount) => start + (final - start) * amount; - - /// - /// Interpolates between 2 values (start and final) using a given base and exponent. - /// - /// The start value. - /// The end value. - /// The base of the exponential. The valid range is [0, 1], where smaller values mean that the final value is achieved more quickly, and values closer to 1 results in slow convergence to the final value. - /// The exponent of the exponential. An exponent of 0 results in the start values, whereas larger exponents make the result converge to the final value. - /// - public static double Damp(double start, double final, double @base, double exponent) - { - if (@base < 0 || @base > 1) - throw new ArgumentOutOfRangeException($"{nameof(@base)} has to lie in [0,1], but is {@base}.", nameof(@base)); - if (exponent < 0) - throw new ArgumentOutOfRangeException($"{nameof(exponent)} has to be bigger than 0, but is {exponent}.", nameof(exponent)); - - return Lerp(start, final, 1 - Math.Pow(@base, exponent)); - } - - public static ColourInfo ValueAt(double time, ColourInfo startColour, ColourInfo endColour, double startTime, double endTime, Easing easing = Easing.None) - { - if (startColour.HasSingleColour && endColour.HasSingleColour) - return ValueAt(time, (Color4)startColour, (Color4)endColour, startTime, endTime, easing); - - return new ColourInfo - { - TopLeft = ValueAt(time, (Color4)startColour.TopLeft, (Color4)endColour.TopLeft, startTime, endTime, easing), - BottomLeft = ValueAt(time, (Color4)startColour.BottomLeft, (Color4)endColour.BottomLeft, startTime, endTime, easing), - TopRight = ValueAt(time, (Color4)startColour.TopRight, (Color4)endColour.TopRight, startTime, endTime, easing), - BottomRight = ValueAt(time, (Color4)startColour.BottomRight, (Color4)endColour.BottomRight, startTime, endTime, easing), - }; - } - - public static EdgeEffectParameters ValueAt(double time, EdgeEffectParameters startParams, EdgeEffectParameters endParams, double startTime, double endTime, Easing easing = Easing.None) - { - return new EdgeEffectParameters - { - Type = startParams.Type, - Hollow = startParams.Hollow, - Colour = ValueAt(time, startParams.Colour, endParams.Colour, startTime, endTime, easing), - Offset = ValueAt(time, startParams.Offset, endParams.Offset, startTime, endTime, easing), - Radius = ValueAt(time, startParams.Radius, endParams.Radius, startTime, endTime, easing), - Roundness = ValueAt(time, startParams.Roundness, endParams.Roundness, startTime, endTime, easing), - }; - } - - public static SRGBColour ValueAt(double time, SRGBColour startColour, SRGBColour endColour, double startTime, double endTime, Easing easing = Easing.None) => - ValueAt(time, (Color4)startColour, (Color4)endColour, startTime, endTime, easing); - - public static Color4 ValueAt(double time, Color4 startColour, Color4 endColour, double startTime, double endTime, Easing easing = Easing.None) - { - if (startColour == endColour) - return startColour; - - double current = time - startTime; - double duration = endTime - startTime; - - if (duration == 0 || current == 0) - return startColour; - - float t = Math.Max(0, Math.Min(1, (float)ApplyEasing(easing, current / duration))); - - return new Color4( - startColour.R + t * (endColour.R - startColour.R), - startColour.G + t * (endColour.G - startColour.G), - startColour.B + t * (endColour.B - startColour.B), - startColour.A + t * (endColour.A - startColour.A)); - } - - public static byte ValueAt(double time, byte val1, byte val2, double startTime, double endTime, Easing easing = Easing.None) => - (byte)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static sbyte ValueAt(double time, sbyte val1, sbyte val2, double startTime, double endTime, Easing easing = Easing.None) => - (sbyte)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static short ValueAt(double time, short val1, short val2, double startTime, double endTime, Easing easing = Easing.None) => - (short)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static ushort ValueAt(double time, ushort val1, ushort val2, double startTime, double endTime, Easing easing = Easing.None) => - (ushort)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static int ValueAt(double time, int val1, int val2, double startTime, double endTime, Easing easing = Easing.None) => - (int)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static uint ValueAt(double time, uint val1, uint val2, double startTime, double endTime, Easing easing = Easing.None) => - (uint)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static long ValueAt(double time, long val1, long val2, double startTime, double endTime, Easing easing = Easing.None) => - (long)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static ulong ValueAt(double time, ulong val1, ulong val2, double startTime, double endTime, Easing easing = Easing.None) => - (ulong)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); - - public static float ValueAt(double time, float val1, float val2, double startTime, double endTime, Easing easing = Easing.None) => - (float)ValueAt(time, (double)val1, val2, startTime, endTime, easing); - - public static decimal ValueAt(double time, decimal val1, decimal val2, double startTime, double endTime, Easing easing = Easing.None) => - (decimal)ValueAt(time, (double)val1, (double)val2, startTime, endTime, easing); - - public static double ValueAt(double time, double val1, double val2, double startTime, double endTime, Easing easing = Easing.None) - { - if (val1 == val2) - return val1; - - double current = time - startTime; - double duration = endTime - startTime; - - if (current == 0) - return val1; - if (duration == 0) - return val2; - - double t = ApplyEasing(easing, current / duration); - return val1 + t * (val2 - val1); - } - - public static Vector2 ValueAt(double time, Vector2 val1, Vector2 val2, double startTime, double endTime, Easing easing = Easing.None) - { - float current = (float)(time - startTime); - float duration = (float)(endTime - startTime); - - if (duration == 0 || current == 0) - return val1; - - float t = (float)ApplyEasing(easing, current / duration); - return val1 + t * (val2 - val1); - } - - public static RectangleF ValueAt(double time, RectangleF val1, RectangleF val2, double startTime, double endTime, Easing easing = Easing.None) - { - float current = (float)(time - startTime); - float duration = (float)(endTime - startTime); - - if (duration == 0 || current == 0) - return val1; - - float t = (float)ApplyEasing(easing, current / duration); - - return new RectangleF( - val1.X + t * (val2.X - val1.X), - val1.Y + t * (val2.Y - val1.Y), - val1.Width + t * (val2.Width - val1.Width), - val1.Height + t * (val2.X - val1.Height)); - } - - public static double ApplyEasing(Easing easing, double time) - { - const double elastic_const = 2 * Math.PI / .3; - const double elastic_const2 = .3 / 4; - - const double back_const = 1.70158; - const double back_const2 = back_const * 1.525; - - const double bounce_const = 1 / 2.75; - - switch (easing) - { - default: - return time; - - case Easing.In: - case Easing.InQuad: - return time * time; - case Easing.Out: - case Easing.OutQuad: - return time * (2 - time); - case Easing.InOutQuad: - if (time < .5) return time * time * 2; - return --time * time * -2 + 1; - - case Easing.InCubic: - return time * time * time; - case Easing.OutCubic: - return --time * time * time + 1; - case Easing.InOutCubic: - if (time < .5) return time * time * time * 4; - return --time * time * time * 4 + 1; - - case Easing.InQuart: - return time * time * time * time; - case Easing.OutQuart: - return 1 - --time * time * time * time; - case Easing.InOutQuart: - if (time < .5) return time * time * time * time * 8; - return --time * time * time * time * -8 + 1; - - case Easing.InQuint: - return time * time * time * time * time; - case Easing.OutQuint: - return --time * time * time * time * time + 1; - case Easing.InOutQuint: - if (time < .5) return time * time * time * time * time * 16; - return --time * time * time * time * time * 16 + 1; - - case Easing.InSine: - return 1 - Math.Cos(time * Math.PI * .5); - case Easing.OutSine: - return Math.Sin(time * Math.PI * .5); - case Easing.InOutSine: - return .5 - .5 * Math.Cos(Math.PI * time); - - case Easing.InExpo: - return Math.Pow(2, 10 * (time - 1)); - case Easing.OutExpo: - return -Math.Pow(2, -10 * time) + 1; - case Easing.InOutExpo: - if (time < .5) return .5 * Math.Pow(2, 20 * time - 10); - return 1 - .5 * Math.Pow(2, -20 * time + 10); - - case Easing.InCirc: - return 1 - Math.Sqrt(1 - time * time); - case Easing.OutCirc: - return Math.Sqrt(1 - --time * time); - case Easing.InOutCirc: - if ((time *= 2) < 1) return .5 - .5 * Math.Sqrt(1 - time * time); - return .5 * Math.Sqrt(1 - (time -= 2) * time) + .5; - - case Easing.InElastic: - return -Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 - time) * elastic_const); - case Easing.OutElastic: - return Math.Pow(2, -10 * time) * Math.Sin((time - elastic_const2) * elastic_const) + 1; - case Easing.OutElasticHalf: - return Math.Pow(2, -10 * time) * Math.Sin((.5 * time - elastic_const2) * elastic_const) + 1; - case Easing.OutElasticQuarter: - return Math.Pow(2, -10 * time) * Math.Sin((.25 * time - elastic_const2) * elastic_const) + 1; - case Easing.InOutElastic: - if ((time *= 2) < 1) - return -.5 * Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 * 1.5 - time) * elastic_const / 1.5); - return .5 * Math.Pow(2, -10 * --time) * Math.Sin((time - elastic_const2 * 1.5) * elastic_const / 1.5) + 1; - - case Easing.InBack: - return time * time * ((back_const + 1) * time - back_const); - case Easing.OutBack: - return --time * time * ((back_const + 1) * time + back_const) + 1; - case Easing.InOutBack: - if ((time *= 2) < 1) return .5 * time * time * ((back_const2 + 1) * time - back_const2); - return .5 * ((time -= 2) * time * ((back_const2 + 1) * time + back_const2) + 2); - - case Easing.InBounce: - time = 1 - time; - if (time < bounce_const) - return 1 - 7.5625 * time * time; - if (time < 2 * bounce_const) - return 1 - (7.5625 * (time -= 1.5 * bounce_const) * time + .75); - if (time < 2.5 * bounce_const) - return 1 - (7.5625 * (time -= 2.25 * bounce_const) * time + .9375); - return 1 - (7.5625 * (time -= 2.625 * bounce_const) * time + .984375); - case Easing.OutBounce: - if (time < bounce_const) - return 7.5625 * time * time; - if (time < 2 * bounce_const) - return 7.5625 * (time -= 1.5 * bounce_const) * time + .75; - if (time < 2.5 * bounce_const) - return 7.5625 * (time -= 2.25 * bounce_const) * time + .9375; - return 7.5625 * (time -= 2.625 * bounce_const) * time + .984375; - case Easing.InOutBounce: - if (time < .5) return .5 - .5 * ApplyEasing(Easing.OutBounce, 1 - time * 2); - return ApplyEasing(Easing.OutBounce, (time - .5) * 2) * .5 + .5; - - case Easing.OutPow10: - return --time * Math.Pow(time, 10) + 1; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.MathUtils +{ + public static class Interpolation + { + public static double Lerp(double start, double final, double amount) => start + (final - start) * amount; + + /// + /// Interpolates between 2 values (start and final) using a given base and exponent. + /// + /// The start value. + /// The end value. + /// The base of the exponential. The valid range is [0, 1], where smaller values mean that the final value is achieved more quickly, and values closer to 1 results in slow convergence to the final value. + /// The exponent of the exponential. An exponent of 0 results in the start values, whereas larger exponents make the result converge to the final value. + /// + public static double Damp(double start, double final, double @base, double exponent) + { + if (@base < 0 || @base > 1) + throw new ArgumentOutOfRangeException($"{nameof(@base)} has to lie in [0,1], but is {@base}.", nameof(@base)); + if (exponent < 0) + throw new ArgumentOutOfRangeException($"{nameof(exponent)} has to be bigger than 0, but is {exponent}.", nameof(exponent)); + + return Lerp(start, final, 1 - Math.Pow(@base, exponent)); + } + + public static ColourInfo ValueAt(double time, ColourInfo startColour, ColourInfo endColour, double startTime, double endTime, Easing easing = Easing.None) + { + if (startColour.HasSingleColour && endColour.HasSingleColour) + return ValueAt(time, (Color4)startColour, (Color4)endColour, startTime, endTime, easing); + + return new ColourInfo + { + TopLeft = ValueAt(time, (Color4)startColour.TopLeft, (Color4)endColour.TopLeft, startTime, endTime, easing), + BottomLeft = ValueAt(time, (Color4)startColour.BottomLeft, (Color4)endColour.BottomLeft, startTime, endTime, easing), + TopRight = ValueAt(time, (Color4)startColour.TopRight, (Color4)endColour.TopRight, startTime, endTime, easing), + BottomRight = ValueAt(time, (Color4)startColour.BottomRight, (Color4)endColour.BottomRight, startTime, endTime, easing), + }; + } + + public static EdgeEffectParameters ValueAt(double time, EdgeEffectParameters startParams, EdgeEffectParameters endParams, double startTime, double endTime, Easing easing = Easing.None) + { + return new EdgeEffectParameters + { + Type = startParams.Type, + Hollow = startParams.Hollow, + Colour = ValueAt(time, startParams.Colour, endParams.Colour, startTime, endTime, easing), + Offset = ValueAt(time, startParams.Offset, endParams.Offset, startTime, endTime, easing), + Radius = ValueAt(time, startParams.Radius, endParams.Radius, startTime, endTime, easing), + Roundness = ValueAt(time, startParams.Roundness, endParams.Roundness, startTime, endTime, easing), + }; + } + + public static SRGBColour ValueAt(double time, SRGBColour startColour, SRGBColour endColour, double startTime, double endTime, Easing easing = Easing.None) => + ValueAt(time, (Color4)startColour, (Color4)endColour, startTime, endTime, easing); + + public static Color4 ValueAt(double time, Color4 startColour, Color4 endColour, double startTime, double endTime, Easing easing = Easing.None) + { + if (startColour == endColour) + return startColour; + + double current = time - startTime; + double duration = endTime - startTime; + + if (duration == 0 || current == 0) + return startColour; + + float t = Math.Max(0, Math.Min(1, (float)ApplyEasing(easing, current / duration))); + + return new Color4( + startColour.R + t * (endColour.R - startColour.R), + startColour.G + t * (endColour.G - startColour.G), + startColour.B + t * (endColour.B - startColour.B), + startColour.A + t * (endColour.A - startColour.A)); + } + + public static byte ValueAt(double time, byte val1, byte val2, double startTime, double endTime, Easing easing = Easing.None) => + (byte)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static sbyte ValueAt(double time, sbyte val1, sbyte val2, double startTime, double endTime, Easing easing = Easing.None) => + (sbyte)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static short ValueAt(double time, short val1, short val2, double startTime, double endTime, Easing easing = Easing.None) => + (short)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static ushort ValueAt(double time, ushort val1, ushort val2, double startTime, double endTime, Easing easing = Easing.None) => + (ushort)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static int ValueAt(double time, int val1, int val2, double startTime, double endTime, Easing easing = Easing.None) => + (int)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static uint ValueAt(double time, uint val1, uint val2, double startTime, double endTime, Easing easing = Easing.None) => + (uint)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static long ValueAt(double time, long val1, long val2, double startTime, double endTime, Easing easing = Easing.None) => + (long)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static ulong ValueAt(double time, ulong val1, ulong val2, double startTime, double endTime, Easing easing = Easing.None) => + (ulong)Math.Round(ValueAt(time, (double)val1, val2, startTime, endTime, easing)); + + public static float ValueAt(double time, float val1, float val2, double startTime, double endTime, Easing easing = Easing.None) => + (float)ValueAt(time, (double)val1, val2, startTime, endTime, easing); + + public static decimal ValueAt(double time, decimal val1, decimal val2, double startTime, double endTime, Easing easing = Easing.None) => + (decimal)ValueAt(time, (double)val1, (double)val2, startTime, endTime, easing); + + public static double ValueAt(double time, double val1, double val2, double startTime, double endTime, Easing easing = Easing.None) + { + if (val1 == val2) + return val1; + + double current = time - startTime; + double duration = endTime - startTime; + + if (current == 0) + return val1; + if (duration == 0) + return val2; + + double t = ApplyEasing(easing, current / duration); + return val1 + t * (val2 - val1); + } + + public static Vector2 ValueAt(double time, Vector2 val1, Vector2 val2, double startTime, double endTime, Easing easing = Easing.None) + { + float current = (float)(time - startTime); + float duration = (float)(endTime - startTime); + + if (duration == 0 || current == 0) + return val1; + + float t = (float)ApplyEasing(easing, current / duration); + return val1 + t * (val2 - val1); + } + + public static RectangleF ValueAt(double time, RectangleF val1, RectangleF val2, double startTime, double endTime, Easing easing = Easing.None) + { + float current = (float)(time - startTime); + float duration = (float)(endTime - startTime); + + if (duration == 0 || current == 0) + return val1; + + float t = (float)ApplyEasing(easing, current / duration); + + return new RectangleF( + val1.X + t * (val2.X - val1.X), + val1.Y + t * (val2.Y - val1.Y), + val1.Width + t * (val2.Width - val1.Width), + val1.Height + t * (val2.X - val1.Height)); + } + + public static double ApplyEasing(Easing easing, double time) + { + const double elastic_const = 2 * Math.PI / .3; + const double elastic_const2 = .3 / 4; + + const double back_const = 1.70158; + const double back_const2 = back_const * 1.525; + + const double bounce_const = 1 / 2.75; + + switch (easing) + { + default: + return time; + + case Easing.In: + case Easing.InQuad: + return time * time; + case Easing.Out: + case Easing.OutQuad: + return time * (2 - time); + case Easing.InOutQuad: + if (time < .5) return time * time * 2; + return --time * time * -2 + 1; + + case Easing.InCubic: + return time * time * time; + case Easing.OutCubic: + return --time * time * time + 1; + case Easing.InOutCubic: + if (time < .5) return time * time * time * 4; + return --time * time * time * 4 + 1; + + case Easing.InQuart: + return time * time * time * time; + case Easing.OutQuart: + return 1 - --time * time * time * time; + case Easing.InOutQuart: + if (time < .5) return time * time * time * time * 8; + return --time * time * time * time * -8 + 1; + + case Easing.InQuint: + return time * time * time * time * time; + case Easing.OutQuint: + return --time * time * time * time * time + 1; + case Easing.InOutQuint: + if (time < .5) return time * time * time * time * time * 16; + return --time * time * time * time * time * 16 + 1; + + case Easing.InSine: + return 1 - Math.Cos(time * Math.PI * .5); + case Easing.OutSine: + return Math.Sin(time * Math.PI * .5); + case Easing.InOutSine: + return .5 - .5 * Math.Cos(Math.PI * time); + + case Easing.InExpo: + return Math.Pow(2, 10 * (time - 1)); + case Easing.OutExpo: + return -Math.Pow(2, -10 * time) + 1; + case Easing.InOutExpo: + if (time < .5) return .5 * Math.Pow(2, 20 * time - 10); + return 1 - .5 * Math.Pow(2, -20 * time + 10); + + case Easing.InCirc: + return 1 - Math.Sqrt(1 - time * time); + case Easing.OutCirc: + return Math.Sqrt(1 - --time * time); + case Easing.InOutCirc: + if ((time *= 2) < 1) return .5 - .5 * Math.Sqrt(1 - time * time); + return .5 * Math.Sqrt(1 - (time -= 2) * time) + .5; + + case Easing.InElastic: + return -Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 - time) * elastic_const); + case Easing.OutElastic: + return Math.Pow(2, -10 * time) * Math.Sin((time - elastic_const2) * elastic_const) + 1; + case Easing.OutElasticHalf: + return Math.Pow(2, -10 * time) * Math.Sin((.5 * time - elastic_const2) * elastic_const) + 1; + case Easing.OutElasticQuarter: + return Math.Pow(2, -10 * time) * Math.Sin((.25 * time - elastic_const2) * elastic_const) + 1; + case Easing.InOutElastic: + if ((time *= 2) < 1) + return -.5 * Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 * 1.5 - time) * elastic_const / 1.5); + return .5 * Math.Pow(2, -10 * --time) * Math.Sin((time - elastic_const2 * 1.5) * elastic_const / 1.5) + 1; + + case Easing.InBack: + return time * time * ((back_const + 1) * time - back_const); + case Easing.OutBack: + return --time * time * ((back_const + 1) * time + back_const) + 1; + case Easing.InOutBack: + if ((time *= 2) < 1) return .5 * time * time * ((back_const2 + 1) * time - back_const2); + return .5 * ((time -= 2) * time * ((back_const2 + 1) * time + back_const2) + 2); + + case Easing.InBounce: + time = 1 - time; + if (time < bounce_const) + return 1 - 7.5625 * time * time; + if (time < 2 * bounce_const) + return 1 - (7.5625 * (time -= 1.5 * bounce_const) * time + .75); + if (time < 2.5 * bounce_const) + return 1 - (7.5625 * (time -= 2.25 * bounce_const) * time + .9375); + return 1 - (7.5625 * (time -= 2.625 * bounce_const) * time + .984375); + case Easing.OutBounce: + if (time < bounce_const) + return 7.5625 * time * time; + if (time < 2 * bounce_const) + return 7.5625 * (time -= 1.5 * bounce_const) * time + .75; + if (time < 2.5 * bounce_const) + return 7.5625 * (time -= 2.25 * bounce_const) * time + .9375; + return 7.5625 * (time -= 2.625 * bounce_const) * time + .984375; + case Easing.InOutBounce: + if (time < .5) return .5 - .5 * ApplyEasing(Easing.OutBounce, 1 - time * 2); + return ApplyEasing(Easing.OutBounce, (time - .5) * 2) * .5 + .5; + + case Easing.OutPow10: + return --time * Math.Pow(time, 10) + 1; + } + } + } +} diff --git a/osu.Framework/MathUtils/NumberFormatter.cs b/osu.Framework/MathUtils/NumberFormatter.cs index 20ff0e433..f3a047d26 100644 --- a/osu.Framework/MathUtils/NumberFormatter.cs +++ b/osu.Framework/MathUtils/NumberFormatter.cs @@ -1,38 +1,38 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.MathUtils -{ - /// - /// Exposes functionality for formatting numbers. - /// - public static class NumberFormatter - { - /// - /// Prints the number with at most two decimal digits, followed by a magnitude suffic (k, M, G, T, etc.) depending on the magnitude of the number. If the number is - /// too large or small this will print the number using scientific notation instead. - /// - /// The number to print. - /// The number with at most two decimal digits, followed by a magnitude suffic (k, M, G, T, etc.) depending on the magnitude of the number. If the number is - /// too large or small this will print the number using scientific notation instead. - public static string PrintWithSiSuffix(double number) - { - // The logarithm is undefined for zero. - if (number == 0) - return "0"; - - var isNeg = number < 0; - number = Math.Abs(number); - var strs = new[] { "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" }; - int log1000 = (int)Math.Floor(Math.Log(number, 1000)); - int index = log1000 + 8; - - if (index < 0 || index >= strs.Length) - return $"{number:E}"; - - return $"{(isNeg ? "-" : "")}{number / Math.Pow(1000, log1000):G3}{strs[index]}"; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.MathUtils +{ + /// + /// Exposes functionality for formatting numbers. + /// + public static class NumberFormatter + { + /// + /// Prints the number with at most two decimal digits, followed by a magnitude suffic (k, M, G, T, etc.) depending on the magnitude of the number. If the number is + /// too large or small this will print the number using scientific notation instead. + /// + /// The number to print. + /// The number with at most two decimal digits, followed by a magnitude suffic (k, M, G, T, etc.) depending on the magnitude of the number. If the number is + /// too large or small this will print the number using scientific notation instead. + public static string PrintWithSiSuffix(double number) + { + // The logarithm is undefined for zero. + if (number == 0) + return "0"; + + var isNeg = number < 0; + number = Math.Abs(number); + var strs = new[] { "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" }; + int log1000 = (int)Math.Floor(Math.Log(number, 1000)); + int index = log1000 + 8; + + if (index < 0 || index >= strs.Length) + return $"{number:E}"; + + return $"{(isNeg ? "-" : "")}{number / Math.Pow(1000, log1000):G3}{strs[index]}"; + } + } +} diff --git a/osu.Framework/MathUtils/Precision.cs b/osu.Framework/MathUtils/Precision.cs index f9967c545..d912a559f 100644 --- a/osu.Framework/MathUtils/Precision.cs +++ b/osu.Framework/MathUtils/Precision.cs @@ -1,49 +1,49 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; - -namespace osu.Framework.MathUtils -{ - public static class Precision - { - public const float FLOAT_EPSILON = 1e-3f; - public const double DOUBLE_EPSILON = 1e-7; - - public static bool DefinitelyBigger(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) - { - return value1 - acceptableDifference > value2; - } - - public static bool DefinitelyBigger(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) - { - return value1 - acceptableDifference > value2; - } - - public static bool AlmostBigger(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) - { - return value1 > value2 - acceptableDifference; - } - - public static bool AlmostBigger(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) - { - return value1 > value2 - acceptableDifference; - } - - public static bool AlmostEquals(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) - { - return Math.Abs(value1 - value2) <= acceptableDifference; - } - - public static bool AlmostEquals(Vector2 value1, Vector2 value2, float acceptableDifference = FLOAT_EPSILON) - { - return AlmostEquals(value1.X, value2.X, acceptableDifference) && AlmostEquals(value1.Y, value2.Y, acceptableDifference); - } - - public static bool AlmostEquals(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) - { - return Math.Abs(value1 - value2) <= acceptableDifference; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; + +namespace osu.Framework.MathUtils +{ + public static class Precision + { + public const float FLOAT_EPSILON = 1e-3f; + public const double DOUBLE_EPSILON = 1e-7; + + public static bool DefinitelyBigger(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) + { + return value1 - acceptableDifference > value2; + } + + public static bool DefinitelyBigger(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) + { + return value1 - acceptableDifference > value2; + } + + public static bool AlmostBigger(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) + { + return value1 > value2 - acceptableDifference; + } + + public static bool AlmostBigger(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) + { + return value1 > value2 - acceptableDifference; + } + + public static bool AlmostEquals(float value1, float value2, float acceptableDifference = FLOAT_EPSILON) + { + return Math.Abs(value1 - value2) <= acceptableDifference; + } + + public static bool AlmostEquals(Vector2 value1, Vector2 value2, float acceptableDifference = FLOAT_EPSILON) + { + return AlmostEquals(value1.X, value2.X, acceptableDifference) && AlmostEquals(value1.Y, value2.Y, acceptableDifference); + } + + public static bool AlmostEquals(double value1, double value2, double acceptableDifference = DOUBLE_EPSILON) + { + return Math.Abs(value1 - value2) <= acceptableDifference; + } + } +} diff --git a/osu.Framework/MathUtils/RNG.cs b/osu.Framework/MathUtils/RNG.cs index 43fc1e646..c06ddfd3e 100644 --- a/osu.Framework/MathUtils/RNG.cs +++ b/osu.Framework/MathUtils/RNG.cs @@ -1,128 +1,128 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.MathUtils -{ - /// - /// Static utility class for random number generation. - /// - public static class RNG - { - // Base RNG. Maybe expose methods for re-seeding in the future? - private static readonly Random random = new Random(); - - /// - /// Returns a non-negative signed integer. - /// - /// A non-negative signed integer. - public static int Next() => random.Next(); - - /// - /// Returns a signed integer in the range [0,maxValue). - /// - /// The maximum value that should be returned (exclusive, the highest possible result is maxValue - 1). - /// A signed integer in the range [0,maxValue). - public static int Next(int maxValue) => random.Next(maxValue); - - /// - /// Returns a signed integer in the range [minValue,maxValue). - /// - /// The minimum value that should be returned (inclusive). - /// The maximum value that should be returned (exclusive, the highest possible result is maxValue - 1). - /// A signed integer in the range [minValue,maxValue). - public static int Next(int minValue, int maxValue) => random.Next(minValue, maxValue); - - /// - /// Returns a double-precision floating point number in the range [0,1). - /// - /// A double-precision floating point number in the range [0,1). - public static double NextDouble() => random.NextDouble(); - - /// - /// Returns a double-precision floating point number in the range [0,maxValue). - /// - /// The maximum value that should be returned (exclusive). - /// A double-precision floating point number in the range [0,maxValue). - public static double NextDouble(double maxValue) - { - if (maxValue < 0.0) - throw new ArgumentOutOfRangeException(nameof(maxValue), "The given maximum value must be greater than or equal to 0."); - - return random.NextDouble() * maxValue; - } - - /// - /// Returns a double-precision floating point number in the range [minValue,maxValue). - /// - /// The minimum value that should be returned (inclusive). - /// The maximum value that should be returned (exclusive). - /// A double-precision floating point number in the range [minValue,maxValue). - public static double NextDouble(double minValue, double maxValue) - { - if (minValue > maxValue) - throw new ArgumentOutOfRangeException(nameof(minValue), "The given minimum value must be less than or equal to the given maximum value."); - - return minValue + random.NextDouble() * (maxValue - minValue); - } - - /// - /// Returns a single-precision floating point number in the range [0,1). - /// - /// A single-precision floating point number in the range [0,1). - public static float NextSingle() => (float)NextDouble(); - - /// - /// Returns a single-precision floating point number in the range [0,maxValue). - /// - /// The maximum value that should be returned (exclusive). - /// A single-precision floating point number in the range [0,maxValue). - public static float NextSingle(float maxValue) - { - if (maxValue < 0.0f) - throw new ArgumentOutOfRangeException(nameof(maxValue), "The given maximum value must be greater than or equal to 0."); - - return NextSingle() * maxValue; - } - - /// - /// Returns a single-precision floating point number in the range [minValue,maxValue). - /// - /// The minimum value that should be returned (inclusive). - /// The maximum value that should be returned (exclusive). - /// A single-precision floating point number in the range [minValue,maxValue). - public static float NextSingle(float minValue, float maxValue) - { - if (minValue > maxValue) - throw new ArgumentOutOfRangeException(nameof(minValue), "The given minimum value must be less than or equal to the given maximum value."); - - return minValue + NextSingle() * (maxValue - minValue); - } - - /// - /// Returns true or false. The likelihood of true and false are determined by trueChance. - /// - /// The chance that the result is true (a value from 0.0 to 1.0). - /// True or false with the given probability. - public static bool NextBool(double trueChance = 0.5) => NextDouble() < trueChance; - - /// - /// Fills the given buffer with random bytes. - /// - /// The buffer that should be filled. - public static void NextBytes(byte[] buffer) => random.NextBytes(buffer); - - /// - /// Creates a new byte array with the given length and fills it with random values. - /// - /// The length the byte array should have. - /// The newly created byte array. - public static byte[] NextBytes(int length) - { - byte[] bytes = new byte[length]; - NextBytes(bytes); - return bytes; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.MathUtils +{ + /// + /// Static utility class for random number generation. + /// + public static class RNG + { + // Base RNG. Maybe expose methods for re-seeding in the future? + private static readonly Random random = new Random(); + + /// + /// Returns a non-negative signed integer. + /// + /// A non-negative signed integer. + public static int Next() => random.Next(); + + /// + /// Returns a signed integer in the range [0,maxValue). + /// + /// The maximum value that should be returned (exclusive, the highest possible result is maxValue - 1). + /// A signed integer in the range [0,maxValue). + public static int Next(int maxValue) => random.Next(maxValue); + + /// + /// Returns a signed integer in the range [minValue,maxValue). + /// + /// The minimum value that should be returned (inclusive). + /// The maximum value that should be returned (exclusive, the highest possible result is maxValue - 1). + /// A signed integer in the range [minValue,maxValue). + public static int Next(int minValue, int maxValue) => random.Next(minValue, maxValue); + + /// + /// Returns a double-precision floating point number in the range [0,1). + /// + /// A double-precision floating point number in the range [0,1). + public static double NextDouble() => random.NextDouble(); + + /// + /// Returns a double-precision floating point number in the range [0,maxValue). + /// + /// The maximum value that should be returned (exclusive). + /// A double-precision floating point number in the range [0,maxValue). + public static double NextDouble(double maxValue) + { + if (maxValue < 0.0) + throw new ArgumentOutOfRangeException(nameof(maxValue), "The given maximum value must be greater than or equal to 0."); + + return random.NextDouble() * maxValue; + } + + /// + /// Returns a double-precision floating point number in the range [minValue,maxValue). + /// + /// The minimum value that should be returned (inclusive). + /// The maximum value that should be returned (exclusive). + /// A double-precision floating point number in the range [minValue,maxValue). + public static double NextDouble(double minValue, double maxValue) + { + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue), "The given minimum value must be less than or equal to the given maximum value."); + + return minValue + random.NextDouble() * (maxValue - minValue); + } + + /// + /// Returns a single-precision floating point number in the range [0,1). + /// + /// A single-precision floating point number in the range [0,1). + public static float NextSingle() => (float)NextDouble(); + + /// + /// Returns a single-precision floating point number in the range [0,maxValue). + /// + /// The maximum value that should be returned (exclusive). + /// A single-precision floating point number in the range [0,maxValue). + public static float NextSingle(float maxValue) + { + if (maxValue < 0.0f) + throw new ArgumentOutOfRangeException(nameof(maxValue), "The given maximum value must be greater than or equal to 0."); + + return NextSingle() * maxValue; + } + + /// + /// Returns a single-precision floating point number in the range [minValue,maxValue). + /// + /// The minimum value that should be returned (inclusive). + /// The maximum value that should be returned (exclusive). + /// A single-precision floating point number in the range [minValue,maxValue). + public static float NextSingle(float minValue, float maxValue) + { + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue), "The given minimum value must be less than or equal to the given maximum value."); + + return minValue + NextSingle() * (maxValue - minValue); + } + + /// + /// Returns true or false. The likelihood of true and false are determined by trueChance. + /// + /// The chance that the result is true (a value from 0.0 to 1.0). + /// True or false with the given probability. + public static bool NextBool(double trueChance = 0.5) => NextDouble() < trueChance; + + /// + /// Fills the given buffer with random bytes. + /// + /// The buffer that should be filled. + public static void NextBytes(byte[] buffer) => random.NextBytes(buffer); + + /// + /// Creates a new byte array with the given length and fills it with random values. + /// + /// The length the byte array should have. + /// The newly created byte array. + public static byte[] NextBytes(int length) + { + byte[] bytes = new byte[length]; + NextBytes(bytes); + return bytes; + } + } +} diff --git a/osu.Framework/MathUtils/Validation.cs b/osu.Framework/MathUtils/Validation.cs index 9905429ed..b805fc3f8 100644 --- a/osu.Framework/MathUtils/Validation.cs +++ b/osu.Framework/MathUtils/Validation.cs @@ -1,43 +1,43 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using osu.Framework.Graphics; - -namespace osu.Framework.MathUtils -{ - public static class Validation - { - /// - /// Returns the exponent of a (single-precision) as byte. - /// - /// The to get the exponent from. - /// Returns a so it's a smaller data type (and faster to pass around). - /// The exponent (bit 2 to 8) of the single-point . - private static unsafe byte singleToExponentAsByte(float value) => (byte)(*(int*)&value >> 23); - - /// - /// Returns whether a value is not , or . - /// - /// - /// Is equivalent to ( || ), but with less overhead. - /// Whether the float is valid in our conditions. - public static bool IsFinite(float toCheck) => singleToExponentAsByte(toCheck) != byte.MaxValue; - - /// - /// Returns whether the two coordinates of a are not infinite or NaN. - /// For further information, see . - /// - /// The to check. - /// False if X or Y are Infinity or NaN, true otherwise. - public static bool IsFinite(Vector2 toCheck) => IsFinite(toCheck.X) && IsFinite(toCheck.Y); - - /// - /// Returns whether the components of a are not infinite or NaN. - /// For further information, see . - /// - /// The to check. - /// False if either component of are Infinity or NaN, true otherwise. - public static bool IsFinite(MarginPadding toCheck) => IsFinite(toCheck.Top) && IsFinite(toCheck.Bottom) && IsFinite(toCheck.Left) && IsFinite(toCheck.Right); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; + +namespace osu.Framework.MathUtils +{ + public static class Validation + { + /// + /// Returns the exponent of a (single-precision) as byte. + /// + /// The to get the exponent from. + /// Returns a so it's a smaller data type (and faster to pass around). + /// The exponent (bit 2 to 8) of the single-point . + private static unsafe byte singleToExponentAsByte(float value) => (byte)(*(int*)&value >> 23); + + /// + /// Returns whether a value is not , or . + /// + /// + /// Is equivalent to ( || ), but with less overhead. + /// Whether the float is valid in our conditions. + public static bool IsFinite(float toCheck) => singleToExponentAsByte(toCheck) != byte.MaxValue; + + /// + /// Returns whether the two coordinates of a are not infinite or NaN. + /// For further information, see . + /// + /// The to check. + /// False if X or Y are Infinity or NaN, true otherwise. + public static bool IsFinite(Vector2 toCheck) => IsFinite(toCheck.X) && IsFinite(toCheck.Y); + + /// + /// Returns whether the components of a are not infinite or NaN. + /// For further information, see . + /// + /// The to check. + /// False if either component of are Infinity or NaN, true otherwise. + public static bool IsFinite(MarginPadding toCheck) => IsFinite(toCheck.Top) && IsFinite(toCheck.Bottom) && IsFinite(toCheck.Left) && IsFinite(toCheck.Right); + } +} diff --git a/osu.Framework/Physics/IRigidBody.cs b/osu.Framework/Physics/IRigidBody.cs index 13eb83223..2883fe442 100644 --- a/osu.Framework/Physics/IRigidBody.cs +++ b/osu.Framework/Physics/IRigidBody.cs @@ -1,133 +1,133 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using OpenTK; -using osu.Framework.Graphics; - -namespace osu.Framework.Physics -{ - /// - /// Contains physical state and methods necessary for rigid body simulation. - /// - public interface IRigidBody : IDrawable - { - /// - /// The which is currently performing a simulation on this . - /// - Drawable Simulation { get; set; } - - /// - /// Controls how elastic the material is. A value of 1 means perfect elasticity - /// (kinetic energy is fully preserved). A value of 0 means all energy is absorbed - /// on collision, i.e. no rebound occurs at all. - /// - float Restitution { get; set; } - - /// - /// How much friction happens between objects. - /// - float FrictionCoefficient { get; set; } - - Vector2 Centre { get; set; } - - float RotationRadians { get; set; } - - float Mass { get; set; } - - Vector2 Velocity { get; } - - Vector2 Momentum { get; set; } - - float AngularVelocity { get; } - - float AngularMomentum { get; set; } - - float MomentOfInertia { get; } - - /// - /// Total velocity at a given location. Includes angular velocity. - /// - Vector2 VelocityAt(Vector2 pos); - - /// - /// Applies a given impulse attacking at a given position. - /// - void ApplyImpulse(Vector2 impulse, Vector2 pos); - - /// - /// Checks for and records all collisions with another body. If collisions were found, - /// their aggregate is handled. - /// - bool CheckAndHandleCollisionWith(IRigidBody other); - - /// - /// Performs an integration step over time. More precisely, updates the - /// physical state as dependent on time according to the forces and torques - /// acting on this body. - /// - void Integrate(Vector2 force, float torque, float dt); - - /// - /// Reads the positional and rotational state of this rigid body from its source. - /// - void ReadState(); - - /// - /// Applies the positional and rotational state of this rigid body to its source. - /// - void ApplyState(); - - /// - /// Whether the given screen-space position is contained within the rigid body. - /// - bool BodyContains(Vector2 screenSpacePos); - } - - /// - /// Helper extension methods operating on . - /// - public static class RigidBodyExtensions - { - /// - /// Helper function for code brevity in . - /// Can be moved into the function as a nested method once C# 7 is out. - /// - public static float ImpulseDenominator(this IRigidBody body, Vector2 pos, Vector2 normal) - { - Vector2 diff = pos - body.Centre; - float perpDot = Vector2.Dot(normal, diff.PerpendicularRight); - return 1.0f / body.Mass + perpDot * perpDot / body.MomentOfInertia; - } - - /// - /// Computes the impulse of a collision of 2 rigid bodies, given the other body, the impact position, - /// and the surface normal of this body at the impact position. - /// - public static Vector2 ComputeImpulse(this IRigidBody body, IRigidBody other, Vector2 pos, Vector2 normal) - { - Vector2 vrel = body.VelocityAt(pos) - other.VelocityAt(pos); - float vrelOrtho = -Vector2.Dot(vrel, normal); - - // We don't want to consider collisions where objects move away from each other. - // (Or with negligible velocity. Let repulsive forces handle these.) - if (vrelOrtho > -0.001f) - return Vector2.Zero; - - float impulseMagnitude = -(1.0f + body.Restitution) * vrelOrtho; - impulseMagnitude /= body.ImpulseDenominator(pos, normal) + other.ImpulseDenominator(pos, normal); - - //impulseMagnitude = Math.Max(impulseMagnitude - 0.01f, 0.0f); - - Vector2 impulse = -normal * impulseMagnitude; - - // Add "friction" to the impulse. We arbitrarily reduce the planar velocity relative to the impulse magnitude. - Vector2 vrelPlanar = vrel + vrelOrtho * normal; - float vrelPlanarLength = vrelPlanar.Length; - if (vrelPlanarLength > 0) - impulse -= vrelPlanar * Math.Min(impulseMagnitude * 0.05f * body.FrictionCoefficient * other.FrictionCoefficient / vrelPlanarLength, body.Mass); - - return impulse; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using OpenTK; +using osu.Framework.Graphics; + +namespace osu.Framework.Physics +{ + /// + /// Contains physical state and methods necessary for rigid body simulation. + /// + public interface IRigidBody : IDrawable + { + /// + /// The which is currently performing a simulation on this . + /// + Drawable Simulation { get; set; } + + /// + /// Controls how elastic the material is. A value of 1 means perfect elasticity + /// (kinetic energy is fully preserved). A value of 0 means all energy is absorbed + /// on collision, i.e. no rebound occurs at all. + /// + float Restitution { get; set; } + + /// + /// How much friction happens between objects. + /// + float FrictionCoefficient { get; set; } + + Vector2 Centre { get; set; } + + float RotationRadians { get; set; } + + float Mass { get; set; } + + Vector2 Velocity { get; } + + Vector2 Momentum { get; set; } + + float AngularVelocity { get; } + + float AngularMomentum { get; set; } + + float MomentOfInertia { get; } + + /// + /// Total velocity at a given location. Includes angular velocity. + /// + Vector2 VelocityAt(Vector2 pos); + + /// + /// Applies a given impulse attacking at a given position. + /// + void ApplyImpulse(Vector2 impulse, Vector2 pos); + + /// + /// Checks for and records all collisions with another body. If collisions were found, + /// their aggregate is handled. + /// + bool CheckAndHandleCollisionWith(IRigidBody other); + + /// + /// Performs an integration step over time. More precisely, updates the + /// physical state as dependent on time according to the forces and torques + /// acting on this body. + /// + void Integrate(Vector2 force, float torque, float dt); + + /// + /// Reads the positional and rotational state of this rigid body from its source. + /// + void ReadState(); + + /// + /// Applies the positional and rotational state of this rigid body to its source. + /// + void ApplyState(); + + /// + /// Whether the given screen-space position is contained within the rigid body. + /// + bool BodyContains(Vector2 screenSpacePos); + } + + /// + /// Helper extension methods operating on . + /// + public static class RigidBodyExtensions + { + /// + /// Helper function for code brevity in . + /// Can be moved into the function as a nested method once C# 7 is out. + /// + public static float ImpulseDenominator(this IRigidBody body, Vector2 pos, Vector2 normal) + { + Vector2 diff = pos - body.Centre; + float perpDot = Vector2.Dot(normal, diff.PerpendicularRight); + return 1.0f / body.Mass + perpDot * perpDot / body.MomentOfInertia; + } + + /// + /// Computes the impulse of a collision of 2 rigid bodies, given the other body, the impact position, + /// and the surface normal of this body at the impact position. + /// + public static Vector2 ComputeImpulse(this IRigidBody body, IRigidBody other, Vector2 pos, Vector2 normal) + { + Vector2 vrel = body.VelocityAt(pos) - other.VelocityAt(pos); + float vrelOrtho = -Vector2.Dot(vrel, normal); + + // We don't want to consider collisions where objects move away from each other. + // (Or with negligible velocity. Let repulsive forces handle these.) + if (vrelOrtho > -0.001f) + return Vector2.Zero; + + float impulseMagnitude = -(1.0f + body.Restitution) * vrelOrtho; + impulseMagnitude /= body.ImpulseDenominator(pos, normal) + other.ImpulseDenominator(pos, normal); + + //impulseMagnitude = Math.Max(impulseMagnitude - 0.01f, 0.0f); + + Vector2 impulse = -normal * impulseMagnitude; + + // Add "friction" to the impulse. We arbitrarily reduce the planar velocity relative to the impulse magnitude. + Vector2 vrelPlanar = vrel + vrelOrtho * normal; + float vrelPlanarLength = vrelPlanar.Length; + if (vrelPlanarLength > 0) + impulse -= vrelPlanar * Math.Min(impulseMagnitude * 0.05f * body.FrictionCoefficient * other.FrictionCoefficient / vrelPlanarLength, body.Mass); + + return impulse; + } + } +} diff --git a/osu.Framework/Physics/RigidBodyContainer.cs b/osu.Framework/Physics/RigidBodyContainer.cs index 15cfdb0f4..786a872e3 100644 --- a/osu.Framework/Physics/RigidBodyContainer.cs +++ b/osu.Framework/Physics/RigidBodyContainer.cs @@ -1,277 +1,277 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using System; -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; - -namespace osu.Framework.Physics -{ - /// - /// Contains physical state and methods necessary for rigid body simulation. - /// - public class RigidBodyContainer : Container, IRigidBody - where T : Drawable - { - public RigidBodyContainer() - { - // The code for rigid body simulation requires that the centre of rotation - // equals the centre of mass, which is the geometric centre in this case. - Origin = Anchor.Centre; - } - - public Drawable Simulation { get; set; } - - /// - /// Controls how elastic the material is. A value of 1 means perfect elasticity - /// (kinetic energy is fully preserved). A value of 0 means all energy is absorbed - /// on collision, i.e. no rebound occurs at all. - /// - public float Restitution { get; set; } = 0.7f; - - /// - /// How much friction happens between objects. - /// - public float FrictionCoefficient { get; set; } = 0.2f; - - public Vector2 Centre { get; set; } - - public float RotationRadians { get; set; } = 1; - - public virtual float Mass { get; set; } = 1; - - public Vector2 Velocity - { - get { return Momentum / Mass; } - set { Momentum = value * Mass; } - } - - public Vector2 Momentum { get; set; } - - public float AngularVelocity - { - get { return AngularMomentum / MomentOfInertia; } - set { AngularMomentum = value * MomentOfInertia; } - } - - public float AngularMomentum { get; set; } - - public float MomentOfInertia { get; private set; } - - /// - /// Total velocity at a given location. Includes angular velocity. - /// - public Vector2 VelocityAt(Vector2 pos) - { - Vector2 diff = pos - Centre; - - // Add orthogonal direction to rotation, scaled by distance from centre - // to the velocity of our centre of mass. - return Velocity + diff.PerpendicularLeft * AngularVelocity; - } - - /// - /// Contains discrete positions on the surface of this shape used for collision detection. - /// In the future this can be potentially replaced by closed-form solutions. - /// - protected List Vertices = new List(); - - /// - /// Normals corresponding to the positions inside . - /// - protected List Normals = new List(); - - protected Matrix3 ScreenToSimulationSpace => Simulation.DrawInfo.MatrixInverse; - - protected Matrix3 SimulationToScreenSpace => Simulation.DrawInfo.Matrix; - - /// - /// Computes the moment of inertia. - /// - protected float ComputeI() - { - Matrix3 mat = DrawInfo.Matrix * Parent.DrawInfo.MatrixInverse; - Vector2 size = DrawSize; - - // Inertial moment for a linearly transformed rectangle with a given size around its center. - return ( - (mat.M11 * mat.M11 + mat.M12 * mat.M12) * size.X * size.X + - (mat.M21 * mat.M21 + mat.M22 * mat.M22) * size.Y * size.Y - ) * Mass / 12; - } - - /// - /// Populates and . - /// - protected virtual void UpdateVertices() - { - Vertices.Clear(); - Normals.Clear(); - - float cornerRadius = CornerRadius; - - // Sides - RectangleF rect = DrawRectangle; - Vector2[] corners = { rect.TopLeft, rect.TopRight, rect.BottomRight, rect.BottomLeft }; - const int amount_side_steps = 2; - - for (int i = 0; i < 4; ++i) - { - Vector2 a = corners[i]; - Vector2 b = corners[(i + 1) % 4]; - Vector2 diff = b - a; - float length = diff.Length; - Vector2 dir = diff / length; - - float usableLength = Math.Max(length - 2 * cornerRadius, 0); - - Vector2 normal = (b - a).PerpendicularRight.Normalized(); - for (int j = 0; j < amount_side_steps; ++j) - { - Vertices.Add(a + dir * (cornerRadius + j * usableLength / (amount_side_steps - 1))); - Normals.Add(normal); - } - } - - const int amount_corner_steps = 10; - if (cornerRadius > 0) - { - // Rounded corners - Vector2[] offsets = { - new Vector2(cornerRadius, cornerRadius), - new Vector2(-cornerRadius, cornerRadius), - new Vector2(-cornerRadius, -cornerRadius), - new Vector2(cornerRadius, -cornerRadius), - }; - - for (int i = 0; i < 4; ++i) - { - Vector2 a = corners[i]; - - float startTheta = (i - 1) * (float)Math.PI / 2; - - for (int j = 0; j < amount_corner_steps; ++j) - { - float theta = startTheta + j * (float)Math.PI / (2 * (amount_corner_steps - 1)); - - Vector2 normal = new Vector2((float)Math.Sin(theta), (float)Math.Cos(theta)); - Vertices.Add(a + offsets[i] + normal * cornerRadius); - Normals.Add(normal); - } - } - } - - // To simulation space - Matrix3 mat = DrawInfo.Matrix * ScreenToSimulationSpace; - Matrix3 normMat = mat.Inverted(); - normMat.Transpose(); - - // Remove translation - normMat.M31 = normMat.M32 = normMat.M13 = normMat.M23 = 0; - Vector2 translation = Vector2Extensions.Transform(Vector2.Zero, normMat); - - for (int i = 0; i < Vertices.Count; ++i) - { - Vertices[i] = Vector2Extensions.Transform(Vertices[i], mat); - Normals[i] = (Vector2Extensions.Transform(Normals[i], normMat) - translation).Normalized(); - } - } - - /// - /// Applies a given impulse attacking at a given position. - /// - public virtual void ApplyImpulse(Vector2 impulse, Vector2 pos) - { - // Offset to our centre of mass. Required to obtain torque - Vector2 diff = pos - Centre; - - Momentum += impulse; - - // Cross product between impulse and offset to centre. - // If they are orthogonal, then the effect on angular momentum is maximized. - // Intuitively, think of hitting something head-on vs hitting it on the far edge. - // The first case will not introduce any rotational movement, whereas the latter - // will. - AngularMomentum += diff.X * impulse.Y - diff.Y * impulse.X; - } - - /// - /// Checks for and records all collisions with another body. If collisions were found, - /// their aggregate is handled. - /// - public bool CheckAndHandleCollisionWith(IRigidBody other) - { - if (!other.ScreenSpaceDrawQuad.AABB.IntersectsWith(ScreenSpaceDrawQuad.AABB)) - return false; - - bool didCollide = false; - for (int i = 0; i < Vertices.Count; ++i) - { - if (other.BodyContains(Vector2Extensions.Transform(Vertices[i], SimulationToScreenSpace))) - { - // Compute both impulse responses _before_ applying them, such that - // they do not influence each other. - Vector2 impulse = this.ComputeImpulse(other, Vertices[i], Normals[i]); - Vector2 impulseOther = other.ComputeImpulse(this, Vertices[i], -Normals[i]); - - ApplyImpulse(impulse, Vertices[i]); - other.ApplyImpulse(impulseOther, Vertices[i]); - - didCollide = true; - } - } - - return didCollide; - } - - /// - /// Performs an integration step over time. More precisely, updates the - /// physical state as dependent on time according to the forces and torques - /// acting on this body. - /// - public void Integrate(Vector2 force, float torque, float dt) - { - Vector2 vPrev = Velocity; - float wPrev = AngularVelocity; - - // Update momenta - Momentum += dt * force; - AngularMomentum += dt * torque; - - // Update position and rotation given _previous_ velocities. This is a symplectic integration technique, which conserves energy. - Centre += dt * vPrev; - RotationRadians += dt * wPrev; - } - - /// - /// Reads the positional and rotational state of this rigid body from its source. - /// - public void ReadState() - { - Matrix3 mat = Parent.DrawInfo.Matrix * ScreenToSimulationSpace; - Centre = Vector2Extensions.Transform(BoundingBox.Centre, mat); - RotationRadians = MathHelper.DegreesToRadians(Rotation); // TODO: Fix rotations - - MomentOfInertia = ComputeI(); - UpdateVertices(); - } - - /// - /// Applies the positional and rotational state of this rigid body to its source. - /// - public virtual void ApplyState() - { - Matrix3 mat = SimulationToScreenSpace * Parent.DrawInfo.MatrixInverse; - Position = Vector2Extensions.Transform(Centre, mat) + (Position - BoundingBox.Centre); - Rotation = MathHelper.RadiansToDegrees(RotationRadians); // TODO: Fix rotations - } - - /// - /// Whether the given screen-space position is contained within the rigid body. - /// - public virtual bool BodyContains(Vector2 screenSpacePos) => Contains(screenSpacePos); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Physics +{ + /// + /// Contains physical state and methods necessary for rigid body simulation. + /// + public class RigidBodyContainer : Container, IRigidBody + where T : Drawable + { + public RigidBodyContainer() + { + // The code for rigid body simulation requires that the centre of rotation + // equals the centre of mass, which is the geometric centre in this case. + Origin = Anchor.Centre; + } + + public Drawable Simulation { get; set; } + + /// + /// Controls how elastic the material is. A value of 1 means perfect elasticity + /// (kinetic energy is fully preserved). A value of 0 means all energy is absorbed + /// on collision, i.e. no rebound occurs at all. + /// + public float Restitution { get; set; } = 0.7f; + + /// + /// How much friction happens between objects. + /// + public float FrictionCoefficient { get; set; } = 0.2f; + + public Vector2 Centre { get; set; } + + public float RotationRadians { get; set; } = 1; + + public virtual float Mass { get; set; } = 1; + + public Vector2 Velocity + { + get { return Momentum / Mass; } + set { Momentum = value * Mass; } + } + + public Vector2 Momentum { get; set; } + + public float AngularVelocity + { + get { return AngularMomentum / MomentOfInertia; } + set { AngularMomentum = value * MomentOfInertia; } + } + + public float AngularMomentum { get; set; } + + public float MomentOfInertia { get; private set; } + + /// + /// Total velocity at a given location. Includes angular velocity. + /// + public Vector2 VelocityAt(Vector2 pos) + { + Vector2 diff = pos - Centre; + + // Add orthogonal direction to rotation, scaled by distance from centre + // to the velocity of our centre of mass. + return Velocity + diff.PerpendicularLeft * AngularVelocity; + } + + /// + /// Contains discrete positions on the surface of this shape used for collision detection. + /// In the future this can be potentially replaced by closed-form solutions. + /// + protected List Vertices = new List(); + + /// + /// Normals corresponding to the positions inside . + /// + protected List Normals = new List(); + + protected Matrix3 ScreenToSimulationSpace => Simulation.DrawInfo.MatrixInverse; + + protected Matrix3 SimulationToScreenSpace => Simulation.DrawInfo.Matrix; + + /// + /// Computes the moment of inertia. + /// + protected float ComputeI() + { + Matrix3 mat = DrawInfo.Matrix * Parent.DrawInfo.MatrixInverse; + Vector2 size = DrawSize; + + // Inertial moment for a linearly transformed rectangle with a given size around its center. + return ( + (mat.M11 * mat.M11 + mat.M12 * mat.M12) * size.X * size.X + + (mat.M21 * mat.M21 + mat.M22 * mat.M22) * size.Y * size.Y + ) * Mass / 12; + } + + /// + /// Populates and . + /// + protected virtual void UpdateVertices() + { + Vertices.Clear(); + Normals.Clear(); + + float cornerRadius = CornerRadius; + + // Sides + RectangleF rect = DrawRectangle; + Vector2[] corners = { rect.TopLeft, rect.TopRight, rect.BottomRight, rect.BottomLeft }; + const int amount_side_steps = 2; + + for (int i = 0; i < 4; ++i) + { + Vector2 a = corners[i]; + Vector2 b = corners[(i + 1) % 4]; + Vector2 diff = b - a; + float length = diff.Length; + Vector2 dir = diff / length; + + float usableLength = Math.Max(length - 2 * cornerRadius, 0); + + Vector2 normal = (b - a).PerpendicularRight.Normalized(); + for (int j = 0; j < amount_side_steps; ++j) + { + Vertices.Add(a + dir * (cornerRadius + j * usableLength / (amount_side_steps - 1))); + Normals.Add(normal); + } + } + + const int amount_corner_steps = 10; + if (cornerRadius > 0) + { + // Rounded corners + Vector2[] offsets = { + new Vector2(cornerRadius, cornerRadius), + new Vector2(-cornerRadius, cornerRadius), + new Vector2(-cornerRadius, -cornerRadius), + new Vector2(cornerRadius, -cornerRadius), + }; + + for (int i = 0; i < 4; ++i) + { + Vector2 a = corners[i]; + + float startTheta = (i - 1) * (float)Math.PI / 2; + + for (int j = 0; j < amount_corner_steps; ++j) + { + float theta = startTheta + j * (float)Math.PI / (2 * (amount_corner_steps - 1)); + + Vector2 normal = new Vector2((float)Math.Sin(theta), (float)Math.Cos(theta)); + Vertices.Add(a + offsets[i] + normal * cornerRadius); + Normals.Add(normal); + } + } + } + + // To simulation space + Matrix3 mat = DrawInfo.Matrix * ScreenToSimulationSpace; + Matrix3 normMat = mat.Inverted(); + normMat.Transpose(); + + // Remove translation + normMat.M31 = normMat.M32 = normMat.M13 = normMat.M23 = 0; + Vector2 translation = Vector2Extensions.Transform(Vector2.Zero, normMat); + + for (int i = 0; i < Vertices.Count; ++i) + { + Vertices[i] = Vector2Extensions.Transform(Vertices[i], mat); + Normals[i] = (Vector2Extensions.Transform(Normals[i], normMat) - translation).Normalized(); + } + } + + /// + /// Applies a given impulse attacking at a given position. + /// + public virtual void ApplyImpulse(Vector2 impulse, Vector2 pos) + { + // Offset to our centre of mass. Required to obtain torque + Vector2 diff = pos - Centre; + + Momentum += impulse; + + // Cross product between impulse and offset to centre. + // If they are orthogonal, then the effect on angular momentum is maximized. + // Intuitively, think of hitting something head-on vs hitting it on the far edge. + // The first case will not introduce any rotational movement, whereas the latter + // will. + AngularMomentum += diff.X * impulse.Y - diff.Y * impulse.X; + } + + /// + /// Checks for and records all collisions with another body. If collisions were found, + /// their aggregate is handled. + /// + public bool CheckAndHandleCollisionWith(IRigidBody other) + { + if (!other.ScreenSpaceDrawQuad.AABB.IntersectsWith(ScreenSpaceDrawQuad.AABB)) + return false; + + bool didCollide = false; + for (int i = 0; i < Vertices.Count; ++i) + { + if (other.BodyContains(Vector2Extensions.Transform(Vertices[i], SimulationToScreenSpace))) + { + // Compute both impulse responses _before_ applying them, such that + // they do not influence each other. + Vector2 impulse = this.ComputeImpulse(other, Vertices[i], Normals[i]); + Vector2 impulseOther = other.ComputeImpulse(this, Vertices[i], -Normals[i]); + + ApplyImpulse(impulse, Vertices[i]); + other.ApplyImpulse(impulseOther, Vertices[i]); + + didCollide = true; + } + } + + return didCollide; + } + + /// + /// Performs an integration step over time. More precisely, updates the + /// physical state as dependent on time according to the forces and torques + /// acting on this body. + /// + public void Integrate(Vector2 force, float torque, float dt) + { + Vector2 vPrev = Velocity; + float wPrev = AngularVelocity; + + // Update momenta + Momentum += dt * force; + AngularMomentum += dt * torque; + + // Update position and rotation given _previous_ velocities. This is a symplectic integration technique, which conserves energy. + Centre += dt * vPrev; + RotationRadians += dt * wPrev; + } + + /// + /// Reads the positional and rotational state of this rigid body from its source. + /// + public void ReadState() + { + Matrix3 mat = Parent.DrawInfo.Matrix * ScreenToSimulationSpace; + Centre = Vector2Extensions.Transform(BoundingBox.Centre, mat); + RotationRadians = MathHelper.DegreesToRadians(Rotation); // TODO: Fix rotations + + MomentOfInertia = ComputeI(); + UpdateVertices(); + } + + /// + /// Applies the positional and rotational state of this rigid body to its source. + /// + public virtual void ApplyState() + { + Matrix3 mat = SimulationToScreenSpace * Parent.DrawInfo.MatrixInverse; + Position = Vector2Extensions.Transform(Centre, mat) + (Position - BoundingBox.Centre); + Rotation = MathHelper.RadiansToDegrees(RotationRadians); // TODO: Fix rotations + } + + /// + /// Whether the given screen-space position is contained within the rigid body. + /// + public virtual bool BodyContains(Vector2 screenSpacePos) => Contains(screenSpacePos); + } +} diff --git a/osu.Framework/Physics/RigidBodySimulation.cs b/osu.Framework/Physics/RigidBodySimulation.cs index 6cb5c1f55..172f26588 100644 --- a/osu.Framework/Physics/RigidBodySimulation.cs +++ b/osu.Framework/Physics/RigidBodySimulation.cs @@ -1,110 +1,110 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK; -using osu.Framework.Graphics; -using System.Collections.Generic; -using System; - -namespace osu.Framework.Physics -{ - /// - /// Applies rigid body simulation to all children. - /// - public class RigidBodySimulation : RigidBodySimulation - { - } - - /// - /// Applies rigid body simulation to all children. - /// - public class RigidBodySimulation : RigidBodyContainer> - where T : Drawable - { - public RigidBodySimulation() - { - // For this special case of a rigid body container we don't ware about the origin - // since no rotation can ever happen. Therefore, let's revert to the usual default. - Origin = Anchor.TopLeft; - } - - /// - /// The relative speed at which the simulation runs. A value of 1 means it runs as fast - /// as the rest of the game. - /// - public float SimulationSpeed = 1; - - private readonly List toSimulate = new List(); - - /// - /// Advances the simulation by a time step. - /// - /// The time step to advance the simulation by. - private void integrate(float dt) - { - toSimulate.Clear(); - - foreach (var d in Children) - toSimulate.Add(d); - toSimulate.Add(this); - - // Read the new state from each drawable in question - foreach (var d in toSimulate) - { - d.Simulation = this; - d.ReadState(); - } - - // Handle collisions between each pair of bodies. - foreach (var d in toSimulate) - foreach (var other in toSimulate) - if (other != d) - d.CheckAndHandleCollisionWith(other); - - // Advance the simulation by the given time step for each body and - // apply the state to each drawable in question. - foreach (var d in toSimulate) - { - d.Integrate(new Vector2(0, 981f * d.Mass), 0, dt); - d.ApplyState(); - } - } - - protected override void UpdateAfterChildren() - { - integrate(SimulationSpeed * (float)Time.Elapsed / 1000); - base.UpdateAfterChildren(); - } - - public override float Mass - { - get { return float.MaxValue; } - set { throw new InvalidOperationException($"May not set the {nameof(Mass)} of a {nameof(RigidBodySimulation)}."); } - } - - protected override void UpdateVertices() - { - base.UpdateVertices(); - - // We want to behave like a hollow box, so all normals need to point inward. - for (int i = 0; i < Normals.Count; ++i) - Normals[i] = -Normals[i]; - } - - // For hollow-box behavior we want to be contained whenever we are _not_ inside - public override bool BodyContains(Vector2 screenSpacePos) => !base.BodyContains(screenSpacePos); - - public override void ApplyImpulse(Vector2 impulse, Vector2 pos) - { - // Do nothing. We want to be immovable. - } - - public override void ApplyState() - { - base.ApplyState(); - - Momentum = Vector2.Zero; - AngularMomentum = 0; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using System.Collections.Generic; +using System; + +namespace osu.Framework.Physics +{ + /// + /// Applies rigid body simulation to all children. + /// + public class RigidBodySimulation : RigidBodySimulation + { + } + + /// + /// Applies rigid body simulation to all children. + /// + public class RigidBodySimulation : RigidBodyContainer> + where T : Drawable + { + public RigidBodySimulation() + { + // For this special case of a rigid body container we don't ware about the origin + // since no rotation can ever happen. Therefore, let's revert to the usual default. + Origin = Anchor.TopLeft; + } + + /// + /// The relative speed at which the simulation runs. A value of 1 means it runs as fast + /// as the rest of the game. + /// + public float SimulationSpeed = 1; + + private readonly List toSimulate = new List(); + + /// + /// Advances the simulation by a time step. + /// + /// The time step to advance the simulation by. + private void integrate(float dt) + { + toSimulate.Clear(); + + foreach (var d in Children) + toSimulate.Add(d); + toSimulate.Add(this); + + // Read the new state from each drawable in question + foreach (var d in toSimulate) + { + d.Simulation = this; + d.ReadState(); + } + + // Handle collisions between each pair of bodies. + foreach (var d in toSimulate) + foreach (var other in toSimulate) + if (other != d) + d.CheckAndHandleCollisionWith(other); + + // Advance the simulation by the given time step for each body and + // apply the state to each drawable in question. + foreach (var d in toSimulate) + { + d.Integrate(new Vector2(0, 981f * d.Mass), 0, dt); + d.ApplyState(); + } + } + + protected override void UpdateAfterChildren() + { + integrate(SimulationSpeed * (float)Time.Elapsed / 1000); + base.UpdateAfterChildren(); + } + + public override float Mass + { + get { return float.MaxValue; } + set { throw new InvalidOperationException($"May not set the {nameof(Mass)} of a {nameof(RigidBodySimulation)}."); } + } + + protected override void UpdateVertices() + { + base.UpdateVertices(); + + // We want to behave like a hollow box, so all normals need to point inward. + for (int i = 0; i < Normals.Count; ++i) + Normals[i] = -Normals[i]; + } + + // For hollow-box behavior we want to be contained whenever we are _not_ inside + public override bool BodyContains(Vector2 screenSpacePos) => !base.BodyContains(screenSpacePos); + + public override void ApplyImpulse(Vector2 impulse, Vector2 pos) + { + // Do nothing. We want to be immovable. + } + + public override void ApplyState() + { + base.ApplyState(); + + Momentum = Vector2.Zero; + AngularMomentum = 0; + } + } +} diff --git a/osu.Framework/Platform/Architecture.cs b/osu.Framework/Platform/Architecture.cs index 8cab43ad5..b7f1a29b6 100644 --- a/osu.Framework/Platform/Architecture.cs +++ b/osu.Framework/Platform/Architecture.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; - -namespace osu.Framework.Platform -{ - internal static class Architecture - { - public static string NativeIncludePath => $@"{Environment.CurrentDirectory}/{arch}/"; - private static string arch => Is64Bit ? @"x64" : @"x86"; - - internal static bool Is64Bit => IntPtr.Size == 8; - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetDllDirectory(string lpPathName); - - internal static void SetIncludePath() - { - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - SetDllDirectory(NativeIncludePath); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform +{ + internal static class Architecture + { + public static string NativeIncludePath => $@"{Environment.CurrentDirectory}/{arch}/"; + private static string arch => Is64Bit ? @"x64" : @"x86"; + + internal static bool Is64Bit => IntPtr.Size == 8; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetDllDirectory(string lpPathName); + + internal static void SetIncludePath() + { + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + SetDllDirectory(NativeIncludePath); + } + } +} diff --git a/osu.Framework/Platform/Clipboard.cs b/osu.Framework/Platform/Clipboard.cs index a84b71340..f3de2d210 100644 --- a/osu.Framework/Platform/Clipboard.cs +++ b/osu.Framework/Platform/Clipboard.cs @@ -1,12 +1,12 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Platform -{ - public abstract class Clipboard - { - public abstract string GetText(); - - public abstract void SetText(string selectedText); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Platform +{ + public abstract class Clipboard + { + public abstract string GetText(); + + public abstract void SetText(string selectedText); + } +} diff --git a/osu.Framework/Platform/DesktopGameHost.cs b/osu.Framework/Platform/DesktopGameHost.cs index 2249ea36f..fb2e11b15 100644 --- a/osu.Framework/Platform/DesktopGameHost.cs +++ b/osu.Framework/Platform/DesktopGameHost.cs @@ -1,136 +1,136 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Input; -using osu.Framework.Input.Handlers; -using osu.Framework.Input.Handlers.Keyboard; -using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Logging; - -namespace osu.Framework.Platform -{ - public abstract class DesktopGameHost : GameHost - { - private readonly TcpIpcProvider ipcProvider; - private readonly Task ipcTask; - - protected DesktopGameHost(string gameName = @"", bool bindIPCPort = false) - : base(gameName) - { - //todo: yeah. - Architecture.SetIncludePath(); - - foreach (string a in Environment.GetCommandLineArgs()) - { - switch (a) - { - case @"--reload-on-change": - ensureShadowCopy(); - break; - } - } - - if (bindIPCPort) - { - ipcProvider = new TcpIpcProvider(); - IsPrimaryInstance = ipcProvider.Bind(); - if (IsPrimaryInstance) - { - ipcProvider.MessageReceived += OnMessageReceived; - ipcTask = Task.Factory.StartNew(ipcProvider.StartAsync, TaskCreationOptions.LongRunning); - } - } - - Logger.Storage = Storage.GetStorageForDirectory("logs"); - } - - /// - /// Copy ourselves to a temporary path and watch for updates to the original assembly. - /// - private void ensureShadowCopy() - { - string exe = System.Reflection.Assembly.GetEntryAssembly().Location; - - Debug.Assert(exe != null); - - // ReSharper disable once PossibleNullReferenceException - if (exe.Contains(@"_shadow")) - { - //we are already running a shadow copy. monitor the original executable path for changes. - exe = exe.Replace(@"_shadow", @""); - - DateTime originalTime = new FileInfo(exe).LastWriteTimeUtc; - - Task.Run(() => - { - while (new FileInfo(exe).LastWriteTimeUtc == originalTime) - Thread.Sleep(1000); - - Process.Start(exe, @"--reload-on-change"); - Environment.Exit(0); - }); - - return; - } - - string shadowExe = exe.Replace(@".exe", @"_shadow.exe"); - - int attempts = 5; - while (attempts-- > 0) - { - try - { - File.Copy(exe, shadowExe, true); - break; - } - catch - { - Thread.Sleep(200); - } - } - - Process.Start(shadowExe, @"--reload-on-change"); - Environment.Exit(0); - } - - public override ITextInputSource GetTextInput() => Window == null ? null : new GameWindowTextInput(Window); - - protected override IEnumerable CreateAvailableInputHandlers() - { - var defaultEnabled = new InputHandler[] - { - new OpenTKMouseHandler(), - new OpenTKKeyboardHandler(), - }; - - var defaultDisabled = new InputHandler[] - { - new OpenTKRawMouseHandler(), - }; - - foreach (var h in defaultDisabled) - h.Enabled.Value = false; - - return defaultEnabled.Concat(defaultDisabled); - } - - public override async Task SendMessageAsync(IpcMessage message) - { - await ipcProvider.SendMessageAsync(message); - } - - protected override void Dispose(bool isDisposing) - { - ipcProvider?.Dispose(); - ipcTask?.Wait(50); - base.Dispose(isDisposing); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Keyboard; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Logging; + +namespace osu.Framework.Platform +{ + public abstract class DesktopGameHost : GameHost + { + private readonly TcpIpcProvider ipcProvider; + private readonly Task ipcTask; + + protected DesktopGameHost(string gameName = @"", bool bindIPCPort = false) + : base(gameName) + { + //todo: yeah. + Architecture.SetIncludePath(); + + foreach (string a in Environment.GetCommandLineArgs()) + { + switch (a) + { + case @"--reload-on-change": + ensureShadowCopy(); + break; + } + } + + if (bindIPCPort) + { + ipcProvider = new TcpIpcProvider(); + IsPrimaryInstance = ipcProvider.Bind(); + if (IsPrimaryInstance) + { + ipcProvider.MessageReceived += OnMessageReceived; + ipcTask = Task.Factory.StartNew(ipcProvider.StartAsync, TaskCreationOptions.LongRunning); + } + } + + Logger.Storage = Storage.GetStorageForDirectory("logs"); + } + + /// + /// Copy ourselves to a temporary path and watch for updates to the original assembly. + /// + private void ensureShadowCopy() + { + string exe = System.Reflection.Assembly.GetEntryAssembly().Location; + + Debug.Assert(exe != null); + + // ReSharper disable once PossibleNullReferenceException + if (exe.Contains(@"_shadow")) + { + //we are already running a shadow copy. monitor the original executable path for changes. + exe = exe.Replace(@"_shadow", @""); + + DateTime originalTime = new FileInfo(exe).LastWriteTimeUtc; + + Task.Run(() => + { + while (new FileInfo(exe).LastWriteTimeUtc == originalTime) + Thread.Sleep(1000); + + Process.Start(exe, @"--reload-on-change"); + Environment.Exit(0); + }); + + return; + } + + string shadowExe = exe.Replace(@".exe", @"_shadow.exe"); + + int attempts = 5; + while (attempts-- > 0) + { + try + { + File.Copy(exe, shadowExe, true); + break; + } + catch + { + Thread.Sleep(200); + } + } + + Process.Start(shadowExe, @"--reload-on-change"); + Environment.Exit(0); + } + + public override ITextInputSource GetTextInput() => Window == null ? null : new GameWindowTextInput(Window); + + protected override IEnumerable CreateAvailableInputHandlers() + { + var defaultEnabled = new InputHandler[] + { + new OpenTKMouseHandler(), + new OpenTKKeyboardHandler(), + }; + + var defaultDisabled = new InputHandler[] + { + new OpenTKRawMouseHandler(), + }; + + foreach (var h in defaultDisabled) + h.Enabled.Value = false; + + return defaultEnabled.Concat(defaultDisabled); + } + + public override async Task SendMessageAsync(IpcMessage message) + { + await ipcProvider.SendMessageAsync(message); + } + + protected override void Dispose(bool isDisposing) + { + ipcProvider?.Dispose(); + ipcTask?.Wait(50); + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Framework/Platform/DesktopGameWindow.cs b/osu.Framework/Platform/DesktopGameWindow.cs index 37cd4cbcf..c68a0ae6a 100644 --- a/osu.Framework/Platform/DesktopGameWindow.cs +++ b/osu.Framework/Platform/DesktopGameWindow.cs @@ -1,201 +1,201 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Drawing; -using System.IO; -using osu.Framework.Configuration; -using osu.Framework.Input; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Platform -{ - public abstract class DesktopGameWindow : GameWindow - { - private const int default_width = 1366; - private const int default_height = 768; - - private readonly BindableInt widthFullscreen = new BindableInt(); - private readonly BindableInt heightFullscreen = new BindableInt(); - private readonly BindableInt width = new BindableInt(); - private readonly BindableInt height = new BindableInt(); - - private readonly BindableDouble windowPositionX = new BindableDouble(); - private readonly BindableDouble windowPositionY = new BindableDouble(); - - public readonly Bindable WindowMode = new Bindable(); - - public readonly Bindable ConfineMouseMode = new Bindable(); - - internal override IGraphicsContext Context => Implementation.Context; - - protected new OpenTK.GameWindow Implementation => (OpenTK.GameWindow)base.Implementation; - - public readonly BindableBool MapAbsoluteInputToWindow = new BindableBool(); - - protected DesktopGameWindow() - : base(default_width, default_height) - { - Resize += OnResize; - Move += OnMove; - } - - public virtual void SetIconFromStream(Stream stream) { } - - public override void SetupWindow(FrameworkConfigManager config) - { - config.BindWith(FrameworkSetting.WidthFullscreen, widthFullscreen); - config.BindWith(FrameworkSetting.HeightFullscreen, heightFullscreen); - - config.BindWith(FrameworkSetting.Width, width); - config.BindWith(FrameworkSetting.Height, height); - - config.BindWith(FrameworkSetting.WindowedPositionX, windowPositionX); - config.BindWith(FrameworkSetting.WindowedPositionY, windowPositionY); - - config.BindWith(FrameworkSetting.ConfineMouseMode, ConfineMouseMode); - - config.BindWith(FrameworkSetting.MapAbsoluteInputToWindow, MapAbsoluteInputToWindow); - - ConfineMouseMode.ValueChanged += confineMouseMode_ValueChanged; - ConfineMouseMode.TriggerChange(); - - config.BindWith(FrameworkSetting.WindowMode, WindowMode); - - WindowMode.ValueChanged += windowMode_ValueChanged; - WindowMode.TriggerChange(); - - Exited += onExit; - } - - protected void OnResize(object sender, EventArgs e) - { - if (ClientSize.IsEmpty) return; - - switch (WindowMode.Value) - { - case Configuration.WindowMode.Windowed: - width.Value = ClientSize.Width; - height.Value = ClientSize.Height; - break; - } - } - - protected void OnMove(object sender, EventArgs e) - { - // The game is windowed and the whole window is on the screen (it is not minimized or moved outside of the screen) - if (WindowMode.Value == Configuration.WindowMode.Windowed - && Position.X > 0 && Position.X < 1 - && Position.Y > 0 && Position.Y < 1) - { - windowPositionX.Value = Position.X; - windowPositionY.Value = Position.Y; - } - } - - private void confineMouseMode_ValueChanged(ConfineMouseMode newValue) - { - bool confine = false; - - switch (newValue) - { - case Input.ConfineMouseMode.Fullscreen: - confine = WindowMode.Value != Configuration.WindowMode.Windowed; - break; - case Input.ConfineMouseMode.Always: - confine = true; - break; - } - - if (confine) - CursorState |= CursorState.Confined; - else - CursorState &= ~CursorState.Confined; - } - - private void windowMode_ValueChanged(WindowMode newMode) - { - switch (newMode) - { - case Configuration.WindowMode.Fullscreen: - DisplayResolution newResolution = DisplayDevice.Default.SelectResolution(widthFullscreen, heightFullscreen, DisplayDevice.Default.BitsPerPixel, DisplayDevice.Default.RefreshRate); - DisplayDevice.Default.ChangeResolution(newResolution); - - WindowState = WindowState.Fullscreen; - break; - case Configuration.WindowMode.Borderless: - DisplayDevice.Default.RestoreResolution(); - - WindowState = WindowState.Maximized; - WindowBorder = WindowBorder.Hidden; - - //must add 1 to enter borderless - ClientSize = new Size(DisplayDevice.Default.Bounds.Width + 1, DisplayDevice.Default.Bounds.Height + 1); - Position = Vector2.Zero; - break; - default: - DisplayDevice.Default.RestoreResolution(); - - WindowState = WindowState.Normal; - WindowBorder = WindowBorder.Resizable; - - ClientSize = new Size(width, height); - Position = new Vector2((float)windowPositionX, (float)windowPositionY); - break; - } - - ConfineMouseMode.TriggerChange(); - } - - private void onExit() - { - switch (WindowMode.Value) - { - case Configuration.WindowMode.Fullscreen: - widthFullscreen.Value = ClientSize.Width; - heightFullscreen.Value = ClientSize.Height; - break; - } - - DisplayDevice.Default.RestoreResolution(); - } - - public Vector2 Position - { - get - { - return new Vector2((float)Location.X / (DisplayDevice.Default.Width - Size.Width), - (float)Location.Y / (DisplayDevice.Default.Height - Size.Height)); - } - set - { - Location = new Point( - (int)Math.Round((DisplayDevice.Default.Width - Size.Width) * value.X), - (int)Math.Round((DisplayDevice.Default.Height - Size.Height) * value.Y)); - } - } - - public override void CycleMode() - { - switch (WindowMode.Value) - { - case Configuration.WindowMode.Windowed: - WindowMode.Value = Configuration.WindowMode.Borderless; - break; - case Configuration.WindowMode.Borderless: - WindowMode.Value = Configuration.WindowMode.Fullscreen; - break; - default: - WindowMode.Value = Configuration.WindowMode.Windowed; - break; - } - } - - public override VSyncMode VSync - { - get => Implementation.VSync; - set => Implementation.VSync = value; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Drawing; +using System.IO; +using osu.Framework.Configuration; +using osu.Framework.Input; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Platform +{ + public abstract class DesktopGameWindow : GameWindow + { + private const int default_width = 1366; + private const int default_height = 768; + + private readonly BindableInt widthFullscreen = new BindableInt(); + private readonly BindableInt heightFullscreen = new BindableInt(); + private readonly BindableInt width = new BindableInt(); + private readonly BindableInt height = new BindableInt(); + + private readonly BindableDouble windowPositionX = new BindableDouble(); + private readonly BindableDouble windowPositionY = new BindableDouble(); + + public readonly Bindable WindowMode = new Bindable(); + + public readonly Bindable ConfineMouseMode = new Bindable(); + + internal override IGraphicsContext Context => Implementation.Context; + + protected new OpenTK.GameWindow Implementation => (OpenTK.GameWindow)base.Implementation; + + public readonly BindableBool MapAbsoluteInputToWindow = new BindableBool(); + + protected DesktopGameWindow() + : base(default_width, default_height) + { + Resize += OnResize; + Move += OnMove; + } + + public virtual void SetIconFromStream(Stream stream) { } + + public override void SetupWindow(FrameworkConfigManager config) + { + config.BindWith(FrameworkSetting.WidthFullscreen, widthFullscreen); + config.BindWith(FrameworkSetting.HeightFullscreen, heightFullscreen); + + config.BindWith(FrameworkSetting.Width, width); + config.BindWith(FrameworkSetting.Height, height); + + config.BindWith(FrameworkSetting.WindowedPositionX, windowPositionX); + config.BindWith(FrameworkSetting.WindowedPositionY, windowPositionY); + + config.BindWith(FrameworkSetting.ConfineMouseMode, ConfineMouseMode); + + config.BindWith(FrameworkSetting.MapAbsoluteInputToWindow, MapAbsoluteInputToWindow); + + ConfineMouseMode.ValueChanged += confineMouseMode_ValueChanged; + ConfineMouseMode.TriggerChange(); + + config.BindWith(FrameworkSetting.WindowMode, WindowMode); + + WindowMode.ValueChanged += windowMode_ValueChanged; + WindowMode.TriggerChange(); + + Exited += onExit; + } + + protected void OnResize(object sender, EventArgs e) + { + if (ClientSize.IsEmpty) return; + + switch (WindowMode.Value) + { + case Configuration.WindowMode.Windowed: + width.Value = ClientSize.Width; + height.Value = ClientSize.Height; + break; + } + } + + protected void OnMove(object sender, EventArgs e) + { + // The game is windowed and the whole window is on the screen (it is not minimized or moved outside of the screen) + if (WindowMode.Value == Configuration.WindowMode.Windowed + && Position.X > 0 && Position.X < 1 + && Position.Y > 0 && Position.Y < 1) + { + windowPositionX.Value = Position.X; + windowPositionY.Value = Position.Y; + } + } + + private void confineMouseMode_ValueChanged(ConfineMouseMode newValue) + { + bool confine = false; + + switch (newValue) + { + case Input.ConfineMouseMode.Fullscreen: + confine = WindowMode.Value != Configuration.WindowMode.Windowed; + break; + case Input.ConfineMouseMode.Always: + confine = true; + break; + } + + if (confine) + CursorState |= CursorState.Confined; + else + CursorState &= ~CursorState.Confined; + } + + private void windowMode_ValueChanged(WindowMode newMode) + { + switch (newMode) + { + case Configuration.WindowMode.Fullscreen: + DisplayResolution newResolution = DisplayDevice.Default.SelectResolution(widthFullscreen, heightFullscreen, DisplayDevice.Default.BitsPerPixel, DisplayDevice.Default.RefreshRate); + DisplayDevice.Default.ChangeResolution(newResolution); + + WindowState = WindowState.Fullscreen; + break; + case Configuration.WindowMode.Borderless: + DisplayDevice.Default.RestoreResolution(); + + WindowState = WindowState.Maximized; + WindowBorder = WindowBorder.Hidden; + + //must add 1 to enter borderless + ClientSize = new Size(DisplayDevice.Default.Bounds.Width + 1, DisplayDevice.Default.Bounds.Height + 1); + Position = Vector2.Zero; + break; + default: + DisplayDevice.Default.RestoreResolution(); + + WindowState = WindowState.Normal; + WindowBorder = WindowBorder.Resizable; + + ClientSize = new Size(width, height); + Position = new Vector2((float)windowPositionX, (float)windowPositionY); + break; + } + + ConfineMouseMode.TriggerChange(); + } + + private void onExit() + { + switch (WindowMode.Value) + { + case Configuration.WindowMode.Fullscreen: + widthFullscreen.Value = ClientSize.Width; + heightFullscreen.Value = ClientSize.Height; + break; + } + + DisplayDevice.Default.RestoreResolution(); + } + + public Vector2 Position + { + get + { + return new Vector2((float)Location.X / (DisplayDevice.Default.Width - Size.Width), + (float)Location.Y / (DisplayDevice.Default.Height - Size.Height)); + } + set + { + Location = new Point( + (int)Math.Round((DisplayDevice.Default.Width - Size.Width) * value.X), + (int)Math.Round((DisplayDevice.Default.Height - Size.Height) * value.Y)); + } + } + + public override void CycleMode() + { + switch (WindowMode.Value) + { + case Configuration.WindowMode.Windowed: + WindowMode.Value = Configuration.WindowMode.Borderless; + break; + case Configuration.WindowMode.Borderless: + WindowMode.Value = Configuration.WindowMode.Fullscreen; + break; + default: + WindowMode.Value = Configuration.WindowMode.Windowed; + break; + } + } + + public override VSyncMode VSync + { + get => Implementation.VSync; + set => Implementation.VSync = value; + } + } +} diff --git a/osu.Framework/Platform/DesktopStorage.cs b/osu.Framework/Platform/DesktopStorage.cs index 3da7e8e3b..754f37988 100644 --- a/osu.Framework/Platform/DesktopStorage.cs +++ b/osu.Framework/Platform/DesktopStorage.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using System.IO; -using osu.Framework.IO.File; - -namespace osu.Framework.Platform -{ - public class DesktopStorage : Storage - { - public DesktopStorage(string baseName) - : base(baseName) - { - } - - protected override string LocateBasePath() => @"./"; //use current directory by default - - public override bool Exists(string path) => File.Exists(GetUsablePathFor(path)); - - public override bool ExistsDirectory(string path) => Directory.Exists(GetUsablePathFor(path)); - - public override void DeleteDirectory(string path) - { - path = GetUsablePathFor(path); - - // handles the case where the directory doesn't exist, which will throw a DirectoryNotFoundException. - if (Directory.Exists(path)) - Directory.Delete(path, true); - } - - public override void Delete(string path) => FileSafety.FileDelete(GetUsablePathFor(path)); - - public override string[] GetDirectories(string path) => Directory.GetDirectories(GetUsablePathFor(path)); - - public override void OpenInNativeExplorer() - { - Process.Start(GetUsablePathFor(string.Empty)); - } - - public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) - { - path = GetUsablePathFor(path, access != FileAccess.Read); - - if (string.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - - switch (access) - { - case FileAccess.Read: - if (!File.Exists(path)) return null; - return File.Open(path, FileMode.Open, access, FileShare.Read); - default: - return File.Open(path, mode, access); - } - } - - public override string GetDatabaseConnectionString(string name) - { - return string.Concat("Data Source=", GetUsablePathFor($@"{name}.db", true)); - } - - public override void DeleteDatabase(string name) => Delete($@"{name}.db"); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using System.IO; +using osu.Framework.IO.File; + +namespace osu.Framework.Platform +{ + public class DesktopStorage : Storage + { + public DesktopStorage(string baseName) + : base(baseName) + { + } + + protected override string LocateBasePath() => @"./"; //use current directory by default + + public override bool Exists(string path) => File.Exists(GetUsablePathFor(path)); + + public override bool ExistsDirectory(string path) => Directory.Exists(GetUsablePathFor(path)); + + public override void DeleteDirectory(string path) + { + path = GetUsablePathFor(path); + + // handles the case where the directory doesn't exist, which will throw a DirectoryNotFoundException. + if (Directory.Exists(path)) + Directory.Delete(path, true); + } + + public override void Delete(string path) => FileSafety.FileDelete(GetUsablePathFor(path)); + + public override string[] GetDirectories(string path) => Directory.GetDirectories(GetUsablePathFor(path)); + + public override void OpenInNativeExplorer() + { + Process.Start(GetUsablePathFor(string.Empty)); + } + + public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) + { + path = GetUsablePathFor(path, access != FileAccess.Read); + + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + switch (access) + { + case FileAccess.Read: + if (!File.Exists(path)) return null; + return File.Open(path, FileMode.Open, access, FileShare.Read); + default: + return File.Open(path, mode, access); + } + } + + public override string GetDatabaseConnectionString(string name) + { + return string.Concat("Data Source=", GetUsablePathFor($@"{name}.db", true)); + } + + public override void DeleteDatabase(string name) => Delete($@"{name}.db"); + } +} diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index 47d2fa6e9..04b74d302 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -1,731 +1,731 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Runtime; -using System.Runtime.ExceptionServices; -using System.Threading; -using System.Threading.Tasks; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Handlers; -using osu.Framework.Localisation; -using osu.Framework.Logging; -using osu.Framework.Statistics; -using osu.Framework.Threading; -using osu.Framework.Timing; -using osu.Framework.IO.File; -using Bitmap = System.Drawing.Bitmap; - -namespace osu.Framework.Platform -{ - public abstract class GameHost : IIpcHost, IDisposable - { - public GameWindow Window { get; protected set; } - - private readonly Toolkit toolkit; - - private FrameworkDebugConfigManager debugConfig; - - private FrameworkConfigManager config; - - public LocalisationEngine Localisation { get; private set; } - - private void setActive(bool isActive) - { - threads.ForEach(t => t.IsActive = isActive); - - activeGCMode.TriggerChange(); - - if (isActive) - Activated?.Invoke(); - else - Deactivated?.Invoke(); - } - - public bool IsActive => InputThread.IsActive; - - public bool IsPrimaryInstance { get; protected set; } = true; - - public event Action Activated; - public event Action Deactivated; - public event Func Exiting; - public event Action Exited; - - /// - /// An unhandled exception was thrown. Return true to ignore and continue running. - /// - public event Func ExceptionThrown; - - public event Action MessageReceived; - - protected void OnMessageReceived(IpcMessage message) => MessageReceived?.Invoke(message); - - public virtual Task SendMessageAsync(IpcMessage message) - { - throw new NotSupportedException("This platform does not implement IPC."); - } - - public virtual Clipboard GetClipboard() => null; - - protected abstract Storage GetStorage(string baseName); - - public Storage Storage { get; protected set; } - - /// - /// If capslock is enabled on the system, false if not overwritten by a subclass - /// - public virtual bool CapsLockEnabled => false; - - private readonly List threads; - - public IEnumerable Threads => threads; - - public void RegisterThread(GameThread t) - { - threads.Add(t); - t.Monitor.EnablePerformanceProfiling = performanceLogging; - } - - public GameThread DrawThread; - public GameThread UpdateThread; - public InputThread InputThread; - - private double maximumUpdateHz; - - public double MaximumUpdateHz - { - get { return maximumUpdateHz; } - - set { UpdateThread.ActiveHz = maximumUpdateHz = value; } - } - - private double maximumDrawHz; - - public double MaximumDrawHz - { - get { return maximumDrawHz; } - - set { DrawThread.ActiveHz = maximumDrawHz = value; } - } - - public double MaximumInactiveHz - { - get { return DrawThread.InactiveHz; } - - set - { - DrawThread.InactiveHz = value; - UpdateThread.InactiveHz = value; - } - } - - private PerformanceMonitor inputMonitor => InputThread.Monitor; - private PerformanceMonitor drawMonitor => DrawThread.Monitor; - - private readonly Lazy fullPathBacking = new Lazy(RuntimeInfo.GetFrameworkAssemblyPath); - - public string FullPath => fullPathBacking.Value; - - protected string Name { get; } - - public DependencyContainer Dependencies { get; } = new DependencyContainer(); - - protected GameHost(string gameName = @"") - { - toolkit = Toolkit.Init(); - - AppDomain.CurrentDomain.UnhandledException += exceptionHandler; - - FileSafety.DeleteCleanupDirectory(); - - Dependencies.CacheAs(this); - Dependencies.CacheAs(Storage = GetStorage(gameName)); - - Name = gameName; - Logger.GameIdentifier = gameName; - - threads = new List - { - (DrawThread = new DrawThread(DrawFrame) - { - OnThreadStart = DrawInitialize, - }), - (UpdateThread = new UpdateThread(UpdateFrame) - { - OnThreadStart = UpdateInitialize, - Monitor = { HandleGC = true }, - }), - (InputThread = new InputThread(null)), //never gets started. - }; - - var path = Path.GetDirectoryName(FullPath); - if (path != null) - Environment.CurrentDirectory = path; - } - - private void exceptionHandler(object sender, UnhandledExceptionEventArgs e) - { - var exception = (Exception)e.ExceptionObject; - - Logger.Error(exception, @"fatal error:", recursive: true); - - var exInfo = ExceptionDispatchInfo.Capture(exception); - - if (ExceptionThrown?.Invoke(exception) != true) - { - AppDomain.CurrentDomain.UnhandledException -= exceptionHandler; - - //we want to throw this exception on the input thread to interrupt window and also headless execution. - InputThread.Scheduler.Add(() => { exInfo.Throw(); }); - } - } - - protected virtual void OnActivated() => UpdateThread.Scheduler.Add(() => setActive(true)); - - protected virtual void OnDeactivated() => UpdateThread.Scheduler.Add(() => setActive(false)); - - /// true to cancel - protected virtual bool OnExitRequested() - { - if (executionState <= ExecutionState.Stopping) return false; - - bool? response = null; - - UpdateThread.Scheduler.Add(delegate { response = Exiting?.Invoke() == true; }); - - //wait for a potentially blocking response - while (!response.HasValue) - Thread.Sleep(1); - - if (response ?? false) - return true; - - Exit(); - return false; - } - - protected virtual void OnExited() - { - Exited?.Invoke(); - } - - protected TripleBuffer DrawRoots = new TripleBuffer(); - - protected virtual void UpdateInitialize() - { - //this was added due to the dependency on GLWrapper.MaxTextureSize begin initialised. - DrawThread.WaitUntilInitialized(); - } - - protected Container Root; - - protected virtual void UpdateFrame() - { - if (Root == null) return; - - if (Window?.WindowState != WindowState.Minimized) - Root.Size = Window != null ? new Vector2(Window.ClientSize.Width, Window.ClientSize.Height) : - new Vector2(config.Get(FrameworkSetting.Width), config.Get(FrameworkSetting.Height)); - - // Ensure we maintain a valid size for any children immediately scaling by the window size - Root.Size = Vector2.ComponentMax(Vector2.One, Root.Size); - - Root.UpdateSubTree(); - Root.UpdateSubTreeMasking(Root, Root.ScreenSpaceDrawQuad.AABBFloat); - - using (var buffer = DrawRoots.Get(UsageType.Write)) - buffer.Object = Root.GenerateDrawNodeSubtree(buffer.Index); - } - - protected virtual void DrawInitialize() - { - Window.MakeCurrent(); - GLWrapper.Initialize(this); - - setVSyncMode(); - - GLWrapper.Reset(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height)); - GLWrapper.ClearColour(Color4.Black); - } - - private long lastDrawFrameId; - - protected virtual void DrawFrame() - { - if (Root == null) - return; - - while (executionState > ExecutionState.Stopping) - { - using (var buffer = DrawRoots.Get(UsageType.Read)) - { - if (buffer?.Object == null || buffer.FrameId == lastDrawFrameId) - { - Thread.Sleep(1); - continue; - } - - using (drawMonitor.BeginCollecting(PerformanceCollectionType.GLReset)) - { - GLWrapper.Reset(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height)); - GLWrapper.ClearColour(Color4.Black); - } - - buffer.Object.Draw(null); - lastDrawFrameId = buffer.FrameId; - break; - } - } - - GLWrapper.FlushCurrentBatch(); - - using (drawMonitor.BeginCollecting(PerformanceCollectionType.SwapBuffer)) - { - Window.SwapBuffers(); - - if (Window.VSync == VSyncMode.On) - // without glFinish, vsync is basically unplayable due to the extra latency introduced. - // we will likely want to give the user control over this in the future as an advanced setting. - GL.Finish(); - } - } - - /// - /// Make a object from the current OpenTK screen buffer - /// - /// object - public async Task TakeScreenshotAsync() - { - if (Window == null) throw new NullReferenceException(nameof(Window)); - - var clientRectangle = new Rectangle(new Point(Window.ClientRectangle.X, Window.ClientRectangle.Y), new Size(Window.ClientSize.Width, Window.ClientSize.Height)); - - bool complete = false; - - var bitmap = new Bitmap(clientRectangle.Width, clientRectangle.Height); - BitmapData data = bitmap.LockBits(clientRectangle, ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); - - DrawThread.Scheduler.Add(() => - { - if (GraphicsContext.CurrentContext == null) - throw new GraphicsContextMissingException(); - - OpenTK.Graphics.OpenGL.GL.ReadPixels(0, 0, clientRectangle.Width, clientRectangle.Height, OpenTK.Graphics.OpenGL.PixelFormat.Bgr, OpenTK.Graphics.OpenGL.PixelType.UnsignedByte, data.Scan0); - complete = true; - }); - - await Task.Run(() => - { - while (!complete) - Thread.Sleep(50); - }); - - bitmap.UnlockBits(data); - bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY); - - return bitmap; - } - - private volatile ExecutionState executionState; - - /// - /// Schedules the game to exit in the next frame. - /// - public void Exit() - { - executionState = ExecutionState.Stopping; - InputThread.Scheduler.Add(exit, false); - } - - /// - /// Exits the game. This must always be called from . - /// - private void exit() - { - // exit() may be called without having been scheduled from Exit(), so ensure the correct exiting state - executionState = ExecutionState.Stopping; - Window?.Close(); - stopAllThreads(); - executionState = ExecutionState.Stopped; - } - - public void Run(Game game) - { - if (executionState != ExecutionState.Idle) - throw new InvalidOperationException("A game that has already been run cannot be restarted."); - - try - { - executionState = ExecutionState.Running; - - setupConfig(); - - if (Window != null) - { - Window.SetupWindow(config); - Window.Title = $@"osu!framework (running ""{Name}"")"; - } - - resetInputHandlers(); - - DrawThread.Start(); - UpdateThread.Start(); - - DrawThread.WaitUntilInitialized(); - bootstrapSceneGraph(game); - - frameSyncMode.TriggerChange(); - enabledInputHandlers.TriggerChange(); - - try - { - if (Window != null) - { - setActive(Window.Focused); - - Window.KeyDown += window_KeyDown; - - Window.ExitRequested += OnExitRequested; - Window.Exited += OnExited; - Window.FocusedChanged += delegate { setActive(Window.Focused); }; - - Window.UpdateFrame += delegate - { - inputPerformanceCollectionPeriod?.Dispose(); - InputThread.RunUpdate(); - inputPerformanceCollectionPeriod = inputMonitor.BeginCollecting(PerformanceCollectionType.WndProc); - }; - Window.Closed += delegate - { - //we need to ensure all threads have stopped before the window is closed (mainly the draw thread - //to avoid GL operations running post-cleanup). - stopAllThreads(); - }; - - Window.Run(); - } - else - { - while (executionState != ExecutionState.Stopped) - InputThread.RunUpdate(); - } - } - catch (OutOfMemoryException) - { - } - } - finally - { - // Close the window and stop all threads - exit(); - } - } - - private void resetInputHandlers() - { - if (AvailableInputHandlers != null) - foreach (var h in AvailableInputHandlers) - h.Dispose(); - - AvailableInputHandlers = CreateAvailableInputHandlers(); - foreach (var handler in AvailableInputHandlers) - { - if (!handler.Initialize(this)) - { - handler.Enabled.Value = false; - break; - } - - (handler as IHasCursorSensitivity)?.Sensitivity.BindTo(cursorSensitivity); - } - } - - /// - /// The clock which is to be used by the scene graph (will be assigned to ). - /// - protected virtual IFrameBasedClock SceneGraphClock => UpdateThread.Clock; - - private void bootstrapSceneGraph(Game game) - { - var root = new UserInputManager - { - Child = new PlatformActionContainer - { - Child = new FrameworkActionContainer - { - Child = game - } - } - }; - - Dependencies.Cache(root); - Dependencies.CacheAs(game); - - game.SetHost(this); - - root.Load(SceneGraphClock, Dependencies); - - //publish bootstrapped scene graph to all threads. - Root = root; - } - - private const int thread_join_timeout = 30000; - - private void stopAllThreads() - { - threads.ForEach(t => t.Exit()); - threads.Where(t => t.Running).ForEach(t => - { - if (!t.Thread.Join(thread_join_timeout)) - Logger.Log($"Thread {t.Name} failed to exit in allocated time ({thread_join_timeout}ms).", LoggingTarget.Runtime, LogLevel.Important); - }); - - // as the input thread isn't actually handled by a thread, the above join does not necessarily mean it has been completed to an exiting state. - while (!InputThread.Exited) - InputThread.RunUpdate(); - } - - private void window_KeyDown(object sender, KeyboardKeyEventArgs e) - { - if (!e.Control) - return; - switch (e.Key) - { - case Key.F7: - var nextMode = frameSyncMode.Value + 1; - if (nextMode > FrameSync.Unlimited) - nextMode = FrameSync.VSync; - frameSyncMode.Value = nextMode; - break; - } - } - - private InvokeOnDisposal inputPerformanceCollectionPeriod; - - private Bindable activeGCMode; - - private Bindable frameSyncMode; - - private Bindable enabledInputHandlers; - - private Bindable cursorSensitivity; - private Bindable performanceLogging; - - private void setupConfig() - { - Dependencies.Cache(debugConfig = new FrameworkDebugConfigManager()); - Dependencies.Cache(config = new FrameworkConfigManager(Storage)); - Dependencies.Cache(Localisation = new LocalisationEngine(config)); - - activeGCMode = debugConfig.GetBindable(DebugSetting.ActiveGCMode); - activeGCMode.ValueChanged += newMode => - { - GCSettings.LatencyMode = IsActive ? newMode : GCLatencyMode.Interactive; - }; - - frameSyncMode = config.GetBindable(FrameworkSetting.FrameSync); - frameSyncMode.ValueChanged += newMode => - { - float refreshRate = DisplayDevice.Default.RefreshRate; - // For invalid refresh rates let's assume 60 Hz as it is most common. - if (refreshRate <= 0) - refreshRate = 60; - - float drawLimiter = refreshRate; - float updateLimiter = drawLimiter * 2; - - setVSyncMode(); - - switch (newMode) - { - case FrameSync.VSync: - drawLimiter = int.MaxValue; - updateLimiter *= 2; - break; - case FrameSync.Limit2x: - drawLimiter *= 2; - updateLimiter *= 2; - break; - case FrameSync.Limit4x: - drawLimiter *= 4; - updateLimiter *= 4; - break; - case FrameSync.Limit8x: - drawLimiter *= 8; - updateLimiter *= 8; - break; - case FrameSync.Unlimited: - drawLimiter = updateLimiter = int.MaxValue; - break; - } - - if (DrawThread != null) DrawThread.ActiveHz = drawLimiter; - if (UpdateThread != null) UpdateThread.ActiveHz = updateLimiter; - }; - - enabledInputHandlers = config.GetBindable(FrameworkSetting.ActiveInputHandlers); - enabledInputHandlers.ValueChanged += enabledString => - { - var configHandlers = enabledString.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)); - bool useDefaults = !configHandlers.Any(); - - // make sure all the handlers in the configuration file are available, else reset to sane defaults. - foreach (string handler in configHandlers) - { - if (AvailableInputHandlers.All(h => h.ToString() != handler)) - { - useDefaults = true; - break; - } - } - - if (useDefaults) - { - resetInputHandlers(); - enabledInputHandlers.Value = string.Join(" ", AvailableInputHandlers.Where(h => h.Enabled).Select(h => h.ToString())); - } - else - { - foreach (var handler in AvailableInputHandlers) - { - var handlerType = handler.ToString(); - handler.Enabled.Value = configHandlers.Any(ch => ch == handlerType); - } - } - }; - - cursorSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); - - performanceLogging = config.GetBindable(FrameworkSetting.PerformanceLogging); - performanceLogging.ValueChanged += enabled => threads.ForEach(t => t.Monitor.EnablePerformanceProfiling = enabled); - performanceLogging.TriggerChange(); - } - - private void setVSyncMode() - { - if (Window == null) return; - - DrawThread.Scheduler.Add(() => Window.VSync = frameSyncMode == FrameSync.VSync ? VSyncMode.On : VSyncMode.Off); - } - - protected abstract IEnumerable CreateAvailableInputHandlers(); - - public IEnumerable AvailableInputHandlers { get; private set; } - - public abstract ITextInputSource GetTextInput(); - - #region IDisposable Support - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (isDisposed) - return; - isDisposed = true; - - if (executionState > ExecutionState.Stopping) - throw new InvalidOperationException($"{nameof(Exit)} must be called before the {nameof(GameHost)} is disposed."); - - // Delay disposal until the game has exited - while (executionState > ExecutionState.Stopped) - Thread.Sleep(10); - - AppDomain.CurrentDomain.UnhandledException -= exceptionHandler; - - Root?.Dispose(); - Root = null; - - config?.Dispose(); - debugConfig?.Dispose(); - - Window?.Dispose(); - - toolkit?.Dispose(); - - Logger.Flush(); - } - - ~GameHost() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - /// - /// Defines the platform-specific key bindings that will be used by . - /// Should be overridden per-platform to provide native key bindings. - /// - public virtual IEnumerable PlatformKeyBindings => new[] - { - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), - new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), - new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), - new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), - new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.BackSpace }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), - new KeyBinding(InputKey.Home, new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), - new KeyBinding(InputKey.End, new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Home }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.End }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), - }; - - /// - /// The game's execution states. All of these states can only be present once per . - /// Note: The order of values in this enum matters. - /// - private enum ExecutionState - { - /// - /// has not been invoked yet. - /// - Idle = 0, - /// - /// The game's execution has completely stopped. - /// - Stopped = 1, - /// - /// The user has invoked , or the window has been called. - /// The game is currently awaiting to stop all execution on the correct thread. - /// - Stopping = 2, - /// - /// has been invoked. - /// - Running = 3 - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Handlers; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using osu.Framework.Timing; +using osu.Framework.IO.File; +using Bitmap = System.Drawing.Bitmap; + +namespace osu.Framework.Platform +{ + public abstract class GameHost : IIpcHost, IDisposable + { + public GameWindow Window { get; protected set; } + + private readonly Toolkit toolkit; + + private FrameworkDebugConfigManager debugConfig; + + private FrameworkConfigManager config; + + public LocalisationEngine Localisation { get; private set; } + + private void setActive(bool isActive) + { + threads.ForEach(t => t.IsActive = isActive); + + activeGCMode.TriggerChange(); + + if (isActive) + Activated?.Invoke(); + else + Deactivated?.Invoke(); + } + + public bool IsActive => InputThread.IsActive; + + public bool IsPrimaryInstance { get; protected set; } = true; + + public event Action Activated; + public event Action Deactivated; + public event Func Exiting; + public event Action Exited; + + /// + /// An unhandled exception was thrown. Return true to ignore and continue running. + /// + public event Func ExceptionThrown; + + public event Action MessageReceived; + + protected void OnMessageReceived(IpcMessage message) => MessageReceived?.Invoke(message); + + public virtual Task SendMessageAsync(IpcMessage message) + { + throw new NotSupportedException("This platform does not implement IPC."); + } + + public virtual Clipboard GetClipboard() => null; + + protected abstract Storage GetStorage(string baseName); + + public Storage Storage { get; protected set; } + + /// + /// If capslock is enabled on the system, false if not overwritten by a subclass + /// + public virtual bool CapsLockEnabled => false; + + private readonly List threads; + + public IEnumerable Threads => threads; + + public void RegisterThread(GameThread t) + { + threads.Add(t); + t.Monitor.EnablePerformanceProfiling = performanceLogging; + } + + public GameThread DrawThread; + public GameThread UpdateThread; + public InputThread InputThread; + + private double maximumUpdateHz; + + public double MaximumUpdateHz + { + get { return maximumUpdateHz; } + + set { UpdateThread.ActiveHz = maximumUpdateHz = value; } + } + + private double maximumDrawHz; + + public double MaximumDrawHz + { + get { return maximumDrawHz; } + + set { DrawThread.ActiveHz = maximumDrawHz = value; } + } + + public double MaximumInactiveHz + { + get { return DrawThread.InactiveHz; } + + set + { + DrawThread.InactiveHz = value; + UpdateThread.InactiveHz = value; + } + } + + private PerformanceMonitor inputMonitor => InputThread.Monitor; + private PerformanceMonitor drawMonitor => DrawThread.Monitor; + + private readonly Lazy fullPathBacking = new Lazy(RuntimeInfo.GetFrameworkAssemblyPath); + + public string FullPath => fullPathBacking.Value; + + protected string Name { get; } + + public DependencyContainer Dependencies { get; } = new DependencyContainer(); + + protected GameHost(string gameName = @"") + { + toolkit = Toolkit.Init(); + + AppDomain.CurrentDomain.UnhandledException += exceptionHandler; + + FileSafety.DeleteCleanupDirectory(); + + Dependencies.CacheAs(this); + Dependencies.CacheAs(Storage = GetStorage(gameName)); + + Name = gameName; + Logger.GameIdentifier = gameName; + + threads = new List + { + (DrawThread = new DrawThread(DrawFrame) + { + OnThreadStart = DrawInitialize, + }), + (UpdateThread = new UpdateThread(UpdateFrame) + { + OnThreadStart = UpdateInitialize, + Monitor = { HandleGC = true }, + }), + (InputThread = new InputThread(null)), //never gets started. + }; + + var path = Path.GetDirectoryName(FullPath); + if (path != null) + Environment.CurrentDirectory = path; + } + + private void exceptionHandler(object sender, UnhandledExceptionEventArgs e) + { + var exception = (Exception)e.ExceptionObject; + + Logger.Error(exception, @"fatal error:", recursive: true); + + var exInfo = ExceptionDispatchInfo.Capture(exception); + + if (ExceptionThrown?.Invoke(exception) != true) + { + AppDomain.CurrentDomain.UnhandledException -= exceptionHandler; + + //we want to throw this exception on the input thread to interrupt window and also headless execution. + InputThread.Scheduler.Add(() => { exInfo.Throw(); }); + } + } + + protected virtual void OnActivated() => UpdateThread.Scheduler.Add(() => setActive(true)); + + protected virtual void OnDeactivated() => UpdateThread.Scheduler.Add(() => setActive(false)); + + /// true to cancel + protected virtual bool OnExitRequested() + { + if (executionState <= ExecutionState.Stopping) return false; + + bool? response = null; + + UpdateThread.Scheduler.Add(delegate { response = Exiting?.Invoke() == true; }); + + //wait for a potentially blocking response + while (!response.HasValue) + Thread.Sleep(1); + + if (response ?? false) + return true; + + Exit(); + return false; + } + + protected virtual void OnExited() + { + Exited?.Invoke(); + } + + protected TripleBuffer DrawRoots = new TripleBuffer(); + + protected virtual void UpdateInitialize() + { + //this was added due to the dependency on GLWrapper.MaxTextureSize begin initialised. + DrawThread.WaitUntilInitialized(); + } + + protected Container Root; + + protected virtual void UpdateFrame() + { + if (Root == null) return; + + if (Window?.WindowState != WindowState.Minimized) + Root.Size = Window != null ? new Vector2(Window.ClientSize.Width, Window.ClientSize.Height) : + new Vector2(config.Get(FrameworkSetting.Width), config.Get(FrameworkSetting.Height)); + + // Ensure we maintain a valid size for any children immediately scaling by the window size + Root.Size = Vector2.ComponentMax(Vector2.One, Root.Size); + + Root.UpdateSubTree(); + Root.UpdateSubTreeMasking(Root, Root.ScreenSpaceDrawQuad.AABBFloat); + + using (var buffer = DrawRoots.Get(UsageType.Write)) + buffer.Object = Root.GenerateDrawNodeSubtree(buffer.Index); + } + + protected virtual void DrawInitialize() + { + Window.MakeCurrent(); + GLWrapper.Initialize(this); + + setVSyncMode(); + + GLWrapper.Reset(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height)); + GLWrapper.ClearColour(Color4.Black); + } + + private long lastDrawFrameId; + + protected virtual void DrawFrame() + { + if (Root == null) + return; + + while (executionState > ExecutionState.Stopping) + { + using (var buffer = DrawRoots.Get(UsageType.Read)) + { + if (buffer?.Object == null || buffer.FrameId == lastDrawFrameId) + { + Thread.Sleep(1); + continue; + } + + using (drawMonitor.BeginCollecting(PerformanceCollectionType.GLReset)) + { + GLWrapper.Reset(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height)); + GLWrapper.ClearColour(Color4.Black); + } + + buffer.Object.Draw(null); + lastDrawFrameId = buffer.FrameId; + break; + } + } + + GLWrapper.FlushCurrentBatch(); + + using (drawMonitor.BeginCollecting(PerformanceCollectionType.SwapBuffer)) + { + Window.SwapBuffers(); + + if (Window.VSync == VSyncMode.On) + // without glFinish, vsync is basically unplayable due to the extra latency introduced. + // we will likely want to give the user control over this in the future as an advanced setting. + GL.Finish(); + } + } + + /// + /// Make a object from the current OpenTK screen buffer + /// + /// object + public async Task TakeScreenshotAsync() + { + if (Window == null) throw new NullReferenceException(nameof(Window)); + + var clientRectangle = new Rectangle(new Point(Window.ClientRectangle.X, Window.ClientRectangle.Y), new Size(Window.ClientSize.Width, Window.ClientSize.Height)); + + bool complete = false; + + var bitmap = new Bitmap(clientRectangle.Width, clientRectangle.Height); + BitmapData data = bitmap.LockBits(clientRectangle, ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); + + DrawThread.Scheduler.Add(() => + { + if (GraphicsContext.CurrentContext == null) + throw new GraphicsContextMissingException(); + + OpenTK.Graphics.OpenGL.GL.ReadPixels(0, 0, clientRectangle.Width, clientRectangle.Height, OpenTK.Graphics.OpenGL.PixelFormat.Bgr, OpenTK.Graphics.OpenGL.PixelType.UnsignedByte, data.Scan0); + complete = true; + }); + + await Task.Run(() => + { + while (!complete) + Thread.Sleep(50); + }); + + bitmap.UnlockBits(data); + bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY); + + return bitmap; + } + + private volatile ExecutionState executionState; + + /// + /// Schedules the game to exit in the next frame. + /// + public void Exit() + { + executionState = ExecutionState.Stopping; + InputThread.Scheduler.Add(exit, false); + } + + /// + /// Exits the game. This must always be called from . + /// + private void exit() + { + // exit() may be called without having been scheduled from Exit(), so ensure the correct exiting state + executionState = ExecutionState.Stopping; + Window?.Close(); + stopAllThreads(); + executionState = ExecutionState.Stopped; + } + + public void Run(Game game) + { + if (executionState != ExecutionState.Idle) + throw new InvalidOperationException("A game that has already been run cannot be restarted."); + + try + { + executionState = ExecutionState.Running; + + setupConfig(); + + if (Window != null) + { + Window.SetupWindow(config); + Window.Title = $@"osu!framework (running ""{Name}"")"; + } + + resetInputHandlers(); + + DrawThread.Start(); + UpdateThread.Start(); + + DrawThread.WaitUntilInitialized(); + bootstrapSceneGraph(game); + + frameSyncMode.TriggerChange(); + enabledInputHandlers.TriggerChange(); + + try + { + if (Window != null) + { + setActive(Window.Focused); + + Window.KeyDown += window_KeyDown; + + Window.ExitRequested += OnExitRequested; + Window.Exited += OnExited; + Window.FocusedChanged += delegate { setActive(Window.Focused); }; + + Window.UpdateFrame += delegate + { + inputPerformanceCollectionPeriod?.Dispose(); + InputThread.RunUpdate(); + inputPerformanceCollectionPeriod = inputMonitor.BeginCollecting(PerformanceCollectionType.WndProc); + }; + Window.Closed += delegate + { + //we need to ensure all threads have stopped before the window is closed (mainly the draw thread + //to avoid GL operations running post-cleanup). + stopAllThreads(); + }; + + Window.Run(); + } + else + { + while (executionState != ExecutionState.Stopped) + InputThread.RunUpdate(); + } + } + catch (OutOfMemoryException) + { + } + } + finally + { + // Close the window and stop all threads + exit(); + } + } + + private void resetInputHandlers() + { + if (AvailableInputHandlers != null) + foreach (var h in AvailableInputHandlers) + h.Dispose(); + + AvailableInputHandlers = CreateAvailableInputHandlers(); + foreach (var handler in AvailableInputHandlers) + { + if (!handler.Initialize(this)) + { + handler.Enabled.Value = false; + break; + } + + (handler as IHasCursorSensitivity)?.Sensitivity.BindTo(cursorSensitivity); + } + } + + /// + /// The clock which is to be used by the scene graph (will be assigned to ). + /// + protected virtual IFrameBasedClock SceneGraphClock => UpdateThread.Clock; + + private void bootstrapSceneGraph(Game game) + { + var root = new UserInputManager + { + Child = new PlatformActionContainer + { + Child = new FrameworkActionContainer + { + Child = game + } + } + }; + + Dependencies.Cache(root); + Dependencies.CacheAs(game); + + game.SetHost(this); + + root.Load(SceneGraphClock, Dependencies); + + //publish bootstrapped scene graph to all threads. + Root = root; + } + + private const int thread_join_timeout = 30000; + + private void stopAllThreads() + { + threads.ForEach(t => t.Exit()); + threads.Where(t => t.Running).ForEach(t => + { + if (!t.Thread.Join(thread_join_timeout)) + Logger.Log($"Thread {t.Name} failed to exit in allocated time ({thread_join_timeout}ms).", LoggingTarget.Runtime, LogLevel.Important); + }); + + // as the input thread isn't actually handled by a thread, the above join does not necessarily mean it has been completed to an exiting state. + while (!InputThread.Exited) + InputThread.RunUpdate(); + } + + private void window_KeyDown(object sender, KeyboardKeyEventArgs e) + { + if (!e.Control) + return; + switch (e.Key) + { + case Key.F7: + var nextMode = frameSyncMode.Value + 1; + if (nextMode > FrameSync.Unlimited) + nextMode = FrameSync.VSync; + frameSyncMode.Value = nextMode; + break; + } + } + + private InvokeOnDisposal inputPerformanceCollectionPeriod; + + private Bindable activeGCMode; + + private Bindable frameSyncMode; + + private Bindable enabledInputHandlers; + + private Bindable cursorSensitivity; + private Bindable performanceLogging; + + private void setupConfig() + { + Dependencies.Cache(debugConfig = new FrameworkDebugConfigManager()); + Dependencies.Cache(config = new FrameworkConfigManager(Storage)); + Dependencies.Cache(Localisation = new LocalisationEngine(config)); + + activeGCMode = debugConfig.GetBindable(DebugSetting.ActiveGCMode); + activeGCMode.ValueChanged += newMode => + { + GCSettings.LatencyMode = IsActive ? newMode : GCLatencyMode.Interactive; + }; + + frameSyncMode = config.GetBindable(FrameworkSetting.FrameSync); + frameSyncMode.ValueChanged += newMode => + { + float refreshRate = DisplayDevice.Default.RefreshRate; + // For invalid refresh rates let's assume 60 Hz as it is most common. + if (refreshRate <= 0) + refreshRate = 60; + + float drawLimiter = refreshRate; + float updateLimiter = drawLimiter * 2; + + setVSyncMode(); + + switch (newMode) + { + case FrameSync.VSync: + drawLimiter = int.MaxValue; + updateLimiter *= 2; + break; + case FrameSync.Limit2x: + drawLimiter *= 2; + updateLimiter *= 2; + break; + case FrameSync.Limit4x: + drawLimiter *= 4; + updateLimiter *= 4; + break; + case FrameSync.Limit8x: + drawLimiter *= 8; + updateLimiter *= 8; + break; + case FrameSync.Unlimited: + drawLimiter = updateLimiter = int.MaxValue; + break; + } + + if (DrawThread != null) DrawThread.ActiveHz = drawLimiter; + if (UpdateThread != null) UpdateThread.ActiveHz = updateLimiter; + }; + + enabledInputHandlers = config.GetBindable(FrameworkSetting.ActiveInputHandlers); + enabledInputHandlers.ValueChanged += enabledString => + { + var configHandlers = enabledString.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)); + bool useDefaults = !configHandlers.Any(); + + // make sure all the handlers in the configuration file are available, else reset to sane defaults. + foreach (string handler in configHandlers) + { + if (AvailableInputHandlers.All(h => h.ToString() != handler)) + { + useDefaults = true; + break; + } + } + + if (useDefaults) + { + resetInputHandlers(); + enabledInputHandlers.Value = string.Join(" ", AvailableInputHandlers.Where(h => h.Enabled).Select(h => h.ToString())); + } + else + { + foreach (var handler in AvailableInputHandlers) + { + var handlerType = handler.ToString(); + handler.Enabled.Value = configHandlers.Any(ch => ch == handlerType); + } + } + }; + + cursorSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); + + performanceLogging = config.GetBindable(FrameworkSetting.PerformanceLogging); + performanceLogging.ValueChanged += enabled => threads.ForEach(t => t.Monitor.EnablePerformanceProfiling = enabled); + performanceLogging.TriggerChange(); + } + + private void setVSyncMode() + { + if (Window == null) return; + + DrawThread.Scheduler.Add(() => Window.VSync = frameSyncMode == FrameSync.VSync ? VSyncMode.On : VSyncMode.Off); + } + + protected abstract IEnumerable CreateAvailableInputHandlers(); + + public IEnumerable AvailableInputHandlers { get; private set; } + + public abstract ITextInputSource GetTextInput(); + + #region IDisposable Support + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + + if (executionState > ExecutionState.Stopping) + throw new InvalidOperationException($"{nameof(Exit)} must be called before the {nameof(GameHost)} is disposed."); + + // Delay disposal until the game has exited + while (executionState > ExecutionState.Stopped) + Thread.Sleep(10); + + AppDomain.CurrentDomain.UnhandledException -= exceptionHandler; + + Root?.Dispose(); + Root = null; + + config?.Dispose(); + debugConfig?.Dispose(); + + Window?.Dispose(); + + toolkit?.Dispose(); + + Logger.Flush(); + } + + ~GameHost() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Defines the platform-specific key bindings that will be used by . + /// Should be overridden per-platform to provide native key bindings. + /// + public virtual IEnumerable PlatformKeyBindings => new[] + { + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), + new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), + new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), + new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), + new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.BackSpace }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), + new KeyBinding(InputKey.Home, new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), + new KeyBinding(InputKey.End, new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Home }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.End }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), + }; + + /// + /// The game's execution states. All of these states can only be present once per . + /// Note: The order of values in this enum matters. + /// + private enum ExecutionState + { + /// + /// has not been invoked yet. + /// + Idle = 0, + /// + /// The game's execution has completely stopped. + /// + Stopped = 1, + /// + /// The user has invoked , or the window has been called. + /// The game is currently awaiting to stop all execution on the correct thread. + /// + Stopping = 2, + /// + /// has been invoked. + /// + Running = 3 + } + } +} diff --git a/osu.Framework/Platform/GameWindow.cs b/osu.Framework/Platform/GameWindow.cs index ccf998522..20a610bd7 100644 --- a/osu.Framework/Platform/GameWindow.cs +++ b/osu.Framework/Platform/GameWindow.cs @@ -1,274 +1,274 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.Logging; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.ES30; -using OpenTK.Platform; -using OpenTK.Input; -using System.ComponentModel; -using System.Drawing; -using JetBrains.Annotations; -using Icon = OpenTK.Icon; - -namespace osu.Framework.Platform -{ - public abstract class GameWindow : IGameWindow - { - /// - /// The associated with this . - /// - [NotNull] - internal abstract IGraphicsContext Context { get; } - - /// - /// Return value decides whether we should intercept and cancel this exit (if possible). - /// - [CanBeNull] - public event Func ExitRequested; - - /// - /// Invoked when the has closed. - /// - [CanBeNull] - public event Action Exited; - - /// - /// Invoked when any key has been pressed. - /// - [CanBeNull] - public event EventHandler KeyDown; - - internal Version GLVersion; - internal Version GLSLVersion; - - protected readonly IGameWindow Implementation; - - /// - /// Whether the OS cursor is currently contained within the game window. - /// - public bool CursorInWindow { get; private set; } - - /// - /// Creates a with a given implementation. - /// - protected GameWindow([NotNull] IGameWindow implementation) - { - Implementation = implementation; - Implementation.KeyDown += OnKeyDown; - - Closing += (sender, e) => e.Cancel = ExitRequested?.Invoke() ?? false; - Closed += (sender, e) => Exited?.Invoke(); - - MouseEnter += (sender, args) => CursorInWindow = true; - MouseLeave += (sender, args) => CursorInWindow = false; - - MakeCurrent(); - - string version = GL.GetString(StringName.Version); - string versionNumberSubstring = getVersionNumberSubstring(version); - GLVersion = new Version(versionNumberSubstring); - version = GL.GetString(StringName.ShadingLanguageVersion); - if (!string.IsNullOrEmpty(version)) - { - try - { - GLSLVersion = new Version(versionNumberSubstring); - } - catch (Exception e) - { - Logger.Error(e, $@"couldn't set GLSL version using string '{version}'"); - } - } - - if (GLSLVersion == null) - GLSLVersion = new Version(); - - //Set up OpenGL related characteristics - GL.Disable(EnableCap.DepthTest); - GL.Disable(EnableCap.StencilTest); - GL.Enable(EnableCap.Blend); - GL.Enable(EnableCap.ScissorTest); - - Logger.Log($@"GL Initialized - GL Version: {GL.GetString(StringName.Version)} - GL Renderer: {GL.GetString(StringName.Renderer)} - GL Shader Language version: {GL.GetString(StringName.ShadingLanguageVersion)} - GL Vendor: {GL.GetString(StringName.Vendor)} - GL Extensions: {GL.GetString(StringName.Extensions)}", LoggingTarget.Runtime, LogLevel.Important); - - Context.MakeCurrent(null); - } - - /// - /// Creates a with given dimensions. - /// Note that this will use the default implementation, which is not compatible with every platform. - /// - protected GameWindow(int width, int height) - : this(new OpenTK.GameWindow(width, height, new GraphicsMode(GraphicsMode.Default.ColorFormat, GraphicsMode.Default.Depth, GraphicsMode.Default.Stencil, GraphicsMode.Default.Samples, GraphicsMode.Default.AccumulatorFormat, 3))) - { - } - - private CursorState cursorState = CursorState.Default; - - /// - /// Controls the state of the OS cursor. - /// - public CursorState CursorState - { - get => cursorState; - set - { - cursorState = value; - - Implementation.Cursor = (cursorState & CursorState.Hidden) > 0 ? MouseCursor.Empty : MouseCursor.Default; - - try - { - Implementation.CursorGrabbed = (cursorState & CursorState.Confined) > 0; - } - catch - { - // may not be supported by platform. - } - } - } - - /// - /// We do not support directly using . - /// It is controlled internally. Use instead. - /// - public MouseCursor Cursor - { - get => throw new InvalidOperationException($@"{nameof(Cursor)} is not supported. Use {nameof(CursorState)}."); - set => throw new InvalidOperationException($@"{nameof(Cursor)} is not supported. Use {nameof(CursorState)}."); - } - - /// - /// We do not support directly using . - /// It is controlled internally. Use instead. - /// - public bool CursorVisible - { - get => throw new InvalidOperationException($@"{nameof(CursorVisible)} is not supported. Use {nameof(CursorState)}."); - set => throw new InvalidOperationException($@"{nameof(CursorVisible)} is not supported. Use {nameof(CursorState)}."); - } - - /// - /// We do not support directly using . - /// It is controlled internally. Use instead. - /// - public bool CursorGrabbed - { - get => throw new InvalidOperationException($@"{nameof(CursorGrabbed)} is not supported. Use {nameof(CursorState)}."); - set => throw new InvalidOperationException($@"{nameof(CursorGrabbed)} is not supported. Use {nameof(CursorState)}."); - } - - private string getVersionNumberSubstring(string version) - { - string result = version.Split(' ').FirstOrDefault(s => char.IsDigit(s, 0)); - if (result != null) return result; - throw new ArgumentException(nameof(version)); - } - - public abstract void SetupWindow(FrameworkConfigManager config); - - protected virtual void OnKeyDown(object sender, KeyboardKeyEventArgs e) => KeyDown?.Invoke(sender, e); - - public virtual VSyncMode VSync { get; set; } - - public virtual void CycleMode() - { - } - - #region Autogenerated IGameWindow implementation - - public void Run() => Implementation.Run(); - public void Run(double updateRate) => Implementation.Run(updateRate); - public void MakeCurrent() => Implementation.MakeCurrent(); - public void SwapBuffers() => Implementation.SwapBuffers(); - - Icon INativeWindow.Icon { get => Implementation.Icon; set => Implementation.Icon = value; } - public string Title { get => Implementation.Title; set => Implementation.Title = value; } - public bool Focused => Implementation.Focused; - public bool Visible { get => Implementation.Visible; set => Implementation.Visible = value; } - public bool Exists => Implementation.Exists; - public IWindowInfo WindowInfo => Implementation.WindowInfo; - public WindowState WindowState { get => Implementation.WindowState; set => Implementation.WindowState = value; } - public WindowBorder WindowBorder { get => Implementation.WindowBorder; set => Implementation.WindowBorder = value; } - public Rectangle Bounds { get => Implementation.Bounds; set => Implementation.Bounds = value; } - public Point Location { get => Implementation.Location; set => Implementation.Location = value; } - public Size Size { get => Implementation.Size; set => Implementation.Size = value; } - public int X { get => Implementation.X; set => Implementation.X = value; } - public int Y { get => Implementation.Y; set => Implementation.Y = value; } - public int Width { get => Implementation.Width; set => Implementation.Width = value; } - public int Height { get => Implementation.Height; set => Implementation.Height = value; } - public Rectangle ClientRectangle { get => Implementation.ClientRectangle; set => Implementation.ClientRectangle = value; } - public Size ClientSize { get => Implementation.ClientSize; set => Implementation.ClientSize = value; } - - public void Close() => Implementation.Close(); - public void ProcessEvents() => Implementation.ProcessEvents(); - public Point PointToClient(Point point) => Implementation.PointToClient(point); - public Point PointToScreen(Point point) => Implementation.PointToScreen(point); - public void Dispose() => Implementation.Dispose(); - - public event EventHandler Load { add => Implementation.Load += value; remove => Implementation.Load -= value; } - public event EventHandler Unload { add => Implementation.Unload += value; remove => Implementation.Unload -= value; } - public event EventHandler UpdateFrame { add => Implementation.UpdateFrame += value; remove => Implementation.UpdateFrame -= value; } - public event EventHandler RenderFrame { add => Implementation.RenderFrame += value; remove => Implementation.RenderFrame -= value; } - public event EventHandler Move { add => Implementation.Move += value; remove => Implementation.Move -= value; } - public event EventHandler Resize { add => Implementation.Resize += value; remove => Implementation.Resize -= value; } - public event EventHandler Closing { add => Implementation.Closing += value; remove => Implementation.Closing -= value; } - public event EventHandler Closed { add => Implementation.Closed += value; remove => Implementation.Closed -= value; } - public event EventHandler Disposed { add => Implementation.Disposed += value; remove => Implementation.Disposed -= value; } - public event EventHandler IconChanged { add => Implementation.IconChanged += value; remove => Implementation.IconChanged -= value; } - public event EventHandler TitleChanged { add => Implementation.TitleChanged += value; remove => Implementation.TitleChanged -= value; } - public event EventHandler VisibleChanged { add => Implementation.VisibleChanged += value; remove => Implementation.VisibleChanged -= value; } - public event EventHandler FocusedChanged { add => Implementation.FocusedChanged += value; remove => Implementation.FocusedChanged -= value; } - public event EventHandler WindowBorderChanged { add => Implementation.WindowBorderChanged += value; remove => Implementation.WindowBorderChanged -= value; } - public event EventHandler WindowStateChanged { add => Implementation.WindowStateChanged += value; remove => Implementation.WindowStateChanged -= value; } - public event EventHandler KeyPress { add => Implementation.KeyPress += value; remove => Implementation.KeyPress -= value; } - public event EventHandler KeyUp { add => Implementation.KeyUp += value; remove => Implementation.KeyUp -= value; } - public event EventHandler MouseLeave { add => Implementation.MouseLeave += value; remove => Implementation.MouseLeave -= value; } - public event EventHandler MouseEnter { add => Implementation.MouseEnter += value; remove => Implementation.MouseEnter -= value; } - public event EventHandler MouseDown { add => Implementation.MouseDown += value; remove => Implementation.MouseDown -= value; } - public event EventHandler MouseUp { add => Implementation.MouseUp += value; remove => Implementation.MouseUp -= value; } - public event EventHandler MouseMove { add => Implementation.MouseMove += value; remove => Implementation.MouseMove -= value; } - public event EventHandler MouseWheel { add => Implementation.MouseWheel += value; remove => Implementation.MouseWheel -= value; } - public event EventHandler FileDrop { add => Implementation.FileDrop += value; remove => Implementation.FileDrop -= value; } - - #endregion - } - - /// - /// Describes our supported states of the OS cursor. - /// - [Flags] - public enum CursorState - { - /// - /// The OS cursor is always visible and can move anywhere. - /// - Default = 0, - - /// - /// The OS cursor is hidden while hovering the , but can still move anywhere. - /// - Hidden = 1, - - /// - /// The OS cursor is confined to the while the window is in focus. - /// - Confined = 2, - - /// - /// The OS cursor is hidden while hovering the . - /// It is confined to the while the window is in focus and can move freely otherwise. - /// - HiddenAndConfined = Hidden | Confined, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Logging; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.ES30; +using OpenTK.Platform; +using OpenTK.Input; +using System.ComponentModel; +using System.Drawing; +using JetBrains.Annotations; +using Icon = OpenTK.Icon; + +namespace osu.Framework.Platform +{ + public abstract class GameWindow : IGameWindow + { + /// + /// The associated with this . + /// + [NotNull] + internal abstract IGraphicsContext Context { get; } + + /// + /// Return value decides whether we should intercept and cancel this exit (if possible). + /// + [CanBeNull] + public event Func ExitRequested; + + /// + /// Invoked when the has closed. + /// + [CanBeNull] + public event Action Exited; + + /// + /// Invoked when any key has been pressed. + /// + [CanBeNull] + public event EventHandler KeyDown; + + internal Version GLVersion; + internal Version GLSLVersion; + + protected readonly IGameWindow Implementation; + + /// + /// Whether the OS cursor is currently contained within the game window. + /// + public bool CursorInWindow { get; private set; } + + /// + /// Creates a with a given implementation. + /// + protected GameWindow([NotNull] IGameWindow implementation) + { + Implementation = implementation; + Implementation.KeyDown += OnKeyDown; + + Closing += (sender, e) => e.Cancel = ExitRequested?.Invoke() ?? false; + Closed += (sender, e) => Exited?.Invoke(); + + MouseEnter += (sender, args) => CursorInWindow = true; + MouseLeave += (sender, args) => CursorInWindow = false; + + MakeCurrent(); + + string version = GL.GetString(StringName.Version); + string versionNumberSubstring = getVersionNumberSubstring(version); + GLVersion = new Version(versionNumberSubstring); + version = GL.GetString(StringName.ShadingLanguageVersion); + if (!string.IsNullOrEmpty(version)) + { + try + { + GLSLVersion = new Version(versionNumberSubstring); + } + catch (Exception e) + { + Logger.Error(e, $@"couldn't set GLSL version using string '{version}'"); + } + } + + if (GLSLVersion == null) + GLSLVersion = new Version(); + + //Set up OpenGL related characteristics + GL.Disable(EnableCap.DepthTest); + GL.Disable(EnableCap.StencilTest); + GL.Enable(EnableCap.Blend); + GL.Enable(EnableCap.ScissorTest); + + Logger.Log($@"GL Initialized + GL Version: {GL.GetString(StringName.Version)} + GL Renderer: {GL.GetString(StringName.Renderer)} + GL Shader Language version: {GL.GetString(StringName.ShadingLanguageVersion)} + GL Vendor: {GL.GetString(StringName.Vendor)} + GL Extensions: {GL.GetString(StringName.Extensions)}", LoggingTarget.Runtime, LogLevel.Important); + + Context.MakeCurrent(null); + } + + /// + /// Creates a with given dimensions. + /// Note that this will use the default implementation, which is not compatible with every platform. + /// + protected GameWindow(int width, int height) + : this(new OpenTK.GameWindow(width, height, new GraphicsMode(GraphicsMode.Default.ColorFormat, GraphicsMode.Default.Depth, GraphicsMode.Default.Stencil, GraphicsMode.Default.Samples, GraphicsMode.Default.AccumulatorFormat, 3))) + { + } + + private CursorState cursorState = CursorState.Default; + + /// + /// Controls the state of the OS cursor. + /// + public CursorState CursorState + { + get => cursorState; + set + { + cursorState = value; + + Implementation.Cursor = (cursorState & CursorState.Hidden) > 0 ? MouseCursor.Empty : MouseCursor.Default; + + try + { + Implementation.CursorGrabbed = (cursorState & CursorState.Confined) > 0; + } + catch + { + // may not be supported by platform. + } + } + } + + /// + /// We do not support directly using . + /// It is controlled internally. Use instead. + /// + public MouseCursor Cursor + { + get => throw new InvalidOperationException($@"{nameof(Cursor)} is not supported. Use {nameof(CursorState)}."); + set => throw new InvalidOperationException($@"{nameof(Cursor)} is not supported. Use {nameof(CursorState)}."); + } + + /// + /// We do not support directly using . + /// It is controlled internally. Use instead. + /// + public bool CursorVisible + { + get => throw new InvalidOperationException($@"{nameof(CursorVisible)} is not supported. Use {nameof(CursorState)}."); + set => throw new InvalidOperationException($@"{nameof(CursorVisible)} is not supported. Use {nameof(CursorState)}."); + } + + /// + /// We do not support directly using . + /// It is controlled internally. Use instead. + /// + public bool CursorGrabbed + { + get => throw new InvalidOperationException($@"{nameof(CursorGrabbed)} is not supported. Use {nameof(CursorState)}."); + set => throw new InvalidOperationException($@"{nameof(CursorGrabbed)} is not supported. Use {nameof(CursorState)}."); + } + + private string getVersionNumberSubstring(string version) + { + string result = version.Split(' ').FirstOrDefault(s => char.IsDigit(s, 0)); + if (result != null) return result; + throw new ArgumentException(nameof(version)); + } + + public abstract void SetupWindow(FrameworkConfigManager config); + + protected virtual void OnKeyDown(object sender, KeyboardKeyEventArgs e) => KeyDown?.Invoke(sender, e); + + public virtual VSyncMode VSync { get; set; } + + public virtual void CycleMode() + { + } + + #region Autogenerated IGameWindow implementation + + public void Run() => Implementation.Run(); + public void Run(double updateRate) => Implementation.Run(updateRate); + public void MakeCurrent() => Implementation.MakeCurrent(); + public void SwapBuffers() => Implementation.SwapBuffers(); + + Icon INativeWindow.Icon { get => Implementation.Icon; set => Implementation.Icon = value; } + public string Title { get => Implementation.Title; set => Implementation.Title = value; } + public bool Focused => Implementation.Focused; + public bool Visible { get => Implementation.Visible; set => Implementation.Visible = value; } + public bool Exists => Implementation.Exists; + public IWindowInfo WindowInfo => Implementation.WindowInfo; + public WindowState WindowState { get => Implementation.WindowState; set => Implementation.WindowState = value; } + public WindowBorder WindowBorder { get => Implementation.WindowBorder; set => Implementation.WindowBorder = value; } + public Rectangle Bounds { get => Implementation.Bounds; set => Implementation.Bounds = value; } + public Point Location { get => Implementation.Location; set => Implementation.Location = value; } + public Size Size { get => Implementation.Size; set => Implementation.Size = value; } + public int X { get => Implementation.X; set => Implementation.X = value; } + public int Y { get => Implementation.Y; set => Implementation.Y = value; } + public int Width { get => Implementation.Width; set => Implementation.Width = value; } + public int Height { get => Implementation.Height; set => Implementation.Height = value; } + public Rectangle ClientRectangle { get => Implementation.ClientRectangle; set => Implementation.ClientRectangle = value; } + public Size ClientSize { get => Implementation.ClientSize; set => Implementation.ClientSize = value; } + + public void Close() => Implementation.Close(); + public void ProcessEvents() => Implementation.ProcessEvents(); + public Point PointToClient(Point point) => Implementation.PointToClient(point); + public Point PointToScreen(Point point) => Implementation.PointToScreen(point); + public void Dispose() => Implementation.Dispose(); + + public event EventHandler Load { add => Implementation.Load += value; remove => Implementation.Load -= value; } + public event EventHandler Unload { add => Implementation.Unload += value; remove => Implementation.Unload -= value; } + public event EventHandler UpdateFrame { add => Implementation.UpdateFrame += value; remove => Implementation.UpdateFrame -= value; } + public event EventHandler RenderFrame { add => Implementation.RenderFrame += value; remove => Implementation.RenderFrame -= value; } + public event EventHandler Move { add => Implementation.Move += value; remove => Implementation.Move -= value; } + public event EventHandler Resize { add => Implementation.Resize += value; remove => Implementation.Resize -= value; } + public event EventHandler Closing { add => Implementation.Closing += value; remove => Implementation.Closing -= value; } + public event EventHandler Closed { add => Implementation.Closed += value; remove => Implementation.Closed -= value; } + public event EventHandler Disposed { add => Implementation.Disposed += value; remove => Implementation.Disposed -= value; } + public event EventHandler IconChanged { add => Implementation.IconChanged += value; remove => Implementation.IconChanged -= value; } + public event EventHandler TitleChanged { add => Implementation.TitleChanged += value; remove => Implementation.TitleChanged -= value; } + public event EventHandler VisibleChanged { add => Implementation.VisibleChanged += value; remove => Implementation.VisibleChanged -= value; } + public event EventHandler FocusedChanged { add => Implementation.FocusedChanged += value; remove => Implementation.FocusedChanged -= value; } + public event EventHandler WindowBorderChanged { add => Implementation.WindowBorderChanged += value; remove => Implementation.WindowBorderChanged -= value; } + public event EventHandler WindowStateChanged { add => Implementation.WindowStateChanged += value; remove => Implementation.WindowStateChanged -= value; } + public event EventHandler KeyPress { add => Implementation.KeyPress += value; remove => Implementation.KeyPress -= value; } + public event EventHandler KeyUp { add => Implementation.KeyUp += value; remove => Implementation.KeyUp -= value; } + public event EventHandler MouseLeave { add => Implementation.MouseLeave += value; remove => Implementation.MouseLeave -= value; } + public event EventHandler MouseEnter { add => Implementation.MouseEnter += value; remove => Implementation.MouseEnter -= value; } + public event EventHandler MouseDown { add => Implementation.MouseDown += value; remove => Implementation.MouseDown -= value; } + public event EventHandler MouseUp { add => Implementation.MouseUp += value; remove => Implementation.MouseUp -= value; } + public event EventHandler MouseMove { add => Implementation.MouseMove += value; remove => Implementation.MouseMove -= value; } + public event EventHandler MouseWheel { add => Implementation.MouseWheel += value; remove => Implementation.MouseWheel -= value; } + public event EventHandler FileDrop { add => Implementation.FileDrop += value; remove => Implementation.FileDrop -= value; } + + #endregion + } + + /// + /// Describes our supported states of the OS cursor. + /// + [Flags] + public enum CursorState + { + /// + /// The OS cursor is always visible and can move anywhere. + /// + Default = 0, + + /// + /// The OS cursor is hidden while hovering the , but can still move anywhere. + /// + Hidden = 1, + + /// + /// The OS cursor is confined to the while the window is in focus. + /// + Confined = 2, + + /// + /// The OS cursor is hidden while hovering the . + /// It is confined to the while the window is in focus and can move freely otherwise. + /// + HiddenAndConfined = Hidden | Confined, + } +} diff --git a/osu.Framework/Platform/HeadlessGameHost.cs b/osu.Framework/Platform/HeadlessGameHost.cs index 74b7b8a76..6c55a5933 100644 --- a/osu.Framework/Platform/HeadlessGameHost.cs +++ b/osu.Framework/Platform/HeadlessGameHost.cs @@ -1,71 +1,71 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Input.Handlers; -using osu.Framework.Timing; - -namespace osu.Framework.Platform -{ - /// - /// A GameHost which doesn't require a graphical or sound device. - /// - public class HeadlessGameHost : DesktopGameHost - { - private readonly IFrameBasedClock customClock; - - protected override IFrameBasedClock SceneGraphClock => customClock ?? base.SceneGraphClock; - - protected override Storage GetStorage(string baseName) => new DesktopStorage($"headless-{baseName}"); - - public HeadlessGameHost(string gameName = @"", bool bindIPC = false, bool realtime = true) - : base(gameName, bindIPC) - { - if (!realtime) customClock = new FramedClock(new FastClock(1000.0 / 30)); - - UpdateThread.Scheduler.Update(); - } - - protected override void UpdateInitialize() - { - } - - protected override void DrawInitialize() - { - } - - protected override void DrawFrame() - { - //we can't draw. - } - - protected override void UpdateFrame() - { - customClock?.ProcessFrame(); - - base.UpdateFrame(); - } - - protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[] { }; - - private class FastClock : IClock - { - private readonly double increment; - private double time; - - /// - /// A clock which increments each time is requested. - /// Run fast. Run consistent. - /// - /// Milliseconds we should increment the clock by each time the time is requested. - public FastClock(double increment) - { - this.increment = increment; - } - - public double CurrentTime => time += increment; - public double Rate => 1; - public bool IsRunning => true; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input.Handlers; +using osu.Framework.Timing; + +namespace osu.Framework.Platform +{ + /// + /// A GameHost which doesn't require a graphical or sound device. + /// + public class HeadlessGameHost : DesktopGameHost + { + private readonly IFrameBasedClock customClock; + + protected override IFrameBasedClock SceneGraphClock => customClock ?? base.SceneGraphClock; + + protected override Storage GetStorage(string baseName) => new DesktopStorage($"headless-{baseName}"); + + public HeadlessGameHost(string gameName = @"", bool bindIPC = false, bool realtime = true) + : base(gameName, bindIPC) + { + if (!realtime) customClock = new FramedClock(new FastClock(1000.0 / 30)); + + UpdateThread.Scheduler.Update(); + } + + protected override void UpdateInitialize() + { + } + + protected override void DrawInitialize() + { + } + + protected override void DrawFrame() + { + //we can't draw. + } + + protected override void UpdateFrame() + { + customClock?.ProcessFrame(); + + base.UpdateFrame(); + } + + protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[] { }; + + private class FastClock : IClock + { + private readonly double increment; + private double time; + + /// + /// A clock which increments each time is requested. + /// Run fast. Run consistent. + /// + /// Milliseconds we should increment the clock by each time the time is requested. + public FastClock(double increment) + { + this.increment = increment; + } + + public double CurrentTime => time += increment; + public double Rate => 1; + public bool IsRunning => true; + } + } +} diff --git a/osu.Framework/Platform/IIpcHost.cs b/osu.Framework/Platform/IIpcHost.cs index 60e8c0fa1..b98311096 100644 --- a/osu.Framework/Platform/IIpcHost.cs +++ b/osu.Framework/Platform/IIpcHost.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading.Tasks; - -namespace osu.Framework.Platform -{ - public interface IIpcHost - { - event Action MessageReceived; - - Task SendMessageAsync(IpcMessage ipcMessage); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading.Tasks; + +namespace osu.Framework.Platform +{ + public interface IIpcHost + { + event Action MessageReceived; + + Task SendMessageAsync(IpcMessage ipcMessage); + } +} diff --git a/osu.Framework/Platform/IpcChannel.cs b/osu.Framework/Platform/IpcChannel.cs index 20ec02f44..0f573909c 100644 --- a/osu.Framework/Platform/IpcChannel.cs +++ b/osu.Framework/Platform/IpcChannel.cs @@ -1,42 +1,42 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading.Tasks; - -namespace osu.Framework.Platform -{ - public class IpcChannel : IDisposable - { - private readonly IIpcHost host; - public event Action MessageReceived; - - public IpcChannel(IIpcHost host) - { - this.host = host; - this.host.MessageReceived += handleMessage; - } - - public async Task SendMessageAsync(T message) - { - var msg = new IpcMessage - { - Type = typeof(T).AssemblyQualifiedName, - Value = message, - }; - await host.SendMessageAsync(msg); - } - - private void handleMessage(IpcMessage message) - { - if (message.Type != typeof(T).AssemblyQualifiedName) - return; - MessageReceived?.Invoke((T)message.Value); - } - - public void Dispose() - { - host.MessageReceived -= handleMessage; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading.Tasks; + +namespace osu.Framework.Platform +{ + public class IpcChannel : IDisposable + { + private readonly IIpcHost host; + public event Action MessageReceived; + + public IpcChannel(IIpcHost host) + { + this.host = host; + this.host.MessageReceived += handleMessage; + } + + public async Task SendMessageAsync(T message) + { + var msg = new IpcMessage + { + Type = typeof(T).AssemblyQualifiedName, + Value = message, + }; + await host.SendMessageAsync(msg); + } + + private void handleMessage(IpcMessage message) + { + if (message.Type != typeof(T).AssemblyQualifiedName) + return; + MessageReceived?.Invoke((T)message.Value); + } + + public void Dispose() + { + host.MessageReceived -= handleMessage; + } + } +} diff --git a/osu.Framework/Platform/IpcMessage.cs b/osu.Framework/Platform/IpcMessage.cs index 36e7c24c1..16c72c7e0 100644 --- a/osu.Framework/Platform/IpcMessage.cs +++ b/osu.Framework/Platform/IpcMessage.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Platform -{ - public class IpcMessage - { - public string Type { get; set; } - public object Value { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Platform +{ + public class IpcMessage + { + public string Type { get; set; } + public object Value { get; set; } + } +} diff --git a/osu.Framework/Platform/Linux/LinuxClipboard.cs b/osu.Framework/Platform/Linux/LinuxClipboard.cs index da56d69c8..45f769c43 100644 --- a/osu.Framework/Platform/Linux/LinuxClipboard.cs +++ b/osu.Framework/Platform/Linux/LinuxClipboard.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -// using System.Windows.Forms; - -namespace osu.Framework.Platform.Linux -{ - public class LinuxClipboard : Clipboard - { - public override string GetText() - { - return string.Empty; - // return System.Windows.Forms.Clipboard.GetText(TextDataFormat.UnicodeText); - } - - public override void SetText(string selectedText) - { - //Clipboard.SetText(selectedText); - - //This works within osu but will hang any application you try to paste to afterwards until osu is closed. - //Likely requires the use of X libraries directly to fix - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +// using System.Windows.Forms; + +namespace osu.Framework.Platform.Linux +{ + public class LinuxClipboard : Clipboard + { + public override string GetText() + { + return string.Empty; + // return System.Windows.Forms.Clipboard.GetText(TextDataFormat.UnicodeText); + } + + public override void SetText(string selectedText) + { + //Clipboard.SetText(selectedText); + + //This works within osu but will hang any application you try to paste to afterwards until osu is closed. + //Likely requires the use of X libraries directly to fix + } + } +} diff --git a/osu.Framework/Platform/Linux/LinuxGameHost.cs b/osu.Framework/Platform/Linux/LinuxGameHost.cs index 27d38e749..d3f5592ba 100644 --- a/osu.Framework/Platform/Linux/LinuxGameHost.cs +++ b/osu.Framework/Platform/Linux/LinuxGameHost.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Platform.Linux -{ - public class LinuxGameHost : DesktopGameHost - { - internal LinuxGameHost(string gameName, bool bindIPC = false) - : base(gameName, bindIPC) - { - Window = new LinuxGameWindow(); - Window.WindowStateChanged += (sender, e) => - { - if (Window.WindowState != OpenTK.WindowState.Minimized) - OnActivated(); - else - OnDeactivated(); - }; - } - - protected override Storage GetStorage(string baseName) => new LinuxStorage(baseName); - - public override Clipboard GetClipboard() => new LinuxClipboard(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Platform.Linux +{ + public class LinuxGameHost : DesktopGameHost + { + internal LinuxGameHost(string gameName, bool bindIPC = false) + : base(gameName, bindIPC) + { + Window = new LinuxGameWindow(); + Window.WindowStateChanged += (sender, e) => + { + if (Window.WindowState != OpenTK.WindowState.Minimized) + OnActivated(); + else + OnDeactivated(); + }; + } + + protected override Storage GetStorage(string baseName) => new LinuxStorage(baseName); + + public override Clipboard GetClipboard() => new LinuxClipboard(); + } +} diff --git a/osu.Framework/Platform/Linux/LinuxGameWindow.cs b/osu.Framework/Platform/Linux/LinuxGameWindow.cs index 76254eb1f..2b57c2e42 100644 --- a/osu.Framework/Platform/Linux/LinuxGameWindow.cs +++ b/osu.Framework/Platform/Linux/LinuxGameWindow.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Platform.Linux -{ - public class LinuxGameWindow : DesktopGameWindow - { - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Platform.Linux +{ + public class LinuxGameWindow : DesktopGameWindow + { + } +} diff --git a/osu.Framework/Platform/Linux/LinuxStorage.cs b/osu.Framework/Platform/Linux/LinuxStorage.cs index 82900b4bb..8b21155cd 100644 --- a/osu.Framework/Platform/Linux/LinuxStorage.cs +++ b/osu.Framework/Platform/Linux/LinuxStorage.cs @@ -1,35 +1,35 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; - -namespace osu.Framework.Platform.Linux -{ - public class LinuxStorage : DesktopStorage - { - public LinuxStorage(string baseName) - : base(baseName) - { - } - - protected override string LocateBasePath() - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); - string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - string[] paths = - { - xdg ?? Path.Combine(home, ".local", "share"), - Path.Combine(home) - }; - - foreach (string path in paths) - { - if (Directory.Exists(path)) - return path; - } - - return paths[0]; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; + +namespace osu.Framework.Platform.Linux +{ + public class LinuxStorage : DesktopStorage + { + public LinuxStorage(string baseName) + : base(baseName) + { + } + + protected override string LocateBasePath() + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + string[] paths = + { + xdg ?? Path.Combine(home, ".local", "share"), + Path.Combine(home) + }; + + foreach (string path in paths) + { + if (Directory.Exists(path)) + return path; + } + + return paths[0]; + } + } +} diff --git a/osu.Framework/Platform/MacOS/MacOSClipboard.cs b/osu.Framework/Platform/MacOS/MacOSClipboard.cs index 21bf7eea8..b682a28dc 100644 --- a/osu.Framework/Platform/MacOS/MacOSClipboard.cs +++ b/osu.Framework/Platform/MacOS/MacOSClipboard.cs @@ -1,32 +1,32 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Platform.MacOS.Native; - -namespace osu.Framework.Platform.MacOS -{ - public class MacOSClipboard : Clipboard - { - internal NSPasteboard GeneralPasteboard = NSPasteboard.GeneralPasteboard(); - - public override string GetText() - { - NSArray classArray = NSArray.ArrayWithObject(Class.Get("NSString")); - if (GeneralPasteboard.CanReadObjectForClasses(classArray, null)) - { - var result = GeneralPasteboard.ReadObjectsForClasses(classArray, null); - var objects = result?.ToArray() ?? new IntPtr[0]; - if (objects.Length > 0 && objects[0] != IntPtr.Zero) - return Cocoa.FromNSString(objects[0]); - } - return string.Empty; - } - - public override void SetText(string selectedText) - { - GeneralPasteboard.ClearContents(); - GeneralPasteboard.WriteObjects(NSArray.ArrayWithObject(Cocoa.ToNSString(selectedText))); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Platform.MacOS.Native; + +namespace osu.Framework.Platform.MacOS +{ + public class MacOSClipboard : Clipboard + { + internal NSPasteboard GeneralPasteboard = NSPasteboard.GeneralPasteboard(); + + public override string GetText() + { + NSArray classArray = NSArray.ArrayWithObject(Class.Get("NSString")); + if (GeneralPasteboard.CanReadObjectForClasses(classArray, null)) + { + var result = GeneralPasteboard.ReadObjectsForClasses(classArray, null); + var objects = result?.ToArray() ?? new IntPtr[0]; + if (objects.Length > 0 && objects[0] != IntPtr.Zero) + return Cocoa.FromNSString(objects[0]); + } + return string.Empty; + } + + public override void SetText(string selectedText) + { + GeneralPasteboard.ClearContents(); + GeneralPasteboard.WriteObjects(NSArray.ArrayWithObject(Cocoa.ToNSString(selectedText))); + } + } +} diff --git a/osu.Framework/Platform/MacOS/MacOSGameHost.cs b/osu.Framework/Platform/MacOS/MacOSGameHost.cs index 1e7da6a0c..702245097 100644 --- a/osu.Framework/Platform/MacOS/MacOSGameHost.cs +++ b/osu.Framework/Platform/MacOS/MacOSGameHost.cs @@ -1,55 +1,55 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; - -namespace osu.Framework.Platform.MacOS -{ - public class MacOSGameHost : DesktopGameHost - { - internal MacOSGameHost(string gameName, bool bindIPC = false) - : base(gameName, bindIPC) - { - Window = new MacOSGameWindow(); - Window.WindowStateChanged += (sender, e) => - { - if (Window.WindowState != OpenTK.WindowState.Minimized) - OnActivated(); - else - OnDeactivated(); - }; - } - - protected override Storage GetStorage(string baseName) => new MacOSStorage(baseName); - - public override Clipboard GetClipboard() => new MacOSClipboard(); - - public override IEnumerable PlatformKeyBindings => new[] - { - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), - new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), - new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), - new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), - new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.BackSpace}), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.BackSpace }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Delete }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Delete)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), - new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), - }; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; + +namespace osu.Framework.Platform.MacOS +{ + public class MacOSGameHost : DesktopGameHost + { + internal MacOSGameHost(string gameName, bool bindIPC = false) + : base(gameName, bindIPC) + { + Window = new MacOSGameWindow(); + Window.WindowStateChanged += (sender, e) => + { + if (Window.WindowState != OpenTK.WindowState.Minimized) + OnActivated(); + else + OnDeactivated(); + }; + } + + protected override Storage GetStorage(string baseName) => new MacOSStorage(baseName); + + public override Clipboard GetClipboard() => new MacOSClipboard(); + + public override IEnumerable PlatformKeyBindings => new[] + { + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.X }), new PlatformAction(PlatformActionType.Cut)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.C }), new PlatformAction(PlatformActionType.Copy)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.V }), new PlatformAction(PlatformActionType.Paste)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.A }), new PlatformAction(PlatformActionType.SelectAll)), + new KeyBinding(InputKey.Left, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Move)), + new KeyBinding(InputKey.Right, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Move)), + new KeyBinding(InputKey.BackSpace, new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Delete)), + new KeyBinding(InputKey.Delete, new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.CharPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.CharNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.BackSpace}), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Delete }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.WordPrevious, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Alt, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.WordNext, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Move)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.BackSpace }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Delete }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Delete)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Shift, InputKey.Left }), new PlatformAction(PlatformActionType.LineStart, PlatformActionMethod.Select)), + new KeyBinding(new KeyCombination(new[] { InputKey.Super, InputKey.Shift, InputKey.Right }), new PlatformAction(PlatformActionType.LineEnd, PlatformActionMethod.Select)), + }; + } +} diff --git a/osu.Framework/Platform/MacOS/MacOSGameWindow.cs b/osu.Framework/Platform/MacOS/MacOSGameWindow.cs index 498e9bd4d..259b2bbf7 100644 --- a/osu.Framework/Platform/MacOS/MacOSGameWindow.cs +++ b/osu.Framework/Platform/MacOS/MacOSGameWindow.cs @@ -1,140 +1,140 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using System.Runtime.InteropServices; -using System.Reflection; -using osu.Framework.Logging; -using osu.Framework.Platform.MacOS.Native; - -namespace osu.Framework.Platform.MacOS -{ - internal class MacOSGameWindow : DesktopGameWindow - { - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - private delegate void FlagsChangedDelegate(IntPtr self, IntPtr cmd, IntPtr notification); - - private FlagsChangedDelegate flagsChangedHandler; - - private readonly IntPtr selModifierFlags = Selector.Get("modifierFlags"); - private readonly IntPtr selKeyCode = Selector.Get("keyCode"); - private MethodInfo methodKeyDown; - private MethodInfo methodKeyUp; - - private const int modifier_flag_left_control = 1 << 0; - private const int modifier_flag_left_shift = 1 << 1; - private const int modifier_flag_right_shift = 1 << 2; - private const int modifier_flag_left_command = 1 << 3; - private const int modifier_flag_right_command = 1 << 4; - private const int modifier_flag_left_alt = 1 << 5; - private const int modifier_flag_right_alt = 1 << 6; - private const int modifier_flag_right_control = 1 << 13; - - private object nativeWindow; - - public MacOSGameWindow() - { - Load += OnLoad; - } - - protected void OnLoad(object sender, EventArgs e) - { - try - { - flagsChangedHandler = flagsChanged; - - var fieldImplementation = typeof(OpenTK.NativeWindow).GetRuntimeFields().Single(x => x.Name == "implementation"); - var typeCocoaNativeWindow = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "CocoaNativeWindow"); - var fieldWindowClass = typeCocoaNativeWindow.GetRuntimeFields().Single(x => x.Name == "windowClass"); - - nativeWindow = fieldImplementation.GetValue(Implementation); - var windowClass = (IntPtr)fieldWindowClass.GetValue(nativeWindow); - - Class.RegisterMethod(windowClass, flagsChangedHandler, "flagsChanged:", "v@:@"); - - methodKeyDown = nativeWindow.GetType().GetRuntimeMethods().Single(x => x.Name == "OnKeyDown"); - methodKeyUp = nativeWindow.GetType().GetRuntimeMethods().Single(x => x.Name == "OnKeyUp"); - } - catch - { - Logger.Log("Window initialisation couldn't complete, likely due to the SDL backend being enabled.", LoggingTarget.Runtime, LogLevel.Important); - Logger.Log("Execution will continue but keyboard functionality may be limited.", LoggingTarget.Runtime, LogLevel.Important); - } - } - - private void flagsChanged(IntPtr self, IntPtr cmd, IntPtr sender) - { - var modifierFlags = Cocoa.SendInt(sender, selModifierFlags); - var keyCode = Cocoa.SendInt(sender, selKeyCode); - - bool keyDown; - OpenTK.Input.Key key; - - switch ((MacOSKeyCodes)keyCode) - { - case MacOSKeyCodes.LShift: - key = OpenTK.Input.Key.LShift; - keyDown = (modifierFlags & modifier_flag_left_shift) > 0; - break; - - case MacOSKeyCodes.RShift: - key = OpenTK.Input.Key.RShift; - keyDown = (modifierFlags & modifier_flag_right_shift) > 0; - break; - - case MacOSKeyCodes.LControl: - key = OpenTK.Input.Key.LControl; - keyDown = (modifierFlags & modifier_flag_left_control) > 0; - break; - - case MacOSKeyCodes.RControl: - key = OpenTK.Input.Key.RControl; - keyDown = (modifierFlags & modifier_flag_right_control) > 0; - break; - - case MacOSKeyCodes.LAlt: - key = OpenTK.Input.Key.LAlt; - keyDown = (modifierFlags & modifier_flag_left_alt) > 0; - break; - - case MacOSKeyCodes.RAlt: - key = OpenTK.Input.Key.RAlt; - keyDown = (modifierFlags & modifier_flag_right_alt) > 0; - break; - - case MacOSKeyCodes.LCommand: - key = OpenTK.Input.Key.LWin; - keyDown = (modifierFlags & modifier_flag_left_command) > 0; - break; - - case MacOSKeyCodes.RCommand: - key = OpenTK.Input.Key.RWin; - keyDown = (modifierFlags & modifier_flag_right_command) > 0; - break; - - default: - return; - } - - if (keyDown) - methodKeyDown.Invoke(nativeWindow, new object[] { key, false }); - else - methodKeyUp.Invoke(nativeWindow, new object[] { key }); - } - } - - internal enum MacOSKeyCodes - { - LShift = 56, - RShift = 60, - LControl = 59, - RControl = 62, - LAlt = 58, - RAlt = 61, - LCommand = 55, - RCommand = 54, - CapsLock = 57, - Function = 63 - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Reflection; +using osu.Framework.Logging; +using osu.Framework.Platform.MacOS.Native; + +namespace osu.Framework.Platform.MacOS +{ + internal class MacOSGameWindow : DesktopGameWindow + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate void FlagsChangedDelegate(IntPtr self, IntPtr cmd, IntPtr notification); + + private FlagsChangedDelegate flagsChangedHandler; + + private readonly IntPtr selModifierFlags = Selector.Get("modifierFlags"); + private readonly IntPtr selKeyCode = Selector.Get("keyCode"); + private MethodInfo methodKeyDown; + private MethodInfo methodKeyUp; + + private const int modifier_flag_left_control = 1 << 0; + private const int modifier_flag_left_shift = 1 << 1; + private const int modifier_flag_right_shift = 1 << 2; + private const int modifier_flag_left_command = 1 << 3; + private const int modifier_flag_right_command = 1 << 4; + private const int modifier_flag_left_alt = 1 << 5; + private const int modifier_flag_right_alt = 1 << 6; + private const int modifier_flag_right_control = 1 << 13; + + private object nativeWindow; + + public MacOSGameWindow() + { + Load += OnLoad; + } + + protected void OnLoad(object sender, EventArgs e) + { + try + { + flagsChangedHandler = flagsChanged; + + var fieldImplementation = typeof(OpenTK.NativeWindow).GetRuntimeFields().Single(x => x.Name == "implementation"); + var typeCocoaNativeWindow = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "CocoaNativeWindow"); + var fieldWindowClass = typeCocoaNativeWindow.GetRuntimeFields().Single(x => x.Name == "windowClass"); + + nativeWindow = fieldImplementation.GetValue(Implementation); + var windowClass = (IntPtr)fieldWindowClass.GetValue(nativeWindow); + + Class.RegisterMethod(windowClass, flagsChangedHandler, "flagsChanged:", "v@:@"); + + methodKeyDown = nativeWindow.GetType().GetRuntimeMethods().Single(x => x.Name == "OnKeyDown"); + methodKeyUp = nativeWindow.GetType().GetRuntimeMethods().Single(x => x.Name == "OnKeyUp"); + } + catch + { + Logger.Log("Window initialisation couldn't complete, likely due to the SDL backend being enabled.", LoggingTarget.Runtime, LogLevel.Important); + Logger.Log("Execution will continue but keyboard functionality may be limited.", LoggingTarget.Runtime, LogLevel.Important); + } + } + + private void flagsChanged(IntPtr self, IntPtr cmd, IntPtr sender) + { + var modifierFlags = Cocoa.SendInt(sender, selModifierFlags); + var keyCode = Cocoa.SendInt(sender, selKeyCode); + + bool keyDown; + OpenTK.Input.Key key; + + switch ((MacOSKeyCodes)keyCode) + { + case MacOSKeyCodes.LShift: + key = OpenTK.Input.Key.LShift; + keyDown = (modifierFlags & modifier_flag_left_shift) > 0; + break; + + case MacOSKeyCodes.RShift: + key = OpenTK.Input.Key.RShift; + keyDown = (modifierFlags & modifier_flag_right_shift) > 0; + break; + + case MacOSKeyCodes.LControl: + key = OpenTK.Input.Key.LControl; + keyDown = (modifierFlags & modifier_flag_left_control) > 0; + break; + + case MacOSKeyCodes.RControl: + key = OpenTK.Input.Key.RControl; + keyDown = (modifierFlags & modifier_flag_right_control) > 0; + break; + + case MacOSKeyCodes.LAlt: + key = OpenTK.Input.Key.LAlt; + keyDown = (modifierFlags & modifier_flag_left_alt) > 0; + break; + + case MacOSKeyCodes.RAlt: + key = OpenTK.Input.Key.RAlt; + keyDown = (modifierFlags & modifier_flag_right_alt) > 0; + break; + + case MacOSKeyCodes.LCommand: + key = OpenTK.Input.Key.LWin; + keyDown = (modifierFlags & modifier_flag_left_command) > 0; + break; + + case MacOSKeyCodes.RCommand: + key = OpenTK.Input.Key.RWin; + keyDown = (modifierFlags & modifier_flag_right_command) > 0; + break; + + default: + return; + } + + if (keyDown) + methodKeyDown.Invoke(nativeWindow, new object[] { key, false }); + else + methodKeyUp.Invoke(nativeWindow, new object[] { key }); + } + } + + internal enum MacOSKeyCodes + { + LShift = 56, + RShift = 60, + LControl = 59, + RControl = 62, + LAlt = 58, + RAlt = 61, + LCommand = 55, + RCommand = 54, + CapsLock = 57, + Function = 63 + } +} diff --git a/osu.Framework/Platform/MacOS/MacOSStorage.cs b/osu.Framework/Platform/MacOS/MacOSStorage.cs index 1085079dc..1c25df09d 100644 --- a/osu.Framework/Platform/MacOS/MacOSStorage.cs +++ b/osu.Framework/Platform/MacOS/MacOSStorage.cs @@ -1,35 +1,35 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; - -namespace osu.Framework.Platform.MacOS -{ - public class MacOSStorage : DesktopStorage - { - public MacOSStorage(string baseName) - : base(baseName) - { - } - - protected override string LocateBasePath() - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); - string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - string[] paths = - { - xdg ?? Path.Combine(home, ".local", "share"), - Path.Combine(home) - }; - - foreach (string path in paths) - { - if (Directory.Exists(path)) - return path; - } - - return paths[0]; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; + +namespace osu.Framework.Platform.MacOS +{ + public class MacOSStorage : DesktopStorage + { + public MacOSStorage(string baseName) + : base(baseName) + { + } + + protected override string LocateBasePath() + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + string[] paths = + { + xdg ?? Path.Combine(home, ".local", "share"), + Path.Combine(home) + }; + + foreach (string path in paths) + { + if (Directory.Exists(path)) + return path; + } + + return paths[0]; + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/Class.cs b/osu.Framework/Platform/MacOS/Native/Class.cs index dd3b0b886..35abd72d9 100644 --- a/osu.Framework/Platform/MacOS/Native/Class.cs +++ b/osu.Framework/Platform/MacOS/Native/Class.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using System.Reflection; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal static class Class - { - private static readonly Type type_class = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Class"); - private static readonly MethodInfo method_class_get = type_class.GetMethod("Get"); - private static readonly MethodInfo method_register_method = type_class.GetMethod("RegisterMethod"); - - public static IntPtr Get(string name) - { - return (IntPtr)method_class_get.Invoke(null, new object[] { name }); - } - - public static void RegisterMethod(IntPtr handle, Delegate d, string selector, string typeString) - { - method_register_method.Invoke(null, new object[] { handle, d, selector, typeString }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using System.Reflection; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal static class Class + { + private static readonly Type type_class = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Class"); + private static readonly MethodInfo method_class_get = type_class.GetMethod("Get"); + private static readonly MethodInfo method_register_method = type_class.GetMethod("RegisterMethod"); + + public static IntPtr Get(string name) + { + return (IntPtr)method_class_get.Invoke(null, new object[] { name }); + } + + public static void RegisterMethod(IntPtr handle, Delegate d, string selector, string typeString) + { + method_register_method.Invoke(null, new object[] { handle, d, selector, typeString }); + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/Cocoa.cs b/osu.Framework/Platform/MacOS/Native/Cocoa.cs index 2c5e76768..00fc06bf9 100644 --- a/osu.Framework/Platform/MacOS/Native/Cocoa.cs +++ b/osu.Framework/Platform/MacOS/Native/Cocoa.cs @@ -1,83 +1,83 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal static class Cocoa - { - internal const string LIB_OBJ_C = "/usr/lib/libobjc.dylib"; - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, int arg); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern int SendInt(IntPtr receiver, IntPtr selector); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern int SendInt(IntPtr receiver, IntPtr selector, IntPtr ptr1); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern int SendInt(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern bool SendBool(IntPtr receiver, IntPtr selector); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern bool SendBool(IntPtr receiver, IntPtr selector, IntPtr ptr1); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern bool SendBool(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern void SendVoid(IntPtr receiver, IntPtr selector); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern void SendVoid(IntPtr receiver, IntPtr selector, IntPtr ptr1); - - [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] - public static extern void SendVoid(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); - - private static readonly Type type_cocoa = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Cocoa"); - private static readonly MethodInfo method_cocoa_from_ns_string = type_cocoa.GetMethod("FromNSString"); - private static readonly MethodInfo method_cocoa_to_ns_string = type_cocoa.GetMethod("ToNSString"); - private static readonly MethodInfo method_cocoa_get_string_constant = type_cocoa.GetMethod("GetStringConstant"); - - public static IntPtr AppKitLibrary; - public static IntPtr FoundationLibrary; - - static Cocoa() - { - AppKitLibrary = (IntPtr)type_cocoa.GetField("AppKitLibrary").GetValue(null); - FoundationLibrary = (IntPtr)type_cocoa.GetField("FoundationLibrary").GetValue(null); - } - - public static string FromNSString(IntPtr handle) - { - return (string)method_cocoa_from_ns_string.Invoke(null, new object[] { handle }); - } - - public static IntPtr ToNSString(string str) - { - return (IntPtr)method_cocoa_to_ns_string.Invoke(null, new object[] { str }); - } - - public static IntPtr GetStringConstant(IntPtr handle, string symbol) - { - return (IntPtr)method_cocoa_get_string_constant.Invoke(null, new object[] { handle, symbol }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal static class Cocoa + { + internal const string LIB_OBJ_C = "/usr/lib/libobjc.dylib"; + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, int arg); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern int SendInt(IntPtr receiver, IntPtr selector); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern int SendInt(IntPtr receiver, IntPtr selector, IntPtr ptr1); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern int SendInt(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern bool SendBool(IntPtr receiver, IntPtr selector); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern bool SendBool(IntPtr receiver, IntPtr selector, IntPtr ptr1); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern bool SendBool(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern void SendVoid(IntPtr receiver, IntPtr selector); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern void SendVoid(IntPtr receiver, IntPtr selector, IntPtr ptr1); + + [DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")] + public static extern void SendVoid(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2); + + private static readonly Type type_cocoa = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Cocoa"); + private static readonly MethodInfo method_cocoa_from_ns_string = type_cocoa.GetMethod("FromNSString"); + private static readonly MethodInfo method_cocoa_to_ns_string = type_cocoa.GetMethod("ToNSString"); + private static readonly MethodInfo method_cocoa_get_string_constant = type_cocoa.GetMethod("GetStringConstant"); + + public static IntPtr AppKitLibrary; + public static IntPtr FoundationLibrary; + + static Cocoa() + { + AppKitLibrary = (IntPtr)type_cocoa.GetField("AppKitLibrary").GetValue(null); + FoundationLibrary = (IntPtr)type_cocoa.GetField("FoundationLibrary").GetValue(null); + } + + public static string FromNSString(IntPtr handle) + { + return (string)method_cocoa_from_ns_string.Invoke(null, new object[] { handle }); + } + + public static IntPtr ToNSString(string str) + { + return (IntPtr)method_cocoa_to_ns_string.Invoke(null, new object[] { str }); + } + + public static IntPtr GetStringConstant(IntPtr handle, string symbol) + { + return (IntPtr)method_cocoa_get_string_constant.Invoke(null, new object[] { handle, symbol }); + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/NSArray.cs b/osu.Framework/Platform/MacOS/Native/NSArray.cs index 03ff3788c..a095ad6c5 100644 --- a/osu.Framework/Platform/MacOS/Native/NSArray.cs +++ b/osu.Framework/Platform/MacOS/Native/NSArray.cs @@ -1,47 +1,47 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal struct NSArray - { - internal IntPtr Handle { get; private set; } - - private static readonly IntPtr class_pointer = Class.Get("NSArray"); - private static readonly IntPtr mutable_class_pointer = Class.Get("NSMutableArray"); - private static readonly IntPtr sel_array_with_object = Selector.Get("arrayWithObject:"); - private static readonly IntPtr sel_array = Selector.Get("array"); - private static readonly IntPtr sel_add_object = Selector.Get("addObject:"); - private static readonly IntPtr sel_count = Selector.Get("count"); - private static readonly IntPtr sel_object_at_index = Selector.Get("objectAtIndex:"); - - internal NSArray(IntPtr handle) - { - Handle = handle; - } - - internal static NSArray ArrayWithObject(IntPtr obj) => new NSArray(Cocoa.SendIntPtr(class_pointer, sel_array_with_object, obj)); - - internal static NSArray ArrayWithObjects(IntPtr[] objs) - { - var mutableArray = Cocoa.SendIntPtr(mutable_class_pointer, sel_array); - foreach (IntPtr obj in objs) - Cocoa.SendVoid(mutableArray, sel_add_object, obj); - return new NSArray(mutableArray); - } - - internal int Count() => Cocoa.SendInt(Handle, sel_count); - - internal IntPtr ObjectAtIndex(int index) => Cocoa.SendIntPtr(Handle, sel_object_at_index, index); - - internal IntPtr[] ToArray() - { - IntPtr[] result = new IntPtr[Count()]; - for (int i = 0; i < result.Length; i++) - result[i] = ObjectAtIndex(i); - return result; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal struct NSArray + { + internal IntPtr Handle { get; private set; } + + private static readonly IntPtr class_pointer = Class.Get("NSArray"); + private static readonly IntPtr mutable_class_pointer = Class.Get("NSMutableArray"); + private static readonly IntPtr sel_array_with_object = Selector.Get("arrayWithObject:"); + private static readonly IntPtr sel_array = Selector.Get("array"); + private static readonly IntPtr sel_add_object = Selector.Get("addObject:"); + private static readonly IntPtr sel_count = Selector.Get("count"); + private static readonly IntPtr sel_object_at_index = Selector.Get("objectAtIndex:"); + + internal NSArray(IntPtr handle) + { + Handle = handle; + } + + internal static NSArray ArrayWithObject(IntPtr obj) => new NSArray(Cocoa.SendIntPtr(class_pointer, sel_array_with_object, obj)); + + internal static NSArray ArrayWithObjects(IntPtr[] objs) + { + var mutableArray = Cocoa.SendIntPtr(mutable_class_pointer, sel_array); + foreach (IntPtr obj in objs) + Cocoa.SendVoid(mutableArray, sel_add_object, obj); + return new NSArray(mutableArray); + } + + internal int Count() => Cocoa.SendInt(Handle, sel_count); + + internal IntPtr ObjectAtIndex(int index) => Cocoa.SendIntPtr(Handle, sel_object_at_index, index); + + internal IntPtr[] ToArray() + { + IntPtr[] result = new IntPtr[Count()]; + for (int i = 0; i < result.Length; i++) + result[i] = ObjectAtIndex(i); + return result; + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/NSDictionary.cs b/osu.Framework/Platform/MacOS/Native/NSDictionary.cs index 1657bf219..edbc3a2f0 100644 --- a/osu.Framework/Platform/MacOS/Native/NSDictionary.cs +++ b/osu.Framework/Platform/MacOS/Native/NSDictionary.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal struct NSDictionary - { - internal IntPtr Handle { get; private set; } - - private static IntPtr classPointer = Class.Get("NSDictionary"); - - internal NSDictionary(IntPtr handle) - { - Handle = handle; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal struct NSDictionary + { + internal IntPtr Handle { get; private set; } + + private static IntPtr classPointer = Class.Get("NSDictionary"); + + internal NSDictionary(IntPtr handle) + { + Handle = handle; + } + } +} diff --git a/osu.Framework/Platform/MacOS/Native/NSPasteboard.cs b/osu.Framework/Platform/MacOS/Native/NSPasteboard.cs index ea0ec3c6f..72fcc0781 100644 --- a/osu.Framework/Platform/MacOS/Native/NSPasteboard.cs +++ b/osu.Framework/Platform/MacOS/Native/NSPasteboard.cs @@ -1,38 +1,38 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal struct NSPasteboard - { - internal IntPtr Handle { get; private set; } - - private static readonly IntPtr class_pointer = Class.Get("NSPasteboard"); - private static readonly IntPtr sel_general_pasteboard = Selector.Get("generalPasteboard"); - private static readonly IntPtr sel_clear_contents = Selector.Get("clearContents"); - private static readonly IntPtr sel_can_read_object_for_classes = Selector.Get("canReadObjectForClasses:options:"); - private static readonly IntPtr sel_read_objects_for_classes = Selector.Get("readObjectsForClasses:options:"); - private static readonly IntPtr sel_write_objects = Selector.Get("writeObjects:"); - - internal NSPasteboard(IntPtr handle) - { - Handle = handle; - } - - internal static NSPasteboard GeneralPasteboard() => new NSPasteboard(Cocoa.SendIntPtr(class_pointer, sel_general_pasteboard)); - - internal int ClearContents() => Cocoa.SendInt(Handle, sel_clear_contents); - - internal bool CanReadObjectForClasses(NSArray classArray, NSDictionary? optionDict) => Cocoa.SendBool(Handle, sel_can_read_object_for_classes, classArray.Handle, optionDict?.Handle ?? IntPtr.Zero); - - internal NSArray? ReadObjectsForClasses(NSArray classArray, NSDictionary? optionDict) - { - var result = Cocoa.SendIntPtr(Handle, sel_read_objects_for_classes, classArray.Handle, optionDict?.Handle ?? IntPtr.Zero); - return result == IntPtr.Zero ? (NSArray?)null : new NSArray(result); - } - - internal bool WriteObjects(NSArray objects) => Cocoa.SendBool(Handle, sel_write_objects, objects.Handle); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal struct NSPasteboard + { + internal IntPtr Handle { get; private set; } + + private static readonly IntPtr class_pointer = Class.Get("NSPasteboard"); + private static readonly IntPtr sel_general_pasteboard = Selector.Get("generalPasteboard"); + private static readonly IntPtr sel_clear_contents = Selector.Get("clearContents"); + private static readonly IntPtr sel_can_read_object_for_classes = Selector.Get("canReadObjectForClasses:options:"); + private static readonly IntPtr sel_read_objects_for_classes = Selector.Get("readObjectsForClasses:options:"); + private static readonly IntPtr sel_write_objects = Selector.Get("writeObjects:"); + + internal NSPasteboard(IntPtr handle) + { + Handle = handle; + } + + internal static NSPasteboard GeneralPasteboard() => new NSPasteboard(Cocoa.SendIntPtr(class_pointer, sel_general_pasteboard)); + + internal int ClearContents() => Cocoa.SendInt(Handle, sel_clear_contents); + + internal bool CanReadObjectForClasses(NSArray classArray, NSDictionary? optionDict) => Cocoa.SendBool(Handle, sel_can_read_object_for_classes, classArray.Handle, optionDict?.Handle ?? IntPtr.Zero); + + internal NSArray? ReadObjectsForClasses(NSArray classArray, NSDictionary? optionDict) + { + var result = Cocoa.SendIntPtr(Handle, sel_read_objects_for_classes, classArray.Handle, optionDict?.Handle ?? IntPtr.Zero); + return result == IntPtr.Zero ? (NSArray?)null : new NSArray(result); + } + + internal bool WriteObjects(NSArray objects) => Cocoa.SendBool(Handle, sel_write_objects, objects.Handle); + } +} diff --git a/osu.Framework/Platform/MacOS/Native/Selector.cs b/osu.Framework/Platform/MacOS/Native/Selector.cs index b7df8eafb..416ca83ad 100644 --- a/osu.Framework/Platform/MacOS/Native/Selector.cs +++ b/osu.Framework/Platform/MacOS/Native/Selector.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using System.Reflection; - -namespace osu.Framework.Platform.MacOS.Native -{ - internal static class Selector - { - private static readonly Type type_selector = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Selector"); - private static readonly MethodInfo method_selector_get = type_selector.GetMethod("Get"); - - public static IntPtr Get(string name) - { - return (IntPtr)method_selector_get.Invoke(null, new object[] { name }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using System.Reflection; + +namespace osu.Framework.Platform.MacOS.Native +{ + internal static class Selector + { + private static readonly Type type_selector = typeof(OpenTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Selector"); + private static readonly MethodInfo method_selector_get = type_selector.GetMethod("Get"); + + public static IntPtr Get(string name) + { + return (IntPtr)method_selector_get.Invoke(null, new object[] { name }); + } + } +} diff --git a/osu.Framework/Platform/Storage.cs b/osu.Framework/Platform/Storage.cs index 5ed35fe80..8289e7572 100644 --- a/osu.Framework/Platform/Storage.cs +++ b/osu.Framework/Platform/Storage.cs @@ -1,121 +1,121 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using osu.Framework.IO.File; - -namespace osu.Framework.Platform -{ - public abstract class Storage - { - protected string BaseName { get; set; } - - protected readonly string BasePath; - - /// - /// An optional path to be added after . - /// - protected string SubDirectory { get; set; } = string.Empty; - - protected Storage(string baseName) - { - BaseName = FileSafety.FilenameStrip(baseName); - BasePath = LocateBasePath(); - if (BasePath == null) - throw new NullReferenceException(nameof(BasePath)); - } - - /// - /// Find the location which will be used as a root for this storage. - /// This should usually be a platform-specific implementation. - /// - /// - protected abstract string LocateBasePath(); - - /// - /// Get a Storage-usable path for the provided path. - /// - /// An incomplete path, usually provided as user input. - /// Create the path if it doesn't already exist. - /// - protected string GetUsablePathFor(string path, bool createIfNotExisting = false) - { - var resolvedPath = Path.Combine(BasePath, BaseName, SubDirectory, path); - if (createIfNotExisting) Directory.CreateDirectory(Path.GetDirectoryName(resolvedPath)); - return resolvedPath; - } - - /// - /// Check whether a file exists at the specified path. - /// - /// The path to check. - /// Whether a file exists. - public abstract bool Exists(string path); - - /// - /// Check whether a directory exists at the specified path. - /// - /// The path to check. - /// Whether a directory exists. - public abstract bool ExistsDirectory(string path); - - /// - /// Delete a directory and all its contents recursively. - /// - /// The path of the directory to delete. - public abstract void DeleteDirectory(string path); - - /// - /// Delete a file. - /// - /// The path of the file to delete. - public abstract void Delete(string path); - - /// - /// Retrieve a list of directories at the specified path. - /// - /// The path to list. - /// A list of directories in the path, relative to the path. - public abstract string[] GetDirectories(string path); - - /// - /// Retrieve a for a contained directory. - /// - /// The subdirectory to use as a root. - /// A more specific storage. - public Storage GetStorageForDirectory(string path) - { - var clone = (Storage)MemberwiseClone(); - clone.SubDirectory = path; - return clone; - } - - /// - /// Retrieve a stream from an underlying file inside this storage. - /// - /// The path of the file. - /// The access requirements. - /// The mode in which the file should be opened. - /// A stream associated with the requested path. - public abstract Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate); - - /// - /// Retrieve an SQLite database connection string from within this storage. - /// - /// The name of the database. - /// An SQLite connection string. - public abstract string GetDatabaseConnectionString(string name); - - /// - /// Delete an SQLite database from within this storage. - /// - /// The name of the database to delete. - public abstract void DeleteDatabase(string name); - - /// - /// Opens a native file browser window to the root path of this storage. - /// - public abstract void OpenInNativeExplorer(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using osu.Framework.IO.File; + +namespace osu.Framework.Platform +{ + public abstract class Storage + { + protected string BaseName { get; set; } + + protected readonly string BasePath; + + /// + /// An optional path to be added after . + /// + protected string SubDirectory { get; set; } = string.Empty; + + protected Storage(string baseName) + { + BaseName = FileSafety.FilenameStrip(baseName); + BasePath = LocateBasePath(); + if (BasePath == null) + throw new NullReferenceException(nameof(BasePath)); + } + + /// + /// Find the location which will be used as a root for this storage. + /// This should usually be a platform-specific implementation. + /// + /// + protected abstract string LocateBasePath(); + + /// + /// Get a Storage-usable path for the provided path. + /// + /// An incomplete path, usually provided as user input. + /// Create the path if it doesn't already exist. + /// + protected string GetUsablePathFor(string path, bool createIfNotExisting = false) + { + var resolvedPath = Path.Combine(BasePath, BaseName, SubDirectory, path); + if (createIfNotExisting) Directory.CreateDirectory(Path.GetDirectoryName(resolvedPath)); + return resolvedPath; + } + + /// + /// Check whether a file exists at the specified path. + /// + /// The path to check. + /// Whether a file exists. + public abstract bool Exists(string path); + + /// + /// Check whether a directory exists at the specified path. + /// + /// The path to check. + /// Whether a directory exists. + public abstract bool ExistsDirectory(string path); + + /// + /// Delete a directory and all its contents recursively. + /// + /// The path of the directory to delete. + public abstract void DeleteDirectory(string path); + + /// + /// Delete a file. + /// + /// The path of the file to delete. + public abstract void Delete(string path); + + /// + /// Retrieve a list of directories at the specified path. + /// + /// The path to list. + /// A list of directories in the path, relative to the path. + public abstract string[] GetDirectories(string path); + + /// + /// Retrieve a for a contained directory. + /// + /// The subdirectory to use as a root. + /// A more specific storage. + public Storage GetStorageForDirectory(string path) + { + var clone = (Storage)MemberwiseClone(); + clone.SubDirectory = path; + return clone; + } + + /// + /// Retrieve a stream from an underlying file inside this storage. + /// + /// The path of the file. + /// The access requirements. + /// The mode in which the file should be opened. + /// A stream associated with the requested path. + public abstract Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate); + + /// + /// Retrieve an SQLite database connection string from within this storage. + /// + /// The name of the database. + /// An SQLite connection string. + public abstract string GetDatabaseConnectionString(string name); + + /// + /// Delete an SQLite database from within this storage. + /// + /// The name of the database to delete. + public abstract void DeleteDatabase(string name); + + /// + /// Opens a native file browser window to the root path of this storage. + /// + public abstract void OpenInNativeExplorer(); + } +} diff --git a/osu.Framework/Platform/TcpIpcProvider.cs b/osu.Framework/Platform/TcpIpcProvider.cs index 2eab15f71..7b2807ac6 100644 --- a/osu.Framework/Platform/TcpIpcProvider.cs +++ b/osu.Framework/Platform/TcpIpcProvider.cs @@ -1,122 +1,122 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace osu.Framework.Platform -{ - public class TcpIpcProvider : IDisposable - { - private const int ipc_port = 45356; - - private TcpListener listener; - private CancellationTokenSource cancelListener; - - public event Action MessageReceived; - - public bool Bind() - { - listener = new TcpListener(IPAddress.Loopback, ipc_port); - try - { - listener.Start(); - cancelListener = new CancellationTokenSource(); - return true; - } - catch (SocketException ex) - { - listener = null; - if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) - return false; - - Console.WriteLine($@"Unhandled exception initializing IPC server: {ex}"); - return false; - } - } - - public async Task StartAsync() - { - var token = cancelListener.Token; - try - { - while (!token.IsCancellationRequested) - { - while (!listener.Pending()) - { - await Task.Delay(10, token); - if (token.IsCancellationRequested) - return; - } - - using (var client = await listener.AcceptTcpClientAsync()) - { - using (var stream = client.GetStream()) - { - byte[] header = new byte[sizeof(int)]; - await stream.ReadAsync(header, 0, sizeof(int), token); - int len = BitConverter.ToInt32(header, 0); - byte[] data = new byte[len]; - await stream.ReadAsync(data, 0, len, token); - var str = Encoding.UTF8.GetString(data); - var json = JToken.Parse(str); - var type = Type.GetType(json["Type"].Value()); - Trace.Assert(type != null); - var msg = new IpcMessage - { - // ReSharper disable once PossibleNullReferenceException - Type = type.AssemblyQualifiedName, - Value = JsonConvert.DeserializeObject( - json["Value"].ToString(), type), - }; - MessageReceived?.Invoke(msg); - } - } - } - } - catch (TaskCanceledException) - { - } - finally - { - try - { - listener.Stop(); - } - catch - { - } - } - } - - public async Task SendMessageAsync(IpcMessage message) - { - using (var client = new TcpClient()) - { - await client.ConnectAsync(IPAddress.Loopback, ipc_port); - using (var stream = client.GetStream()) - { - var str = JsonConvert.SerializeObject(message, Formatting.None); - byte[] data = Encoding.UTF8.GetBytes(str); - byte[] header = BitConverter.GetBytes(data.Length); - await stream.WriteAsync(header, 0, header.Length); - await stream.WriteAsync(data, 0, data.Length); - await stream.FlushAsync(); - } - } - } - - public void Dispose() - { - if (listener != null) - cancelListener.Cancel(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Framework.Platform +{ + public class TcpIpcProvider : IDisposable + { + private const int ipc_port = 45356; + + private TcpListener listener; + private CancellationTokenSource cancelListener; + + public event Action MessageReceived; + + public bool Bind() + { + listener = new TcpListener(IPAddress.Loopback, ipc_port); + try + { + listener.Start(); + cancelListener = new CancellationTokenSource(); + return true; + } + catch (SocketException ex) + { + listener = null; + if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) + return false; + + Console.WriteLine($@"Unhandled exception initializing IPC server: {ex}"); + return false; + } + } + + public async Task StartAsync() + { + var token = cancelListener.Token; + try + { + while (!token.IsCancellationRequested) + { + while (!listener.Pending()) + { + await Task.Delay(10, token); + if (token.IsCancellationRequested) + return; + } + + using (var client = await listener.AcceptTcpClientAsync()) + { + using (var stream = client.GetStream()) + { + byte[] header = new byte[sizeof(int)]; + await stream.ReadAsync(header, 0, sizeof(int), token); + int len = BitConverter.ToInt32(header, 0); + byte[] data = new byte[len]; + await stream.ReadAsync(data, 0, len, token); + var str = Encoding.UTF8.GetString(data); + var json = JToken.Parse(str); + var type = Type.GetType(json["Type"].Value()); + Trace.Assert(type != null); + var msg = new IpcMessage + { + // ReSharper disable once PossibleNullReferenceException + Type = type.AssemblyQualifiedName, + Value = JsonConvert.DeserializeObject( + json["Value"].ToString(), type), + }; + MessageReceived?.Invoke(msg); + } + } + } + } + catch (TaskCanceledException) + { + } + finally + { + try + { + listener.Stop(); + } + catch + { + } + } + } + + public async Task SendMessageAsync(IpcMessage message) + { + using (var client = new TcpClient()) + { + await client.ConnectAsync(IPAddress.Loopback, ipc_port); + using (var stream = client.GetStream()) + { + var str = JsonConvert.SerializeObject(message, Formatting.None); + byte[] data = Encoding.UTF8.GetBytes(str); + byte[] header = BitConverter.GetBytes(data.Length); + await stream.WriteAsync(header, 0, header.Length); + await stream.WriteAsync(data, 0, data.Length); + await stream.FlushAsync(); + } + } + } + + public void Dispose() + { + if (listener != null) + cancelListener.Cancel(); + } + } +} diff --git a/osu.Framework/Platform/Windows/Native/Execution.cs b/osu.Framework/Platform/Windows/Native/Execution.cs index 78b4546b9..c318e8991 100644 --- a/osu.Framework/Platform/Windows/Native/Execution.cs +++ b/osu.Framework/Platform/Windows/Native/Execution.cs @@ -1,24 +1,24 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; - -namespace osu.Framework.Platform.Windows.Native -{ - internal static class Execution - { - [DllImport("kernel32.dll")] - internal static extern uint SetThreadExecutionState(ExecutionState state); - - [Flags] - internal enum ExecutionState : uint - { - AwaymodeRequired = 0x00000040, - Continuous = 0x80000000, - DisplayRequired = 0x00000002, - SystemRequired = 0x00000001, - UserPresent = 0x00000004, - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; + +namespace osu.Framework.Platform.Windows.Native +{ + internal static class Execution + { + [DllImport("kernel32.dll")] + internal static extern uint SetThreadExecutionState(ExecutionState state); + + [Flags] + internal enum ExecutionState : uint + { + AwaymodeRequired = 0x00000040, + Continuous = 0x80000000, + DisplayRequired = 0x00000002, + SystemRequired = 0x00000001, + UserPresent = 0x00000004, + } + } +} diff --git a/osu.Framework/Platform/Windows/Native/Input.cs b/osu.Framework/Platform/Windows/Native/Input.cs index b5d3d1f92..356870a6e 100644 --- a/osu.Framework/Platform/Windows/Native/Input.cs +++ b/osu.Framework/Platform/Windows/Native/Input.cs @@ -1,2649 +1,2649 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using OpenTK; - -namespace osu.Framework.Platform.Windows.Native -{ - internal static class Input - { - [DllImport("user32.dll")] - public static extern bool RegisterTouchWindow(IntPtr hWnd, int flags); - - [DllImport(@"user32.dll")] - public static extern int SetProp(IntPtr hWnd, string lpString, int hData); - - [DllImport(@"user32.dll")] - public static extern int RemoveProp(IntPtr hWnd, string lpString); - - [DllImport("user32.dll")] - public static extern int GetSystemMetrics(int nIndex); - - [DllImport("user32.dll")] - public static extern bool RegisterRawInputDevices( - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] - RawInputDevice[] pRawInputDevices, - int uiNumDevices, - int cbSize); - - [DllImport("user32.dll")] - public static extern bool GetTouchInputInfo( - IntPtr hTouchInput, - int uiNumDevices, - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] - RawTouchInput[] pRawTouchInputs, - int cbSize); - - [DllImport("user32.dll")] - public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); - - [DllImport("user32.dll")] - public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, out RawInput pData, ref int pcbSize, int cbSizeHeader); - - [DllImport("user32.dll")] - public static extern bool GetPointerInfo(int pointerID, out RawPointerInput type); - - internal static Rectangle GetVirtualScreenRect() => new Rectangle( - GetSystemMetrics(SM_XVIRTUALSCREEN), - GetSystemMetrics(SM_YVIRTUALSCREEN), - GetSystemMetrics(SM_CXVIRTUALSCREEN), - GetSystemMetrics(SM_CYVIRTUALSCREEN) - ); - - public const int SM_XVIRTUALSCREEN = 76; - public const int SM_YVIRTUALSCREEN = 77; - - public const int SM_CXVIRTUALSCREEN = 78; - public const int SM_CYVIRTUALSCREEN = 79; - - public const int WM_MOUSEACTIVATE = 0x21; - - public const int WM_NCPOINTERUPDATE = 0x0241; - public const int WM_NCPOINTERDOWN = 0x0242; - public const int WM_NCPOINTERUP = 0x0243; - public const int WM_POINTERUPDATE = 0x0245; - public const int WM_POINTERDOWN = 0x0246; - public const int WM_POINTERUP = 0x0247; - public const int WM_POINTERENTER = 0x0249; - public const int WM_POINTERLEAVE = 0x024A; - public const int WM_POINTERACTIVATE = 0x024B; - public const int WM_POINTERCAPTURECHANGED = 0x024C; - public const int WM_POINTERWHEEL = 0x024E; - public const int WM_POINTERHWHEEL = 0x024F; - - public const int WM_INPUT = 0x00FF; - public const int WM_TOUCH = 0x0240; - - public const int TWF_FINETOUCH = 0x00000001; - public const int TWF_WANTPALM = 0x00000002; - - public const int TABLET_DISABLE_PRESSANDHOLD = 0x00000001; - public const int TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008; - public const int TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010; - public const int TABLET_DISABLE_TOUCHUIFORCEON = 0x00000100; - public const int TABLET_DISABLE_TOUCHUIFORCEOFF = 0x00000200; - public const int TABLET_DISABLE_TOUCHSWITCH = 0x00008000; - public const int TABLET_DISABLE_FLICKS = 0x00010000; - public const int TABLET_ENABLE_FLICKSONCONTEXT = 0x00020000; - public const int TABLET_ENABLE_FLICKLEARNINGMODE = 0x00040000; - public const int TABLET_DISABLE_SMOOTHSCROLLING = 0x00080000; - public const int TABLET_DISABLE_FLICKFALLBACKKEYS = 0x00100000; - public const int TABLET_ENABLE_MULTITOUCHDATA = 0x01000000; - } - - /// - /// Enumeration containing pointer types. - /// - public enum RawPointerType : uint - { - Generic = 0x00000001, - Touch = 0x00000002, - Pen = 0x00000003, - Mouse = 0x00000004, - Touchpad = 0x00000005, - } - - public enum RawPointerButtonType : uint - { - None = 0, - FirstButtonDown, - FirstButtonUp, - SecondButtonDown, - SecondButtonUp, - ThirdButtonDown, - ThirdButtonUp, - FourthButtonDown, - FourthButtonUp, - FifthButtonDown, - FifthButtonUp, - } - - /// - /// Enumeration containing pointer flags. - /// - [Flags] - public enum RawPointerFlags : uint - { - /// - /// Default. - /// - None = 0x00000000, - - /// - /// Indicates the arrival of a new pointer. - /// - New = 0x00000001, - - /// - /// Indicates that this pointer continues to exist. When this flag is not set, it indicates the pointer has left detection range. - /// This flag is typically not set only when a hovering pointer leaves detection range (POINTER_FLAG_UPDATE is set) or when a pointer in contact with a window surface leaves detection range (POINTER_FLAG_UP is set). - /// - InRange = 0x00000002, - - /// - /// Indicates that this pointer is in contact with the digitizer surface. When this flag is not set, it indicates a hovering pointer. - /// - InContact = 0x00000004, - - /// - /// Indicates a primary action, analogous to a left mouse button down. - /// A touch pointer has this flag set when it is in contact with the digitizer surface. - /// A pen pointer has this flag set when it is in contact with the digitizer surface with no buttons pressed. - /// A mouse pointer has this flag set when the left mouse button is down. - /// - FirstButton = 0x00000010, - - /// - /// Indicates a secondary action, analogous to a right mouse button down. - /// A touch pointer does not use this flag. - /// A pen pointer has this flag set when it is in contact with the digitizer surface with the pen barrel button pressed. - /// A mouse pointer has this flag set when the right mouse button is down. - /// - SecondButton = 0x00000020, - - /// - /// Analogous to a mouse wheel button down. - /// A touch pointer does not use this flag. - /// A pen pointer does not use this flag. - /// A mouse pointer has this flag set when the mouse wheel button is down. - /// - ThirdButton = 0x00000040, - - /// - /// Analogous to a first extended mouse (XButton1) button down. - /// A touch pointer does not use this flag. - /// A pen pointer does not use this flag. - /// A mouse pointer has this flag set when the first extended mouse (XBUTTON1) button is down. - /// - FourthButton = 0x00000080, - - /// - /// Analogous to a second extended mouse (XButton2) button down. - /// A touch pointer does not use this flag. - /// A pen pointer does not use this flag. - /// A mouse pointer has this flag set when the second extended mouse (XBUTTON2) button is down. - /// - FifthButton = 0x00000100, - - /// - /// Indicates that this pointer has been designated as the primary pointer. A primary pointer is a single pointer that can perform actions beyond those available to non-primary pointers. For example, when a primary pointer makes contact with a window’s surface, it may provide the window an opportunity to activate by sending it a WM_POINTERACTIVATE message. - /// The primary pointer is identified from all current user interactions on the system (mouse, touch, pen, and so on). As such, the primary pointer might not be associated with your app. The first contact in a multi-touch interaction is set as the primary pointer. Once a primary pointer is identified, all contacts must be lifted before a new contact can be identified as a primary pointer. For apps that don't process pointer input, only the primary pointer's events are promoted to mouse events. - /// - Primary = 0x00002000, - - /// - /// Confidence is a suggestion from the source device about whether the pointer represents an intended or accidental interaction, which is especially relevant for PT_TOUCH pointers where an accidental interaction (such as with the palm of the hand) can trigger input. The presence of this flag indicates that the source device has high confidence that this input is part of an intended interaction. - /// - Confidence = 0x000004000, - - /// - /// Indicates that the pointer is departing in an abnormal manner, such as when the system receives invalid input for the pointer or when a device with active pointers departs abruptly. If the application receiving the input is in a position to do so, it should treat the interaction as not completed and reverse any effects of the concerned pointer. - /// - Canceled = 0x000008000, - - /// - /// Indicates that this pointer transitioned to a down state; that is, it made contact with the digitizer surface. - /// - Down = 0x00010000, - - /// - /// Indicates that this is a simple update that does not include pointer state changes. - /// - Update = 0x00020000, - - /// - /// Indicates that this pointer transitioned to an up state; that is, it broke contact with the digitizer surface. - /// - Up = 0x00040000, - - /// - /// Indicates input associated with a pointer wheel. For mouse pointers, this is equivalent to the action of the mouse scroll wheel (WM_MOUSEWHEEL). - /// - Wheel = 0x00080000, - - /// - /// Indicates input associated with a pointer h-wheel. For mouse pointers, this is equivalent to the action of the mouse horizontal scroll wheel (WM_MOUSEHWHEEL). - /// - HWheel = 0x00100000, - - /// - /// Indicates that this pointer was captured by (associated with) another element and the original element has lost capture (see WM_POINTERCAPTURECHANGED). - /// - CaptureChanged = 0x00200000, - } - - /// - /// Contains information about the state of a touch input - /// - [StructLayout(LayoutKind.Sequential)] - public struct RawPointerInput - { - public RawPointerType Type; - public int ID; - public uint FrameID; - public RawPointerFlags Flags; - public IntPtr SourceDevice; - public IntPtr TargetWindow; - public Point PixelLocation; - public Point HimetricLocation; - public Point PixelLocationRaw; - public Point HimetricLocationRaw; - public int Time; - public uint HistoryCount; - public int InputData; - public uint KeyStates; - public ulong PerformanceCount; - public RawPointerButtonType ButtonChangeType; - } - - /// - /// Contains information about the state of a touch input - /// - [StructLayout(LayoutKind.Sequential)] - public struct RawTouchInput - { - /// - /// The x-coordinate (horizontal point) of the touch input. This member is indicated in hundredths of a pixel of physical screen coordinates. - /// - public int X; - - /// - /// The y-coordinate (vertical point) of the touch input. This member is indicated in hundredths of a pixel of physical screen coordinates. - /// - public int Y; - - /// - /// A device handle for the source input device. Each device is given a unique provider at run time by the touch input provider. - /// - public IntPtr Source; - - /// - /// A touch point identifier that distinguishes a particular touch input. This value stays consistent in a touch contact sequence from the point a contact comes down until it comes back up. An ID may be reused later for subsequent contacts. - /// - public int ID; - - /// - /// A set of bit flags that specify various aspects of touch point press, release, and motion. The bits in this member can be any reasonable combination of the values in the Remarks section. - /// - public RawTouchFlags Flags; - - /// - /// A set of bit flags that specify which of the optional fields in the structure contain valid values. The availability of valid information in the optional fields is device-specific. Applications should use an optional field value only when the corresponding bit is set in Mask.. - /// - public RawTouchMaskFlags Mask; - - /// - /// The time stamp for the event, in milliseconds. The consuming application should note that the system performs no validation on this field; when the TOUCHINPUTMASKF_TIMEFROMSYSTEM flag is not set, the accuracy and sequencing of values in this field are completely dependent on the touch input provider. - /// - public int Time; - - /// - /// An additional value associated with the touch event. - /// - public int ExtraInfo; - - /// - /// The width of the touch contact area in hundredths of a pixel in physical screen coordinates. This value is only valid if the Mask member has the TOUCHEVENTFMASK_CONTACTAREA flag set. - /// - public uint AreaWidth; - - /// - /// The height of the touch contact area in hundredths of a pixel in physical screen coordinates. This value is only valid if the Mask member has the TOUCHEVENTFMASK_CONTACTAREA flag set. - /// - public uint AreaHeight; - } - - /// - /// Enumeration containing flags for raw touch input. - /// - [Flags] - public enum RawTouchFlags : uint - { - /// - /// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN. - /// - Move = 0x0001, - - /// - /// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP. - /// - Down = 0x0002, - - /// - /// A touch point was removed. - /// - Up = 0x0004, - - /// - /// A touch point is in range. This flag is used to enable touch hover support on compatible hardware. Applications that do not want support for hover can ignore this flag. - /// - InRange = 0x0008, - - /// - /// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following FontText for more information on primary touch points. - /// - Primary = 0x0010, - - /// - /// When received using GetTouchInputInfo, this input was not coalesced. - /// - NoCoalesce = 0x0020, - - /// - /// The touch event came from the user's palm. - /// - Palm = 0x0080, - } - - /// - /// Enumeration containing mask flags for raw touch input. - /// - [Flags] - public enum RawTouchMaskFlags : uint - { - /// - /// AreaWidth and AreaHeight are valid. - /// - ContactArea = 0x0004, - - /// - /// ExtraInfo is valid. - /// - ExtraInfo = 0x0002, - - /// - /// The system time was set in the TOUCHINPUT structure. - /// - TimeFromSystem = 0x0001, - } - - /// - /// Value type for raw input. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct RawInput - { - public RawInputHeader Header; - public RawInputData Data; - - public static readonly int SizeInBytes = - BlittableValueType.Stride; - } - - [StructLayout(LayoutKind.Explicit)] - public struct RawInputData - { - [FieldOffset(0)] public RawMouse Mouse; - [FieldOffset(0)] public RawKeyboard Keyboard; - [FieldOffset(0)] public RawInputHid HID; - } - - //unused structs - /// - /// Value type for raw input from a keyboard. - /// - [StructLayout(LayoutKind.Sequential)] - public struct RawKeyboard - { - /// Scan code for key depression. - public short MakeCode; - - /// Scan code information. - public RawKeyboardFlags Flags; - - /// Reserved. - public short Reserved; - - /// Virtual key code. - public VirtualKeys VirtualKey; - - /// Corresponding window message. - public WindowsMessages Message; - - /// Extra information. - public int ExtraInformation; - } - - /// - /// Enumeration containing flags for raw keyboard input. - /// - [Flags] - public enum RawKeyboardFlags : ushort - { - /// - KeyMake = 0, - - /// - KeyBreak = 1, - - /// - KeyE0 = 2, - - /// - KeyE1 = 4, - - /// - TerminalServerSetLED = 8, - - /// - TerminalServerShadow = 0x10, - - /// - TerminalServerVKPACKET = 0x20 - } - - public struct RawInputHid - { - } - - /// - /// Contains information about the state of the mouse. - /// - [StructLayout(LayoutKind.Explicit)] - public struct RawMouse - { - /// - /// The mouse state. - /// - [FieldOffset(0)] public RawMouseFlags Flags; - - /// - /// Flags for the event. - /// - [FieldOffset(4)] public RawMouseButtons ButtonFlags; - - /// - /// If the mouse wheel is moved, this will contain the delta amount. - /// - [FieldOffset(6)] public short ButtonData; - - /// - /// Raw button data. - /// - [FieldOffset(8)] public uint RawButtons; - - /// - /// The motion in the X direction. This is signed relative motion or - /// absolute motion, depending on the value of usFlags. - /// - [FieldOffset(12)] public int LastX; - - /// - /// The motion in the Y direction. This is signed relative motion or absolute motion, - /// depending on the value of usFlags. - /// - [FieldOffset(16)] public int LastY; - - /// - /// The device-specific additional information for the event. - /// - [FieldOffset(20)] public uint ExtraInformation; - } - - /// - /// Enumeration containing the flags for raw mouse data. - /// - [Flags] - public enum RawMouseFlags - : ushort - { - /// Relative to the last position. - MoveRelative = 0, - - /// Absolute positioning. - MoveAbsolute = 1, - - /// Coordinate data is mapped to a virtual desktop. - VirtualDesktop = 2, - - /// Attributes for the mouse have changed. - AttributesChanged = 4, - - /// WM_MOUSEMOVE and WM_INPUT don't coalesce - MoveNoCoalesce = 8, - } - - /// - /// Enumeration containing the button data for raw mouse input. - /// - [Flags] - public enum RawMouseButtons - : ushort - { - /// No button. - None = 0, - - /// Left (button 1) down. - LeftDown = 0x0001, - - /// Left (button 1) up. - LeftUp = 0x0002, - - /// Right (button 2) down. - RightDown = 0x0004, - - /// Right (button 2) up. - RightUp = 0x0008, - - /// Middle (button 3) down. - MiddleDown = 0x0010, - - /// Middle (button 3) up. - MiddleUp = 0x0020, - - /// Button 4 down. - Button4Down = 0x0040, - - /// Button 4 up. - Button4Up = 0x0080, - - /// Button 5 down. - Button5Down = 0x0100, - - /// Button 5 up. - Button5Up = 0x0200, - - /// Mouse wheel moved. - MouseWheel = 0x0400 - } - - /// - /// Enumeration contanining the command types to issue. - /// - public enum RawInputCommand - { - /// - /// Get input data. - /// - Input = 0x10000003, - - /// - /// Get header data. - /// - Header = 0x10000005 - } - - /// - /// Enumeration containing the type device the raw input is coming from. - /// - public enum RawInputType - { - /// - /// Mouse input. - /// - Mouse = 0, - - /// - /// Keyboard input. - /// - Keyboard = 1, - - /// - /// Another device that is not the keyboard or the mouse. - /// - HID = 2 - } - - /// - /// Value type for a raw input header. - /// - [StructLayout(LayoutKind.Sequential)] - public struct RawInputHeader - { - /// Type of device the input is coming from. - public RawInputType Type; - - /// Size of the packet of data. - public int Size; - - /// Handle to the device sending the data. - public IntPtr Device; - - /// wParam from the window message. - public IntPtr wParam; - } - - [StructLayout(LayoutKind.Sequential)] - public struct RawInputDevice - { - /// Top level collection Usage page for the raw input device. - public HIDUsagePage UsagePage; - - /// Top level collection Usage for the raw input device. - public HIDUsage Usage; - - /// Mode flag that specifies how to interpret the information provided by UsagePage and Usage. - public RawInputDeviceFlags Flags; - - /// Handle to the target device. If NULL, it follows the keyboard focus. - public IntPtr WindowHandle; - } - - /// Enumeration containing flags for a raw input device. - [Flags] - public enum RawInputDeviceFlags - { - /// No flags. - None = 0, - - /// If set, this removes the top level collection from the inclusion list. This tells the operating system to stop reading from a device which matches the top level collection. - Remove = 0x00000001, - - /// If set, this specifies the top level collections to exclude when reading a complete usage page. This flag only affects a TLC whose usage page is already specified with PageOnly. - Exclude = 0x00000010, - - /// If set, this specifies all devices whose top level collection is from the specified usUsagePage. Note that Usage must be zero. To exclude a particular top level collection, use Exclude. - PageOnly = 0x00000020, - - /// If set, this prevents any devices specified by UsagePage or Usage from generating legacy messages. This is only for the mouse and keyboard. - NoLegacy = 0x00000030, - - /// If set, this enables the caller to receive the input even when the caller is not in the foreground. Note that WindowHandle must be specified. - InputSink = 0x00000100, - - /// If set, the mouse button click does not activate the other window. - CaptureMouse = 0x00000200, - - /// If set, the application-defined keyboard device hotkeys are not handled. However, the system hotkeys; for example, ALT+TAB and CTRL+ALT+DEL, are still handled. By default, all keyboard hotkeys are handled. NoHotKeys can be specified even if NoLegacy is not specified and WindowHandle is NULL. - NoHotKeys = 0x00000200, - - /// If set, application keys are handled. NoLegacy must be specified. Keyboard only. - AppKeys = 0x00000400 - } - - /// - /// Enumeration for virtual keys. - /// - public enum VirtualKeys - : ushort - { - /// - LeftButton = 0x01, - - /// - RightButton = 0x02, - - /// - Cancel = 0x03, - - /// - MiddleButton = 0x04, - - /// - ExtraButton1 = 0x05, - - /// - ExtraButton2 = 0x06, - - /// - Back = 0x08, - - /// - Tab = 0x09, - - /// - Clear = 0x0C, - - /// - Return = 0x0D, - - /// - Shift = 0x10, - - /// - Control = 0x11, - - /// - Menu = 0x12, - - /// - Pause = 0x13, - - /// - CapsLock = 0x14, - - /// - Kana = 0x15, - - /// - Hangeul = 0x15, - - /// - Hangul = 0x15, - - /// - Junja = 0x17, - - /// - Final = 0x18, - - /// - Hanja = 0x19, - - /// - Kanji = 0x19, - - /// - Escape = 0x1B, - - /// - Convert = 0x1C, - - /// - NonConvert = 0x1D, - - /// - Accept = 0x1E, - - /// - ModeChange = 0x1F, - - /// - Space = 0x20, - - /// - Prior = 0x21, - - /// - Next = 0x22, - - /// - End = 0x23, - - /// - Home = 0x24, - - /// - Left = 0x25, - - /// - Up = 0x26, - - /// - Right = 0x27, - - /// - Down = 0x28, - - /// - Select = 0x29, - - /// - Print = 0x2A, - - /// - Execute = 0x2B, - - /// - Snapshot = 0x2C, - - /// - Insert = 0x2D, - - /// - Delete = 0x2E, - - /// - Help = 0x2F, - - /// - N0 = 0x30, - - /// - N1 = 0x31, - - /// - N2 = 0x32, - - /// - N3 = 0x33, - - /// - N4 = 0x34, - - /// - N5 = 0x35, - - /// - N6 = 0x36, - - /// - N7 = 0x37, - - /// - N8 = 0x38, - - /// - N9 = 0x39, - - /// - A = 0x41, - - /// - B = 0x42, - - /// - C = 0x43, - - /// - D = 0x44, - - /// - E = 0x45, - - /// - F = 0x46, - - /// - G = 0x47, - - /// - H = 0x48, - - /// - I = 0x49, - - /// - J = 0x4A, - - /// - K = 0x4B, - - /// - L = 0x4C, - - /// - M = 0x4D, - - /// - N = 0x4E, - - /// - O = 0x4F, - - /// - P = 0x50, - - /// - Q = 0x51, - - /// - R = 0x52, - - /// - S = 0x53, - - /// - T = 0x54, - - /// - U = 0x55, - - /// - V = 0x56, - - /// - W = 0x57, - - /// - X = 0x58, - - /// - Y = 0x59, - - /// - Z = 0x5A, - - /// - LeftWindows = 0x5B, - - /// - RightWindows = 0x5C, - - /// - Application = 0x5D, - - /// - Sleep = 0x5F, - - /// - Numpad0 = 0x60, - - /// - Numpad1 = 0x61, - - /// - Numpad2 = 0x62, - - /// - Numpad3 = 0x63, - - /// - Numpad4 = 0x64, - - /// - Numpad5 = 0x65, - - /// - Numpad6 = 0x66, - - /// - Numpad7 = 0x67, - - /// - Numpad8 = 0x68, - - /// - Numpad9 = 0x69, - - /// - Multiply = 0x6A, - - /// - Add = 0x6B, - - /// - Separator = 0x6C, - - /// - Subtract = 0x6D, - - /// - Decimal = 0x6E, - - /// - Divide = 0x6F, - - /// - F1 = 0x70, - - /// - F2 = 0x71, - - /// - F3 = 0x72, - - /// - F4 = 0x73, - - /// - F5 = 0x74, - - /// - F6 = 0x75, - - /// - F7 = 0x76, - - /// - F8 = 0x77, - - /// - F9 = 0x78, - - /// - F10 = 0x79, - - /// - F11 = 0x7A, - - /// - F12 = 0x7B, - - /// - F13 = 0x7C, - - /// - F14 = 0x7D, - - /// - F15 = 0x7E, - - /// - F16 = 0x7F, - - /// - F17 = 0x80, - - /// - F18 = 0x81, - - /// - F19 = 0x82, - - /// - F20 = 0x83, - - /// - F21 = 0x84, - - /// - F22 = 0x85, - - /// - F23 = 0x86, - - /// - F24 = 0x87, - - /// - NumLock = 0x90, - - /// - ScrollLock = 0x91, - - /// - NEC_Equal = 0x92, - - /// - Fujitsu_Jisho = 0x92, - - /// - Fujitsu_Masshou = 0x93, - - /// - Fujitsu_Touroku = 0x94, - - /// - Fujitsu_Loya = 0x95, - - /// - Fujitsu_Roya = 0x96, - - /// - LeftShift = 0xA0, - - /// - RightShift = 0xA1, - - /// - LeftControl = 0xA2, - - /// - RightControl = 0xA3, - - /// - LeftMenu = 0xA4, - - /// - RightMenu = 0xA5, - - /// - BrowserBack = 0xA6, - - /// - BrowserForward = 0xA7, - - /// - BrowserRefresh = 0xA8, - - /// - BrowserStop = 0xA9, - - /// - BrowserSearch = 0xAA, - - /// - BrowserFavorites = 0xAB, - - /// - BrowserHome = 0xAC, - - /// - VolumeMute = 0xAD, - - /// - VolumeDown = 0xAE, - - /// - VolumeUp = 0xAF, - - /// - MediaNextTrack = 0xB0, - - /// - MediaPrevTrack = 0xB1, - - /// - MediaStop = 0xB2, - - /// - MediaPlayPause = 0xB3, - - /// - LaunchMail = 0xB4, - - /// - LaunchMediaSelect = 0xB5, - - /// - LaunchApplication1 = 0xB6, - - /// - LaunchApplication2 = 0xB7, - - /// - OEM1 = 0xBA, - - /// - OEMPlus = 0xBB, - - /// - OEMComma = 0xBC, - - /// - OEMMinus = 0xBD, - - /// - OEMPeriod = 0xBE, - - /// - OEM2 = 0xBF, - - /// - OEM3 = 0xC0, - - /// - OEM4 = 0xDB, - - /// - OEM5 = 0xDC, - - /// - OEM6 = 0xDD, - - /// - OEM7 = 0xDE, - - /// - OEM8 = 0xDF, - - /// - OEMAX = 0xE1, - - /// - OEM102 = 0xE2, - - /// - ICOHelp = 0xE3, - - /// - ICO00 = 0xE4, - - /// - ProcessKey = 0xE5, - - /// - ICOClear = 0xE6, - - /// - Packet = 0xE7, - - /// - OEMReset = 0xE9, - - /// - OEMJump = 0xEA, - - /// - OEMPA1 = 0xEB, - - /// - OEMPA2 = 0xEC, - - /// - OEMPA3 = 0xED, - - /// - OEMWSCtrl = 0xEE, - - /// - OEMCUSel = 0xEF, - - /// - OEMATTN = 0xF0, - - /// - OEMFinish = 0xF1, - - /// - OEMCopy = 0xF2, - - /// - OEMAuto = 0xF3, - - /// - OEMENLW = 0xF4, - - /// - OEMBackTab = 0xF5, - - /// - ATTN = 0xF6, - - /// - CRSel = 0xF7, - - /// - EXSel = 0xF8, - - /// - EREOF = 0xF9, - - /// - Play = 0xFA, - - /// - Zoom = 0xFB, - - /// - Noname = 0xFC, - - /// - PA1 = 0xFD, - - /// - OEMClear = 0xFE - } - - public enum HIDUsage : ushort - { - Pointer = 0x01, - Mouse = 0x02, - Joystick = 0x04, - Gamepad = 0x05, - Keyboard = 0x06, - Keypad = 0x07, - SystemControl = 0x80, - X = 0x30, - Y = 0x31, - Z = 0x32, - RelativeX = 0x33, - RelativeY = 0x34, - RelativeZ = 0x35, - Slider = 0x36, - Dial = 0x37, - Wheel = 0x38, - HatSwitch = 0x39, - CountedBuffer = 0x3A, - ByteCount = 0x3B, - MotionWakeup = 0x3C, - VX = 0x40, - VY = 0x41, - VZ = 0x42, - VBRX = 0x43, - VBRY = 0x44, - VBRZ = 0x45, - VNO = 0x46, - SystemControlPower = 0x81, - SystemControlSleep = 0x82, - SystemControlWake = 0x83, - SystemControlContextMenu = 0x84, - SystemControlMainMenu = 0x85, - SystemControlApplicationMenu = 0x86, - SystemControlHelpMenu = 0x87, - SystemControlMenuExit = 0x88, - SystemControlMenuSelect = 0x89, - SystemControlMenuRight = 0x8A, - SystemControlMenuLeft = 0x8B, - SystemControlMenuUp = 0x8C, - SystemControlMenuDown = 0x8D, - KeyboardNoEvent = 0x00, - KeyboardRollover = 0x01, - KeyboardPostFail = 0x02, - KeyboardUndefined = 0x03, - KeyboardaA = 0x04, - KeyboardzZ = 0x1D, - Keyboard1 = 0x1E, - Keyboard0 = 0x27, - KeyboardLeftControl = 0xE0, - KeyboardLeftShift = 0xE1, - KeyboardLeftALT = 0xE2, - KeyboardLeftGUI = 0xE3, - KeyboardRightControl = 0xE4, - KeyboardRightShift = 0xE5, - KeyboardRightALT = 0xE6, - KeyboardRightGUI = 0xE7, - KeyboardScrollLock = 0x47, - KeyboardNumLock = 0x53, - KeyboardCapsLock = 0x39, - KeyboardF1 = 0x3A, - KeyboardF12 = 0x45, - KeyboardReturn = 0x28, - KeyboardEscape = 0x29, - KeyboardDelete = 0x2A, - KeyboardPrintScreen = 0x46, - LEDNumLock = 0x01, - LEDCapsLock = 0x02, - LEDScrollLock = 0x03, - LEDCompose = 0x04, - LEDKana = 0x05, - LEDPower = 0x06, - LEDShift = 0x07, - LEDDoNotDisturb = 0x08, - LEDMute = 0x09, - LEDToneEnable = 0x0A, - LEDHighCutFilter = 0x0B, - LEDLowCutFilter = 0x0C, - LEDEqualizerEnable = 0x0D, - LEDSoundFieldOn = 0x0E, - LEDSurroundFieldOn = 0x0F, - LEDRepeat = 0x10, - LEDStereo = 0x11, - LEDSamplingRateDirect = 0x12, - LEDSpinning = 0x13, - LEDCAV = 0x14, - LEDCLV = 0x15, - LEDRecordingFormatDet = 0x16, - LEDOffHook = 0x17, - LEDRing = 0x18, - LEDMessageWaiting = 0x19, - LEDDataMode = 0x1A, - LEDBatteryOperation = 0x1B, - LEDBatteryOK = 0x1C, - LEDBatteryLow = 0x1D, - LEDSpeaker = 0x1E, - LEDHeadset = 0x1F, - LEDHold = 0x20, - LEDMicrophone = 0x21, - LEDCoverage = 0x22, - LEDNightMode = 0x23, - LEDSendCalls = 0x24, - LEDCallPickup = 0x25, - LEDConference = 0x26, - LEDStandBy = 0x27, - LEDCameraOn = 0x28, - LEDCameraOff = 0x29, - LEDOnLine = 0x2A, - LEDOffLine = 0x2B, - LEDBusy = 0x2C, - LEDReady = 0x2D, - LEDPaperOut = 0x2E, - LEDPaperJam = 0x2F, - LEDRemote = 0x30, - LEDForward = 0x31, - LEDReverse = 0x32, - LEDStop = 0x33, - LEDRewind = 0x34, - LEDFastForward = 0x35, - LEDPlay = 0x36, - LEDPause = 0x37, - LEDRecord = 0x38, - LEDError = 0x39, - LEDSelectedIndicator = 0x3A, - LEDInUseIndicator = 0x3B, - LEDMultiModeIndicator = 0x3C, - LEDIndicatorOn = 0x3D, - LEDIndicatorFlash = 0x3E, - LEDIndicatorSlowBlink = 0x3F, - LEDIndicatorFastBlink = 0x40, - LEDIndicatorOff = 0x41, - LEDFlashOnTime = 0x42, - LEDSlowBlinkOnTime = 0x43, - LEDSlowBlinkOffTime = 0x44, - LEDFastBlinkOnTime = 0x45, - LEDFastBlinkOffTime = 0x46, - LEDIndicatorColor = 0x47, - LEDRed = 0x48, - LEDGreen = 0x49, - LEDAmber = 0x4A, - LEDGenericIndicator = 0x3B, - TelephonyPhone = 0x01, - TelephonyAnsweringMachine = 0x02, - TelephonyMessageControls = 0x03, - TelephonyHandset = 0x04, - TelephonyHeadset = 0x05, - TelephonyKeypad = 0x06, - TelephonyProgrammableButton = 0x07, - SimulationRudder = 0xBA, - SimulationThrottle = 0xBB - } - - public enum HIDUsagePage : ushort - { - Undefined = 0x00, - Generic = 0x01, - Simulation = 0x02, - VR = 0x03, - Sport = 0x04, - Game = 0x05, - Keyboard = 0x07, - LED = 0x08, - Button = 0x09, - Ordinal = 0x0A, - Telephony = 0x0B, - Consumer = 0x0C, - Digitizer = 0x0D, - PID = 0x0F, - Unicode = 0x10, - AlphaNumeric = 0x14, - Medical = 0x40, - MonitorPage0 = 0x80, - MonitorPage1 = 0x81, - MonitorPage2 = 0x82, - MonitorPage3 = 0x83, - PowerPage0 = 0x84, - PowerPage1 = 0x85, - PowerPage2 = 0x86, - PowerPage3 = 0x87, - BarCode = 0x8C, - Scale = 0x8D, - MSR = 0x8E - } - - /// - /// Windows Messages - /// Defined in winuser.h from Windows SDK v6.1 - /// Documentation pulled from MSDN. - /// - public enum WindowsMessages : uint - { - /// - /// The WM_NULL message performs no operation. An application sends the WM_NULL message if it wants to post a message that the recipient window will ignore. - /// - NULL = 0x0000, - - /// - /// The WM_CREATE message is sent when an application requests that a window be created by calling the CreateWindowEx or CreateWindow function. (The message is sent before the function returns.) The window procedure of the new window receives this message after the window is created, but before the window becomes visible. - /// - CREATE = 0x0001, - - /// - /// The WM_DESTROY message is sent when a window is being destroyed. It is sent to the window procedure of the window being destroyed after the window is removed from the screen. - /// This message is sent first to the window being destroyed and then to the child windows (if any) as they are destroyed. During the processing of the message, it can be assumed that all child windows still exist. - /// /// - DESTROY = 0x0002, - - /// - /// The WM_MOVE message is sent after a window has been moved. - /// - MOVE = 0x0003, - - /// - /// The WM_SIZE message is sent to a window after its size has changed. - /// - SIZE = 0x0005, - - /// - /// The WM_ACTIVATE message is sent to both the window being activated and the window being deactivated. If the windows use the same input queue, the message is sent synchronously, first to the window procedure of the top-level window being deactivated, then to the window procedure of the top-level window being activated. If the windows use different input queues, the message is sent asynchronously, so the window is activated immediately. - /// - ACTIVATE = 0x0006, - - /// - /// The WM_SETFOCUS message is sent to a window after it has gained the keyboard focus. - /// - SETFOCUS = 0x0007, - - /// - /// The WM_KILLFOCUS message is sent to a window immediately before it loses the keyboard focus. - /// - KILLFOCUS = 0x0008, - - /// - /// The WM_ENABLE message is sent when an application changes the enabled state of a window. It is sent to the window whose enabled state is changing. This message is sent before the EnableWindow function returns, but after the enabled state (WS_DISABLED style bit) of the window has changed. - /// - ENABLE = 0x000A, - - /// - /// An application sends the WM_SETREDRAW message to a window to allow changes in that window to be redrawn or to prevent changes in that window from being redrawn. - /// - SETREDRAW = 0x000B, - - /// - /// An application sends a WM_SETTEXT message to set the FontText of a window. - /// - SETTEXT = 0x000C, - - /// - /// An application sends a WM_GETTEXT message to copy the FontText that corresponds to a window into a buffer provided by the caller. - /// - GETTEXT = 0x000D, - - /// - /// An application sends a WM_GETTEXTLENGTH message to determine the length, in characters, of the FontText associated with a window. - /// - GETTEXTLENGTH = 0x000E, - - /// - /// The WM_PAINT message is sent when the system or another application makes a request to paint a portion of an application's window. The message is sent when the UpdateWindow or RedrawWindow function is called, or by the DispatchMessage function when the application obtains a WM_PAINT message by using the GetMessage or PeekMessage function. - /// - PAINT = 0x000F, - - /// - /// The WM_CLOSE message is sent as a signal that a window or an application should terminate. - /// - CLOSE = 0x0010, - - /// - /// The WM_QUERYENDSESSION message is sent when the user chooses to end the session or when an application calls one of the system shutdown functions. If any application returns zero, the session is not ended. The system stops sending WM_QUERYENDSESSION messages as soon as one application returns zero. - /// After processing this message, the system sends the WM_ENDSESSION message with the wParam parameter set to the results of the WM_QUERYENDSESSION message. - /// - QUERYENDSESSION = 0x0011, - - /// - /// The WM_QUERYOPEN message is sent to an icon when the user requests that the window be restored to its previous size and position. - /// - QUERYOPEN = 0x0013, - - /// - /// The WM_ENDSESSION message is sent to an application after the system processes the results of the WM_QUERYENDSESSION message. The WM_ENDSESSION message informs the application whether the session is ending. - /// - ENDSESSION = 0x0016, - - /// - /// The WM_QUIT message indicates a request to terminate an application and is generated when the application calls the PostQuitMessage function. It causes the GetMessage function to return zero. - /// - QUIT = 0x0012, - - /// - /// The WM_ERASEBKGND message is sent when the window background must be erased (for example, when a window is resized). The message is sent to prepare an invalidated portion of a window for painting. - /// - ERASEBKGND = 0x0014, - - /// - /// This message is sent to all top-level windows when a change is made to a system color setting. - /// - SYSCOLORCHANGE = 0x0015, - - /// - /// The WM_SHOWWINDOW message is sent to a window when the window is about to be hidden or shown. - /// - SHOWWINDOW = 0x0018, - - /// - /// An application sends the WM_WININICHANGE message to all top-level windows after making a change to the WIN.INI file. The SystemParametersInfo function sends this message after an application uses the function to change a setting in WIN.INI. - /// Note The WM_WININICHANGE message is provided only for compatibility with earlier versions of the system. Applications should use the WM_SETTINGCHANGE message. - /// - WININICHANGE = 0x001A, - - /// - /// An application sends the WM_WININICHANGE message to all top-level windows after making a change to the WIN.INI file. The SystemParametersInfo function sends this message after an application uses the function to change a setting in WIN.INI. - /// Note The WM_WININICHANGE message is provided only for compatibility with earlier versions of the system. Applications should use the WM_SETTINGCHANGE message. - /// - SETTINGCHANGE = WININICHANGE, - - /// - /// The WM_DEVMODECHANGE message is sent to all top-level windows whenever the user changes device-mode settings. - /// - DEVMODECHANGE = 0x001B, - - /// - /// The WM_ACTIVATEAPP message is sent when a window belonging to a different application than the active window is about to be activated. The message is sent to the application whose window is being activated and to the application whose window is being deactivated. - /// - ACTIVATEAPP = 0x001C, - - /// - /// An application sends the WM_FONTCHANGE message to all top-level windows in the system after changing the pool of font resources. - /// - FONTCHANGE = 0x001D, - - /// - /// A message that is sent whenever there is a change in the system time. - /// - TIMECHANGE = 0x001E, - - /// - /// The WM_CANCELMODE message is sent to cancel certain modes, such as mouse capture. For example, the system sends this message to the active window when a dialog box or message box is displayed. Certain functions also send this message explicitly to the specified window regardless of whether it is the active window. For example, the EnableWindow function sends this message when disabling the specified window. - /// - CANCELMODE = 0x001F, - - /// - /// The WM_SETCURSOR message is sent to a window if the mouse causes the cursor to move within a window and mouse input is not captured. - /// - SETCURSOR = 0x0020, - - /// - /// The WM_MOUSEACTIVATE message is sent when the cursor is in an inactive window and the user presses a mouse button. The parent window receives this message only if the child window passes it to the DefWindowProc function. - /// - MOUSEACTIVATE = 0x0021, - - /// - /// The WM_CHILDACTIVATE message is sent to a child window when the user clicks the window's title bar or when the window is activated, moved, or sized. - /// - CHILDACTIVATE = 0x0022, - - /// - /// The WM_QUEUESYNC message is sent by a computer-based training (CBT) application to separate user-input messages from other messages sent through the WH_JOURNALPLAYBACK Hook procedure. - /// - QUEUESYNC = 0x0023, - - /// - /// The WM_GETMINMAXINFO message is sent to a window when the size or position of the window is about to change. An application can use this message to override the window's default maximized size and position, or its default minimum or maximum tracking size. - /// - GETMINMAXINFO = 0x0024, - - /// - /// Windows NT 3.51 and earlier: The WM_PAINTICON message is sent to a minimized window when the icon is to be painted. This message is not sent by newer versions of Microsoft Windows, except in unusual circumstances explained in the Remarks. - /// - PAINTICON = 0x0026, - - /// - /// Windows NT 3.51 and earlier: The WM_ICONERASEBKGND message is sent to a minimized window when the background of the icon must be filled before painting the icon. A window receives this message only if a class icon is defined for the window; otherwise, WM_ERASEBKGND is sent. This message is not sent by newer versions of Windows. - /// - ICONERASEBKGND = 0x0027, - - /// - /// The WM_NEXTDLGCTL message is sent to a dialog box procedure to set the keyboard focus to a different control in the dialog box. - /// - NEXTDLGCTL = 0x0028, - - /// - /// The WM_SPOOLERSTATUS message is sent from Print Manager whenever a job is added to or removed from the Print Manager queue. - /// - SPOOLERSTATUS = 0x002A, - - /// - /// The WM_DRAWITEM message is sent to the parent window of an owner-drawn button, combo box, list box, or menu when a visual aspect of the button, combo box, list box, or menu has changed. - /// - DRAWITEM = 0x002B, - - /// - /// The WM_MEASUREITEM message is sent to the owner window of a combo box, list box, list view control, or menu item when the control or menu is created. - /// - MEASUREITEM = 0x002C, - - /// - /// Sent to the owner of a list box or combo box when the list box or combo box is destroyed or when items are removed by the LB_DELETESTRING, LB_RESETCONTENT, CB_DELETESTRING, or CB_RESETCONTENT message. The system sends a WM_DELETEITEM message for each deleted item. The system sends the WM_DELETEITEM message for any deleted list box or combo box item with nonzero item data. - /// - DELETEITEM = 0x002D, - - /// - /// Sent by a list box with the LBS_WANTKEYBOARDINPUT style to its owner in response to a WM_KEYDOWN message. - /// - VKEYTOITEM = 0x002E, - - /// - /// Sent by a list box with the LBS_WANTKEYBOARDINPUT style to its owner in response to a WM_CHAR message. - /// - CHARTOITEM = 0x002F, - - /// - /// An application sends a WM_SETFONT message to specify the font that a control is to use when drawing FontText. - /// - SETFONT = 0x0030, - - /// - /// An application sends a WM_GETFONT message to a control to retrieve the font with which the control is currently drawing its FontText. - /// - GETFONT = 0x0031, - - /// - /// An application sends a WM_SETHOTKEY message to a window to associate a hot key with the window. When the user presses the hot key, the system activates the window. - /// - SETHOTKEY = 0x0032, - - /// - /// An application sends a WM_GETHOTKEY message to determine the hot key associated with a window. - /// - GETHOTKEY = 0x0033, - - /// - /// The WM_QUERYDRAGICON message is sent to a minimized (iconic) window. The window is about to be dragged by the user but does not have an icon defined for its class. An application can return a handle to an icon or cursor. The system displays this cursor or icon while the user drags the icon. - /// - QUERYDRAGICON = 0x0037, - - /// - /// The system sends the WM_COMPAREITEM message to determine the relative position of a new item in the sorted list of an owner-drawn combo box or list box. Whenever the application adds a new item, the system sends this message to the owner of a combo box or list box created with the CBS_SORT or LBS_SORT style. - /// - COMPAREITEM = 0x0039, - - /// - /// Active Accessibility sends the WM_GETOBJECT message to obtain information about an accessible object contained in a server application. - /// Applications never send this message directly. It is sent only by Active Accessibility in response to calls to AccessibleObjectFromPoint, AccessibleObjectFromEvent, or AccessibleObjectFromWindow. However, server applications handle this message. - /// - GETOBJECT = 0x003D, - - /// - /// The WM_COMPACTING message is sent to all top-level windows when the system detects more than 12.5 percent of system time over a 30- to 60-second interval is being spent compacting memory. This indicates that system memory is low. - /// - COMPACTING = 0x0041, - - /// - /// WM_COMMNOTIFY is Obsolete for Win32-Based Applications - /// - [Obsolete] COMMNOTIFY = 0x0044, - - /// - /// The WM_WINDOWPOSCHANGING message is sent to a window whose size, position, or place in the Z order is about to change as a result of a call to the SetWindowPos function or another window-management function. - /// - WINDOWPOSCHANGING = 0x0046, - - /// - /// The WM_WINDOWPOSCHANGED message is sent to a window whose size, position, or place in the Z order has changed as a result of a call to the SetWindowPos function or another window-management function. - /// - WINDOWPOSCHANGED = 0x0047, - - /// - /// Notifies applications that the system, typically a battery-powered personal computer, is about to enter a suspended mode. - /// Use: POWERBROADCAST - /// - [Obsolete] POWER = 0x0048, - - /// - /// An application sends the WM_COPYDATA message to pass data to another application. - /// - COPYDATA = 0x004A, - - /// - /// The WM_CANCELJOURNAL message is posted to an application when a user cancels the application's journaling activities. The message is posted with a NULL window handle. - /// - CANCELJOURNAL = 0x004B, - - /// - /// Sent by a common control to its parent window when an event has occurred or the control requires some information. - /// - NOTIFY = 0x004E, - - /// - /// The WM_INPUTLANGCHANGEREQUEST message is posted to the window with the focus when the user chooses a new input language, either with the hotkey (specified in the Keyboard control panel application) or from the indicator on the system taskbar. An application can accept the change by passing the message to the DefWindowProc function or reject the change (and prevent it from taking place) by returning immediately. - /// - INPUTLANGCHANGEREQUEST = 0x0050, - - /// - /// The WM_INPUTLANGCHANGE message is sent to the topmost affected window after an application's input language has been changed. You should make any application-specific settings and pass the message to the DefWindowProc function, which passes the message to all first-level child windows. These child windows can pass the message to DefWindowProc to have it pass the message to their child windows, and so on. - /// - INPUTLANGCHANGE = 0x0051, - - /// - /// Sent to an application that has initiated a training card with Microsoft Windows Help. The message informs the application when the user clicks an authorable button. An application initiates a training card by specifying the HELP_TCARD command in a call to the WinHelp function. - /// - TCARD = 0x0052, - - /// - /// Indicates that the user pressed the F1 key. If a menu is active when F1 is pressed, WM_HELP is sent to the window associated with the menu; otherwise, WM_HELP is sent to the window that has the keyboard focus. If no window has the keyboard focus, WM_HELP is sent to the currently active window. - /// - HELP = 0x0053, - - /// - /// The WM_USERCHANGED message is sent to all windows after the user has logged on or off. When the user logs on or off, the system updates the user-specific settings. The system sends this message immediately after updating the settings. - /// - USERCHANGED = 0x0054, - - /// - /// Determines if a window accepts ANSI or Unicode structures in the WM_NOTIFY notification message. WM_NOTIFYFORMAT messages are sent from a common control to its parent window and from the parent window to the common control. - /// - NOTIFYFORMAT = 0x0055, - - /// - /// The WM_CONTEXTMENU message notifies a window that the user clicked the right mouse button (right-clicked) in the window. - /// - CONTEXTMENU = 0x007B, - - /// - /// The WM_STYLECHANGING message is sent to a window when the SetWindowLong function is about to change one or more of the window's styles. - /// - STYLECHANGING = 0x007C, - - /// - /// The WM_STYLECHANGED message is sent to a window after the SetWindowLong function has changed one or more of the window's styles - /// - STYLECHANGED = 0x007D, - - /// - /// The WM_DISPLAYCHANGE message is sent to all windows when the display resolution has changed. - /// - DISPLAYCHANGE = 0x007E, - - /// - /// The WM_GETICON message is sent to a window to retrieve a handle to the large or small icon associated with a window. The system displays the large icon in the ALT+TAB dialog, and the small icon in the window caption. - /// - GETICON = 0x007F, - - /// - /// An application sends the WM_SETICON message to associate a new large or small icon with a window. The system displays the large icon in the ALT+TAB dialog box, and the small icon in the window caption. - /// - SETICON = 0x0080, - - /// - /// The WM_NCCREATE message is sent prior to the WM_CREATE message when a window is first created. - /// - NCCREATE = 0x0081, - - /// - /// The WM_NCDESTROY message informs a window that its nonclient area is being destroyed. The DestroyWindow function sends the WM_NCDESTROY message to the window following the WM_DESTROY message. WM_DESTROY is used to free the allocated memory object associated with the window. - /// The WM_NCDESTROY message is sent after the child windows have been destroyed. In contrast, WM_DESTROY is sent before the child windows are destroyed. - /// - NCDESTROY = 0x0082, - - /// - /// The WM_NCCALCSIZE message is sent when the size and position of a window's client area must be calculated. By processing this message, an application can control the content of the window's client area when the size or position of the window changes. - /// - NCCALCSIZE = 0x0083, - - /// - /// The WM_NCHITTEST message is sent to a window when the cursor moves, or when a mouse button is pressed or released. If the mouse is not captured, the message is sent to the window beneath the cursor. Otherwise, the message is sent to the window that has captured the mouse. - /// - NCHITTEST = 0x0084, - - /// - /// The WM_NCPAINT message is sent to a window when its frame must be painted. - /// - NCPAINT = 0x0085, - - /// - /// The WM_NCACTIVATE message is sent to a window when its nonclient area needs to be changed to indicate an active or inactive state. - /// - NCACTIVATE = 0x0086, - - /// - /// The WM_GETDLGCODE message is sent to the window procedure associated with a control. By default, the system handles all keyboard input to the control; the system interprets certain types of keyboard input as dialog box navigation keys. To override this default behavior, the control can respond to the WM_GETDLGCODE message to indicate the types of input it wants to process itself. - /// - GETDLGCODE = 0x0087, - - /// - /// The WM_SYNCPAINT message is used to synchronize painting while avoiding linking independent GUI threads. - /// - SYNCPAINT = 0x0088, - - /// - /// The WM_NCMOUSEMOVE message is posted to a window when the cursor is moved within the nonclient area of the window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCMOUSEMOVE = 0x00A0, - - /// - /// The WM_NCLBUTTONDOWN message is posted when the user presses the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCLBUTTONDOWN = 0x00A1, - - /// - /// The WM_NCLBUTTONUP message is posted when the user releases the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCLBUTTONUP = 0x00A2, - - /// - /// The WM_NCLBUTTONDBLCLK message is posted when the user double-clicks the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCLBUTTONDBLCLK = 0x00A3, - - /// - /// The WM_NCRBUTTONDOWN message is posted when the user presses the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCRBUTTONDOWN = 0x00A4, - - /// - /// The WM_NCRBUTTONUP message is posted when the user releases the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCRBUTTONUP = 0x00A5, - - /// - /// The WM_NCRBUTTONDBLCLK message is posted when the user double-clicks the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCRBUTTONDBLCLK = 0x00A6, - - /// - /// The WM_NCMBUTTONDOWN message is posted when the user presses the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCMBUTTONDOWN = 0x00A7, - - /// - /// The WM_NCMBUTTONUP message is posted when the user releases the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCMBUTTONUP = 0x00A8, - - /// - /// The WM_NCMBUTTONDBLCLK message is posted when the user double-clicks the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCMBUTTONDBLCLK = 0x00A9, - - /// - /// The WM_NCXBUTTONDOWN message is posted when the user presses the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCXBUTTONDOWN = 0x00AB, - - /// - /// The WM_NCXBUTTONUP message is posted when the user releases the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCXBUTTONUP = 0x00AC, - - /// - /// The WM_NCXBUTTONDBLCLK message is posted when the user double-clicks the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. - /// - NCXBUTTONDBLCLK = 0x00AD, - - /// - /// The WM_INPUT_DEVICE_CHANGE message is sent to the window that registered to receive raw input. A window receives this message through its WindowProc function. - /// - INPUT_DEVICE_CHANGE = 0x00FE, - - /// - /// The WM_INPUT message is sent to the window that is getting raw input. - /// - INPUT = 0x00FF, - - /// - /// This message filters for keyboard messages. - /// - KEYFIRST = 0x0100, - - /// - /// The WM_KEYDOWN message is posted to the window with the keyboard focus when a nonsystem key is pressed. A nonsystem key is a key that is pressed when the ALT key is not pressed. - /// - KEYDOWN = 0x0100, - - /// - /// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus. - /// - KEYUP = 0x0101, - - /// - /// The WM_CHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed. - /// - CHAR = 0x0102, - - /// - /// The WM_DEADCHAR message is posted to the window with the keyboard focus when a WM_KEYUP message is translated by the TranslateMessage function. WM_DEADCHAR specifies a character code generated by a dead key. A dead key is a key that generates a character, such as the umlaut (double-dot), that is combined with another character to form a composite character. For example, the umlaut-O character (Ö) is generated by typing the dead key for the umlaut character, and then typing the O key. - /// - DEADCHAR = 0x0103, - - /// - /// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when the user presses the F10 key (which activates the menu bar) or holds down the ALT key and then presses another key. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYDOWN message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter. - /// - SYSKEYDOWN = 0x0104, - - /// - /// The WM_SYSKEYUP message is posted to the window with the keyboard focus when the user releases a key that was pressed while the ALT key was held down. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYUP message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter. - /// - SYSKEYUP = 0x0105, - - /// - /// The WM_SYSCHAR message is posted to the window with the keyboard focus when a WM_SYSKEYDOWN message is translated by the TranslateMessage function. It specifies the character code of a system character key — that is, a character key that is pressed while the ALT key is down. - /// - SYSCHAR = 0x0106, - - /// - /// The WM_SYSDEADCHAR message is sent to the window with the keyboard focus when a WM_SYSKEYDOWN message is translated by the TranslateMessage function. WM_SYSDEADCHAR specifies the character code of a system dead key — that is, a dead key that is pressed while holding down the ALT key. - /// - SYSDEADCHAR = 0x0107, - - /// - /// The WM_UNICHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_UNICHAR message contains the character code of the key that was pressed. - /// The WM_UNICHAR message is equivalent to WM_CHAR, but it uses Unicode Transformation Format (UTF)-32, whereas WM_CHAR uses UTF-16. It is designed to send or post Unicode characters to ANSI windows and it can can handle Unicode Supplementary Plane characters. - /// - UNICHAR = 0x0109, - - /// - /// This message filters for keyboard messages. - /// - KEYLAST = 0x0109, - - /// - /// Sent immediately before the IME generates the composition string as a result of a keystroke. A window receives this message through its WindowProc function. - /// - IME_STARTCOMPOSITION = 0x010D, - - /// - /// Sent to an application when the IME ends composition. A window receives this message through its WindowProc function. - /// - IME_ENDCOMPOSITION = 0x010E, - - /// - /// Sent to an application when the IME changes composition status as a result of a keystroke. A window receives this message through its WindowProc function. - /// - IME_COMPOSITION = 0x010F, - IME_KEYLAST = 0x010F, - - /// - /// The WM_INITDIALOG message is sent to the dialog box procedure immediately before a dialog box is displayed. Dialog box procedures typically use this message to initialize controls and carry out any other initialization tasks that affect the appearance of the dialog box. - /// - INITDIALOG = 0x0110, - - /// - /// The WM_COMMAND message is sent when the user selects a command item from a menu, when a control sends a notification message to its parent window, or when an accelerator keystroke is translated. - /// - COMMAND = 0x0111, - - /// - /// A window receives this message when the user chooses a command from the Window menu, clicks the maximize button, minimize button, restore button, close button, or moves the form. You can stop the form from moving by filtering this out. - /// - SYSCOMMAND = 0x0112, - - /// - /// The WM_TIMER message is posted to the installing thread's message queue when a timer expires. The message is posted by the GetMessage or PeekMessage function. - /// - TIMER = 0x0113, - - /// - /// The WM_HSCROLL message is sent to a window when a scroll event occurs in the window's standard horizontal scroll bar. This message is also sent to the owner of a horizontal scroll bar control when a scroll event occurs in the control. - /// - HSCROLL = 0x0114, - - /// - /// The WM_VSCROLL message is sent to a window when a scroll event occurs in the window's standard vertical scroll bar. This message is also sent to the owner of a vertical scroll bar control when a scroll event occurs in the control. - /// - VSCROLL = 0x0115, - - /// - /// The WM_INITMENU message is sent when a menu is about to become active. It occurs when the user clicks an item on the menu bar or presses a menu key. This allows the application to modify the menu before it is displayed. - /// - INITMENU = 0x0116, - - /// - /// The WM_INITMENUPOPUP message is sent when a drop-down menu or submenu is about to become active. This allows an application to modify the menu before it is displayed, without changing the entire menu. - /// - INITMENUPOPUP = 0x0117, - - /// - /// The WM_MENUSELECT message is sent to a menu's owner window when the user selects a menu item. - /// - MENUSELECT = 0x011F, - - /// - /// The WM_MENUCHAR message is sent when a menu is active and the user presses a key that does not correspond to any mnemonic or accelerator key. This message is sent to the window that owns the menu. - /// - MENUCHAR = 0x0120, - - /// - /// The WM_ENTERIDLE message is sent to the owner window of a modal dialog box or menu that is entering an idle state. A modal dialog box or menu enters an idle state when no messages are waiting in its queue after it has processed one or more previous messages. - /// - ENTERIDLE = 0x0121, - - /// - /// The WM_MENURBUTTONUP message is sent when the user releases the right mouse button while the cursor is on a menu item. - /// - MENURBUTTONUP = 0x0122, - - /// - /// The WM_MENUDRAG message is sent to the owner of a drag-and-drop menu when the user drags a menu item. - /// - MENUDRAG = 0x0123, - - /// - /// The WM_MENUGETOBJECT message is sent to the owner of a drag-and-drop menu when the mouse cursor enters a menu item or moves from the center of the item to the top or bottom of the item. - /// - MENUGETOBJECT = 0x0124, - - /// - /// The WM_UNINITMENUPOPUP message is sent when a drop-down menu or submenu has been destroyed. - /// - UNINITMENUPOPUP = 0x0125, - - /// - /// The WM_MENUCOMMAND message is sent when the user makes a selection from a menu. - /// - MENUCOMMAND = 0x0126, - - /// - /// An application sends the WM_CHANGEUISTATE message to indicate that the user interface (UI) state should be changed. - /// - CHANGEUISTATE = 0x0127, - - /// - /// An application sends the WM_UPDATEUISTATE message to change the user interface (UI) state for the specified window and all its child windows. - /// - UPDATEUISTATE = 0x0128, - - /// - /// An application sends the WM_QUERYUISTATE message to retrieve the user interface (UI) state for a window. - /// - QUERYUISTATE = 0x0129, - - /// - /// The WM_CTLCOLORMSGBOX message is sent to the owner window of a message box before Windows draws the message box. By responding to this message, the owner window can set the FontText and background colors of the message box by using the given display device context handle. - /// - CTLCOLORMSGBOX = 0x0132, - - /// - /// An edit control that is not read-only or disabled sends the WM_CTLCOLOREDIT message to its parent window when the control is about to be drawn. By responding to this message, the parent window can use the specified device context handle to set the FontText and background colors of the edit control. - /// - CTLCOLOREDIT = 0x0133, - - /// - /// Sent to the parent window of a list box before the system draws the list box. By responding to this message, the parent window can set the FontText and background colors of the list box by using the specified display device context handle. - /// - CTLCOLORLISTBOX = 0x0134, - - /// - /// The WM_CTLCOLORBTN message is sent to the parent window of a button before drawing the button. The parent window can change the button's FontText and background colors. However, only owner-drawn buttons respond to the parent window processing this message. - /// - CTLCOLORBTN = 0x0135, - - /// - /// The WM_CTLCOLORDLG message is sent to a dialog box before the system draws the dialog box. By responding to this message, the dialog box can set its FontText and background colors using the specified display device context handle. - /// - CTLCOLORDLG = 0x0136, - - /// - /// The WM_CTLCOLORSCROLLBAR message is sent to the parent window of a scroll bar control when the control is about to be drawn. By responding to this message, the parent window can use the display context handle to set the background color of the scroll bar control. - /// - CTLCOLORSCROLLBAR = 0x0137, - - /// - /// A static control, or an edit control that is read-only or disabled, sends the WM_CTLCOLORSTATIC message to its parent window when the control is about to be drawn. By responding to this message, the parent window can use the specified device context handle to set the FontText and background colors of the static control. - /// - CTLCOLORSTATIC = 0x0138, - - /// - /// Use WM_MOUSEFIRST to specify the first mouse message. Use the PeekMessage() Function. - /// - MOUSEFIRST = 0x0200, - - /// - /// The WM_MOUSEMOVE message is posted to a window when the cursor moves. If the mouse is not captured, the message is posted to the window that contains the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - MOUSEMOVE = 0x0200, - - /// - /// The WM_LBUTTONDOWN message is posted when the user presses the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - LBUTTONDOWN = 0x0201, - - /// - /// The WM_LBUTTONUP message is posted when the user releases the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - LBUTTONUP = 0x0202, - - /// - /// The WM_LBUTTONDBLCLK message is posted when the user double-clicks the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - LBUTTONDBLCLK = 0x0203, - - /// - /// The WM_RBUTTONDOWN message is posted when the user presses the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - RBUTTONDOWN = 0x0204, - - /// - /// The WM_RBUTTONUP message is posted when the user releases the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - RBUTTONUP = 0x0205, - - /// - /// The WM_RBUTTONDBLCLK message is posted when the user double-clicks the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - RBUTTONDBLCLK = 0x0206, - - /// - /// The WM_MBUTTONDOWN message is posted when the user presses the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - MBUTTONDOWN = 0x0207, - - /// - /// The WM_MBUTTONUP message is posted when the user releases the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - MBUTTONUP = 0x0208, - - /// - /// The WM_MBUTTONDBLCLK message is posted when the user double-clicks the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - MBUTTONDBLCLK = 0x0209, - - /// - /// The WM_MOUSEWHEEL message is sent to the focus window when the mouse wheel is rotated. The DefWindowProc function propagates the message to the window's parent. There should be no public forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it. - /// - MOUSEWHEEL = 0x020A, - - /// - /// The WM_XBUTTONDOWN message is posted when the user presses the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - XBUTTONDOWN = 0x020B, - - /// - /// The WM_XBUTTONUP message is posted when the user releases the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - XBUTTONUP = 0x020C, - - /// - /// The WM_XBUTTONDBLCLK message is posted when the user double-clicks the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. - /// - XBUTTONDBLCLK = 0x020D, - - /// - /// The WM_MOUSEHWHEEL message is sent to the focus window when the mouse's horizontal scroll wheel is tilted or rotated. The DefWindowProc function propagates the message to the window's parent. There should be no public forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it. - /// - MOUSEHWHEEL = 0x020E, - - /// - /// Use WM_MOUSELAST to specify the last mouse message. Used with PeekMessage() Function. - /// - MOUSELAST = 0x020E, - - /// - /// The WM_PARENTNOTIFY message is sent to the parent of a child window when the child window is created or destroyed, or when the user clicks a mouse button while the cursor is over the child window. When the child window is being created, the system sends WM_PARENTNOTIFY just before the CreateWindow or CreateWindowEx function that creates the window returns. When the child window is being destroyed, the system sends the message before any processing to destroy the window takes place. - /// - PARENTNOTIFY = 0x0210, - - /// - /// The WM_ENTERMENULOOP message informs an application's main window procedure that a menu modal loop has been entered. - /// - ENTERMENULOOP = 0x0211, - - /// - /// The WM_EXITMENULOOP message informs an application's main window procedure that a menu modal loop has been exited. - /// - EXITMENULOOP = 0x0212, - - /// - /// The WM_NEXTMENU message is sent to an application when the right or left arrow key is used to switch between the menu bar and the system menu. - /// - NEXTMENU = 0x0213, - - /// - /// The WM_SIZING message is sent to a window that the user is resizing. By processing this message, an application can monitor the size and position of the drag rectangle and, if needed, change its size or position. - /// - SIZING = 0x0214, - - /// - /// The WM_CAPTURECHANGED message is sent to the window that is losing the mouse capture. - /// - CAPTURECHANGED = 0x0215, - - /// - /// The WM_MOVING message is sent to a window that the user is moving. By processing this message, an application can monitor the position of the drag rectangle and, if needed, change its position. - /// - MOVING = 0x0216, - - /// - /// Notifies applications that a power-management event has occurred. - /// - POWERBROADCAST = 0x0218, - - /// - /// Notifies an application of a change to the hardware configuration of a device or the computer. - /// - DEVICECHANGE = 0x0219, - - /// - /// An application sends the WM_MDICREATE message to a multiple-document interface (MDI) client window to create an MDI child window. - /// - MDICREATE = 0x0220, - - /// - /// An application sends the WM_MDIDESTROY message to a multiple-document interface (MDI) client window to close an MDI child window. - /// - MDIDESTROY = 0x0221, - - /// - /// An application sends the WM_MDIACTIVATE message to a multiple-document interface (MDI) client window to instruct the client window to activate a different MDI child window. - /// - MDIACTIVATE = 0x0222, - - /// - /// An application sends the WM_MDIRESTORE message to a multiple-document interface (MDI) client window to restore an MDI child window from maximized or minimized size. - /// - MDIRESTORE = 0x0223, - - /// - /// An application sends the WM_MDINEXT message to a multiple-document interface (MDI) client window to activate the next or previous child window. - /// - MDINEXT = 0x0224, - - /// - /// An application sends the WM_MDIMAXIMIZE message to a multiple-document interface (MDI) client window to maximize an MDI child window. The system resizes the child window to make its client area fill the client window. The system places the child window's window menu icon in the rightmost position of the frame window's menu bar, and places the child window's restore icon in the leftmost position. The system also appends the title bar FontText of the child window to that of the frame window. - /// - MDIMAXIMIZE = 0x0225, - - /// - /// An application sends the WM_MDITILE message to a multiple-document interface (MDI) client window to arrange all of its MDI child windows in a tile format. - /// - MDITILE = 0x0226, - - /// - /// An application sends the WM_MDICASCADE message to a multiple-document interface (MDI) client window to arrange all its child windows in a cascade format. - /// - MDICASCADE = 0x0227, - - /// - /// An application sends the WM_MDIICONARRANGE message to a multiple-document interface (MDI) client window to arrange all minimized MDI child windows. It does not affect child windows that are not minimized. - /// - MDIICONARRANGE = 0x0228, - - /// - /// An application sends the WM_MDIGETACTIVE message to a multiple-document interface (MDI) client window to retrieve the handle to the active MDI child window. - /// - MDIGETACTIVE = 0x0229, - - /// - /// An application sends the WM_MDISETMENU message to a multiple-document interface (MDI) client window to replace the entire menu of an MDI frame window, to replace the window menu of the frame window, or both. - /// - MDISETMENU = 0x0230, - - /// - /// The WM_ENTERSIZEMOVE message is sent one time to a window after it enters the moving or sizing modal loop. The window enters the moving or sizing modal loop when the user clicks the window's title bar or sizing border, or when the window passes the WM_SYSCOMMAND message to the DefWindowProc function and the wParam parameter of the message specifies the SC_MOVE or SC_SIZE value. The operation is complete when DefWindowProc returns. - /// The system sends the WM_ENTERSIZEMOVE message regardless of whether the dragging of full windows is enabled. - /// - ENTERSIZEMOVE = 0x0231, - - /// - /// The WM_EXITSIZEMOVE message is sent one time to a window, after it has exited the moving or sizing modal loop. The window enters the moving or sizing modal loop when the user clicks the window's title bar or sizing border, or when the window passes the WM_SYSCOMMAND message to the DefWindowProc function and the wParam parameter of the message specifies the SC_MOVE or SC_SIZE value. The operation is complete when DefWindowProc returns. - /// - EXITSIZEMOVE = 0x0232, - - /// - /// Sent when the user drops a file on the window of an application that has registered itself as a recipient of dropped files. - /// - DROPFILES = 0x0233, - - /// - /// An application sends the WM_MDIREFRESHMENU message to a multiple-document interface (MDI) client window to refresh the window menu of the MDI frame window. - /// - MDIREFRESHMENU = 0x0234, - - /// - /// Sent to an application when a window is activated. A window receives this message through its WindowProc function. - /// - IME_SETCONTEXT = 0x0281, - - /// - /// Sent to an application to notify it of changes to the IME window. A window receives this message through its WindowProc function. - /// - IME_NOTIFY = 0x0282, - - /// - /// Sent by an application to direct the IME window to carry out the requested command. The application uses this message to control the IME window that it has created. To send this message, the application calls the SendMessage function with the following parameters. - /// - IME_CONTROL = 0x0283, - - /// - /// Sent to an application when the IME window finds no space to extend the area for the composition window. A window receives this message through its WindowProc function. - /// - IME_COMPOSITIONFULL = 0x0284, - - /// - /// Sent to an application when the operating system is about to change the current IME. A window receives this message through its WindowProc function. - /// - IME_SELECT = 0x0285, - - /// - /// Sent to an application when the IME gets a character of the conversion result. A window receives this message through its WindowProc function. - /// - IME_CHAR = 0x0286, - - /// - /// Sent to an application to provide commands and request information. A window receives this message through its WindowProc function. - /// - IME_REQUEST = 0x0288, - - /// - /// Sent to an application by the IME to notify the application of a key press and to keep message order. A window receives this message through its WindowProc function. - /// - IME_KEYDOWN = 0x0290, - - /// - /// Sent to an application by the IME to notify the application of a key release and to keep message order. A window receives this message through its WindowProc function. - /// - IME_KEYUP = 0x0291, - - /// - /// The WM_MOUSEHOVER message is posted to a window when the cursor hovers over the client area of the window for the period of time specified in a prior call to TrackMouseEvent. - /// - MOUSEHOVER = 0x02A1, - - /// - /// The WM_MOUSELEAVE message is posted to a window when the cursor leaves the client area of the window specified in a prior call to TrackMouseEvent. - /// - MOUSELEAVE = 0x02A3, - - /// - /// The WM_NCMOUSEHOVER message is posted to a window when the cursor hovers over the nonclient area of the window for the period of time specified in a prior call to TrackMouseEvent. - /// - NCMOUSEHOVER = 0x02A0, - - /// - /// The WM_NCMOUSELEAVE message is posted to a window when the cursor leaves the nonclient area of the window specified in a prior call to TrackMouseEvent. - /// - NCMOUSELEAVE = 0x02A2, - - /// - /// The WM_WTSSESSION_CHANGE message notifies applications of changes in session state. - /// - WTSSESSION_CHANGE = 0x02B1, - TABLET_FIRST = 0x02c0, - TABLET_LAST = 0x02df, - - /// - /// An application sends a WM_CUT message to an edit control or combo box to delete (cut) the current selection, if any, in the edit control and copy the deleted FontText to the clipboard in CF_TEXT format. - /// - CUT = 0x0300, - - /// - /// An application sends the WM_COPY message to an edit control or combo box to copy the current selection to the clipboard in CF_TEXT format. - /// - COPY = 0x0301, - - /// - /// An application sends a WM_PASTE message to an edit control or combo box to copy the current content of the clipboard to the edit control at the current caret position. Data is inserted only if the clipboard contains data in CF_TEXT format. - /// - PASTE = 0x0302, - - /// - /// An application sends a WM_CLEAR message to an edit control or combo box to delete (clear) the current selection, if any, from the edit control. - /// - CLEAR = 0x0303, - - /// - /// An application sends a WM_UNDO message to an edit control to undo the last operation. When this message is sent to an edit control, the previously deleted FontText is restored or the previously added FontText is deleted. - /// - UNDO = 0x0304, - - /// - /// The WM_RENDERFORMAT message is sent to the clipboard owner if it has delayed rendering a specific clipboard format and if an application has requested data in that format. The clipboard owner must render data in the specified format and place it on the clipboard by calling the SetClipboardData function. - /// - RENDERFORMAT = 0x0305, - - /// - /// The WM_RENDERALLFORMATS message is sent to the clipboard owner before it is destroyed, if the clipboard owner has delayed rendering one or more clipboard formats. For the content of the clipboard to remain available to other applications, the clipboard owner must render data in all the formats it is capable of generating, and place the data on the clipboard by calling the SetClipboardData function. - /// - RENDERALLFORMATS = 0x0306, - - /// - /// The WM_DESTROYCLIPBOARD message is sent to the clipboard owner when a call to the EmptyClipboard function empties the clipboard. - /// - DESTROYCLIPBOARD = 0x0307, - - /// - /// The WM_DRAWCLIPBOARD message is sent to the first window in the clipboard viewer chain when the content of the clipboard changes. This enables a clipboard viewer window to display the new content of the clipboard. - /// - DRAWCLIPBOARD = 0x0308, - - /// - /// The WM_PAINTCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and the clipboard viewer's client area needs repainting. - /// - PAINTCLIPBOARD = 0x0309, - - /// - /// The WM_VSCROLLCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and an event occurs in the clipboard viewer's vertical scroll bar. The owner should scroll the clipboard image and update the scroll bar values. - /// - VSCROLLCLIPBOARD = 0x030A, - - /// - /// The WM_SIZECLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and the clipboard viewer's client area has changed size. - /// - SIZECLIPBOARD = 0x030B, - - /// - /// The WM_ASKCBFORMATNAME message is sent to the clipboard owner by a clipboard viewer window to request the name of a CF_OWNERDISPLAY clipboard format. - /// - ASKCBFORMATNAME = 0x030C, - - /// - /// The WM_CHANGECBCHAIN message is sent to the first window in the clipboard viewer chain when a window is being removed from the chain. - /// - CHANGECBCHAIN = 0x030D, - - /// - /// The WM_HSCROLLCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window. This occurs when the clipboard contains data in the CF_OWNERDISPLAY format and an event occurs in the clipboard viewer's horizontal scroll bar. The owner should scroll the clipboard image and update the scroll bar values. - /// - HSCROLLCLIPBOARD = 0x030E, - - /// - /// This message informs a window that it is about to receive the keyboard focus, giving the window the opportunity to realize its logical palette when it receives the focus. - /// - QUERYNEWPALETTE = 0x030F, - - /// - /// The WM_PALETTEISCHANGING message informs applications that an application is going to realize its logical palette. - /// - PALETTEISCHANGING = 0x0310, - - /// - /// This message is sent by the OS to all top-level and overlapped windows after the window with the keyboard focus realizes its logical palette. - /// This message enables windows that do not have the keyboard focus to realize their logical palettes and update their client areas. - /// - PALETTECHANGED = 0x0311, - - /// - /// The WM_HOTKEY message is posted when the user presses a hot key registered by the RegisterHotKey function. The message is placed at the top of the message queue associated with the thread that registered the hot key. - /// - HOTKEY = 0x0312, - - /// - /// The WM_PRINT message is sent to a window to request that it draw itself in the specified device context, most commonly in a printer device context. - /// - PRINT = 0x0317, - - /// - /// The WM_PRINTCLIENT message is sent to a window to request that it draw its client area in the specified device context, most commonly in a printer device context. - /// - PRINTCLIENT = 0x0318, - - /// - /// The WM_APPCOMMAND message notifies a window that the user generated an application command event, for example, by clicking an application command button using the mouse or typing an application command key on the keyboard. - /// - APPCOMMAND = 0x0319, - - /// - /// The WM_THEMECHANGED message is broadcast to every window following a theme change event. Examples of theme change events are the activation of a theme, the deactivation of a theme, or a transition from one theme to another. - /// - THEMECHANGED = 0x031A, - - /// - /// Sent when the contents of the clipboard have changed. - /// - CLIPBOARDUPDATE = 0x031D, - - /// - /// The system will send a window the WM_DWMCOMPOSITIONCHANGED message to indicate that the availability of desktop composition has changed. - /// - DWMCOMPOSITIONCHANGED = 0x031E, - - /// - /// WM_DWMNCRENDERINGCHANGED is called when the non-client area rendering status of a window has changed. Only windows that have set the flag DWM_BLURBEHIND.fTransitionOnMaximized to true will get this message. - /// - DWMNCRENDERINGCHANGED = 0x031F, - - /// - /// Sent to all top-level windows when the colorization color has changed. - /// - DWMCOLORIZATIONCOLORCHANGED = 0x0320, - - /// - /// WM_DWMWINDOWMAXIMIZEDCHANGE will let you know when a DWM composed window is maximized. You also have to register for this message as well. You'd have other windowd go opaque when this message is sent. - /// - DWMWINDOWMAXIMIZEDCHANGE = 0x0321, - - /// - /// Sent to request extended title bar information. A window receives this message through its WindowProc function. - /// - GETTITLEBARINFOEX = 0x033F, - HANDHELDFIRST = 0x0358, - HANDHELDLAST = 0x035F, - AFXFIRST = 0x0360, - AFXLAST = 0x037F, - PENWINFIRST = 0x0380, - PENWINLAST = 0x038F, - - /// - /// The WM_APP constant is used by applications to help define private messages, usually of the form WM_APP+X, where X is an integer value. - /// - APP = 0x8000, - - /// - /// The WM_USER constant is used by applications to help define private messages for use by private window classes, usually of the form WM_USER+X, where X is an integer value. - /// - USER = 0x0400, - - /// - /// An application sends the WM_CPL_LAUNCH message to Windows Control Panel to request that a Control Panel application be started. - /// - CPL_LAUNCH = USER + 0x1000, - - /// - /// The WM_CPL_LAUNCHED message is sent when a Control Panel application, started by the WM_CPL_LAUNCH message, has closed. The WM_CPL_LAUNCHED message is sent to the window identified by the wParam parameter of the WM_CPL_LAUNCH message that started the application. - /// - CPL_LAUNCHED = USER + 0x1001, - - /// - /// WM_SYSTIMER is a well-known yet still undocumented message. Windows uses WM_SYSTIMER for public actions like scrolling. - /// - SYSTIMER = 0x118, - - /// - /// The accessibility state has changed. - /// - HSHELL_ACCESSIBILITYSTATE = 11, - - /// - /// The shell should activate its main window. - /// - HSHELL_ACTIVATESHELLWINDOW = 3, - - /// - /// The user completed an input event (for example, pressed an application command button on the mouse or an application command key on the keyboard), and the application did not handle the WM_APPCOMMAND message generated by that input. - /// If the Shell procedure handles the WM_COMMAND message, it should not call CallNextHookEx. See the Return Value section for more information. - /// - HSHELL_APPCOMMAND = 12, - - /// - /// A window is being minimized or maximized. The system needs the coordinates of the minimized rectangle for the window. - /// - HSHELL_GETMINRECT = 5, - - /// - /// Keyboard language was changed or a new keyboard layout was loaded. - /// - HSHELL_LANGUAGE = 8, - - /// - /// The title of a window in the task bar has been redrawn. - /// - HSHELL_REDRAW = 6, - - /// - /// The user has selected the task list. A shell application that provides a task list should return TRUE to prevent Windows from starting its task list. - /// - HSHELL_TASKMAN = 7, - - /// - /// A top-level, unowned window has been created. The window exists when the system calls this hook. - /// - HSHELL_WINDOWCREATED = 1, - - /// - /// A top-level, unowned window is about to be destroyed. The window still exists when the system calls this hook. - /// - HSHELL_WINDOWDESTROYED = 2, - - /// - /// The activation has changed to a different top-level, unowned window. - /// - HSHELL_WINDOWACTIVATED = 4, - - /// - /// A top-level window is being replaced. The window exists when the system calls this hook. - /// - HSHELL_WINDOWREPLACED = 13 - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using OpenTK; + +namespace osu.Framework.Platform.Windows.Native +{ + internal static class Input + { + [DllImport("user32.dll")] + public static extern bool RegisterTouchWindow(IntPtr hWnd, int flags); + + [DllImport(@"user32.dll")] + public static extern int SetProp(IntPtr hWnd, string lpString, int hData); + + [DllImport(@"user32.dll")] + public static extern int RemoveProp(IntPtr hWnd, string lpString); + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(int nIndex); + + [DllImport("user32.dll")] + public static extern bool RegisterRawInputDevices( + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] + RawInputDevice[] pRawInputDevices, + int uiNumDevices, + int cbSize); + + [DllImport("user32.dll")] + public static extern bool GetTouchInputInfo( + IntPtr hTouchInput, + int uiNumDevices, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] + RawTouchInput[] pRawTouchInputs, + int cbSize); + + [DllImport("user32.dll")] + public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); + + [DllImport("user32.dll")] + public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, out RawInput pData, ref int pcbSize, int cbSizeHeader); + + [DllImport("user32.dll")] + public static extern bool GetPointerInfo(int pointerID, out RawPointerInput type); + + internal static Rectangle GetVirtualScreenRect() => new Rectangle( + GetSystemMetrics(SM_XVIRTUALSCREEN), + GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), + GetSystemMetrics(SM_CYVIRTUALSCREEN) + ); + + public const int SM_XVIRTUALSCREEN = 76; + public const int SM_YVIRTUALSCREEN = 77; + + public const int SM_CXVIRTUALSCREEN = 78; + public const int SM_CYVIRTUALSCREEN = 79; + + public const int WM_MOUSEACTIVATE = 0x21; + + public const int WM_NCPOINTERUPDATE = 0x0241; + public const int WM_NCPOINTERDOWN = 0x0242; + public const int WM_NCPOINTERUP = 0x0243; + public const int WM_POINTERUPDATE = 0x0245; + public const int WM_POINTERDOWN = 0x0246; + public const int WM_POINTERUP = 0x0247; + public const int WM_POINTERENTER = 0x0249; + public const int WM_POINTERLEAVE = 0x024A; + public const int WM_POINTERACTIVATE = 0x024B; + public const int WM_POINTERCAPTURECHANGED = 0x024C; + public const int WM_POINTERWHEEL = 0x024E; + public const int WM_POINTERHWHEEL = 0x024F; + + public const int WM_INPUT = 0x00FF; + public const int WM_TOUCH = 0x0240; + + public const int TWF_FINETOUCH = 0x00000001; + public const int TWF_WANTPALM = 0x00000002; + + public const int TABLET_DISABLE_PRESSANDHOLD = 0x00000001; + public const int TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008; + public const int TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010; + public const int TABLET_DISABLE_TOUCHUIFORCEON = 0x00000100; + public const int TABLET_DISABLE_TOUCHUIFORCEOFF = 0x00000200; + public const int TABLET_DISABLE_TOUCHSWITCH = 0x00008000; + public const int TABLET_DISABLE_FLICKS = 0x00010000; + public const int TABLET_ENABLE_FLICKSONCONTEXT = 0x00020000; + public const int TABLET_ENABLE_FLICKLEARNINGMODE = 0x00040000; + public const int TABLET_DISABLE_SMOOTHSCROLLING = 0x00080000; + public const int TABLET_DISABLE_FLICKFALLBACKKEYS = 0x00100000; + public const int TABLET_ENABLE_MULTITOUCHDATA = 0x01000000; + } + + /// + /// Enumeration containing pointer types. + /// + public enum RawPointerType : uint + { + Generic = 0x00000001, + Touch = 0x00000002, + Pen = 0x00000003, + Mouse = 0x00000004, + Touchpad = 0x00000005, + } + + public enum RawPointerButtonType : uint + { + None = 0, + FirstButtonDown, + FirstButtonUp, + SecondButtonDown, + SecondButtonUp, + ThirdButtonDown, + ThirdButtonUp, + FourthButtonDown, + FourthButtonUp, + FifthButtonDown, + FifthButtonUp, + } + + /// + /// Enumeration containing pointer flags. + /// + [Flags] + public enum RawPointerFlags : uint + { + /// + /// Default. + /// + None = 0x00000000, + + /// + /// Indicates the arrival of a new pointer. + /// + New = 0x00000001, + + /// + /// Indicates that this pointer continues to exist. When this flag is not set, it indicates the pointer has left detection range. + /// This flag is typically not set only when a hovering pointer leaves detection range (POINTER_FLAG_UPDATE is set) or when a pointer in contact with a window surface leaves detection range (POINTER_FLAG_UP is set). + /// + InRange = 0x00000002, + + /// + /// Indicates that this pointer is in contact with the digitizer surface. When this flag is not set, it indicates a hovering pointer. + /// + InContact = 0x00000004, + + /// + /// Indicates a primary action, analogous to a left mouse button down. + /// A touch pointer has this flag set when it is in contact with the digitizer surface. + /// A pen pointer has this flag set when it is in contact with the digitizer surface with no buttons pressed. + /// A mouse pointer has this flag set when the left mouse button is down. + /// + FirstButton = 0x00000010, + + /// + /// Indicates a secondary action, analogous to a right mouse button down. + /// A touch pointer does not use this flag. + /// A pen pointer has this flag set when it is in contact with the digitizer surface with the pen barrel button pressed. + /// A mouse pointer has this flag set when the right mouse button is down. + /// + SecondButton = 0x00000020, + + /// + /// Analogous to a mouse wheel button down. + /// A touch pointer does not use this flag. + /// A pen pointer does not use this flag. + /// A mouse pointer has this flag set when the mouse wheel button is down. + /// + ThirdButton = 0x00000040, + + /// + /// Analogous to a first extended mouse (XButton1) button down. + /// A touch pointer does not use this flag. + /// A pen pointer does not use this flag. + /// A mouse pointer has this flag set when the first extended mouse (XBUTTON1) button is down. + /// + FourthButton = 0x00000080, + + /// + /// Analogous to a second extended mouse (XButton2) button down. + /// A touch pointer does not use this flag. + /// A pen pointer does not use this flag. + /// A mouse pointer has this flag set when the second extended mouse (XBUTTON2) button is down. + /// + FifthButton = 0x00000100, + + /// + /// Indicates that this pointer has been designated as the primary pointer. A primary pointer is a single pointer that can perform actions beyond those available to non-primary pointers. For example, when a primary pointer makes contact with a window’s surface, it may provide the window an opportunity to activate by sending it a WM_POINTERACTIVATE message. + /// The primary pointer is identified from all current user interactions on the system (mouse, touch, pen, and so on). As such, the primary pointer might not be associated with your app. The first contact in a multi-touch interaction is set as the primary pointer. Once a primary pointer is identified, all contacts must be lifted before a new contact can be identified as a primary pointer. For apps that don't process pointer input, only the primary pointer's events are promoted to mouse events. + /// + Primary = 0x00002000, + + /// + /// Confidence is a suggestion from the source device about whether the pointer represents an intended or accidental interaction, which is especially relevant for PT_TOUCH pointers where an accidental interaction (such as with the palm of the hand) can trigger input. The presence of this flag indicates that the source device has high confidence that this input is part of an intended interaction. + /// + Confidence = 0x000004000, + + /// + /// Indicates that the pointer is departing in an abnormal manner, such as when the system receives invalid input for the pointer or when a device with active pointers departs abruptly. If the application receiving the input is in a position to do so, it should treat the interaction as not completed and reverse any effects of the concerned pointer. + /// + Canceled = 0x000008000, + + /// + /// Indicates that this pointer transitioned to a down state; that is, it made contact with the digitizer surface. + /// + Down = 0x00010000, + + /// + /// Indicates that this is a simple update that does not include pointer state changes. + /// + Update = 0x00020000, + + /// + /// Indicates that this pointer transitioned to an up state; that is, it broke contact with the digitizer surface. + /// + Up = 0x00040000, + + /// + /// Indicates input associated with a pointer wheel. For mouse pointers, this is equivalent to the action of the mouse scroll wheel (WM_MOUSEWHEEL). + /// + Wheel = 0x00080000, + + /// + /// Indicates input associated with a pointer h-wheel. For mouse pointers, this is equivalent to the action of the mouse horizontal scroll wheel (WM_MOUSEHWHEEL). + /// + HWheel = 0x00100000, + + /// + /// Indicates that this pointer was captured by (associated with) another element and the original element has lost capture (see WM_POINTERCAPTURECHANGED). + /// + CaptureChanged = 0x00200000, + } + + /// + /// Contains information about the state of a touch input + /// + [StructLayout(LayoutKind.Sequential)] + public struct RawPointerInput + { + public RawPointerType Type; + public int ID; + public uint FrameID; + public RawPointerFlags Flags; + public IntPtr SourceDevice; + public IntPtr TargetWindow; + public Point PixelLocation; + public Point HimetricLocation; + public Point PixelLocationRaw; + public Point HimetricLocationRaw; + public int Time; + public uint HistoryCount; + public int InputData; + public uint KeyStates; + public ulong PerformanceCount; + public RawPointerButtonType ButtonChangeType; + } + + /// + /// Contains information about the state of a touch input + /// + [StructLayout(LayoutKind.Sequential)] + public struct RawTouchInput + { + /// + /// The x-coordinate (horizontal point) of the touch input. This member is indicated in hundredths of a pixel of physical screen coordinates. + /// + public int X; + + /// + /// The y-coordinate (vertical point) of the touch input. This member is indicated in hundredths of a pixel of physical screen coordinates. + /// + public int Y; + + /// + /// A device handle for the source input device. Each device is given a unique provider at run time by the touch input provider. + /// + public IntPtr Source; + + /// + /// A touch point identifier that distinguishes a particular touch input. This value stays consistent in a touch contact sequence from the point a contact comes down until it comes back up. An ID may be reused later for subsequent contacts. + /// + public int ID; + + /// + /// A set of bit flags that specify various aspects of touch point press, release, and motion. The bits in this member can be any reasonable combination of the values in the Remarks section. + /// + public RawTouchFlags Flags; + + /// + /// A set of bit flags that specify which of the optional fields in the structure contain valid values. The availability of valid information in the optional fields is device-specific. Applications should use an optional field value only when the corresponding bit is set in Mask.. + /// + public RawTouchMaskFlags Mask; + + /// + /// The time stamp for the event, in milliseconds. The consuming application should note that the system performs no validation on this field; when the TOUCHINPUTMASKF_TIMEFROMSYSTEM flag is not set, the accuracy and sequencing of values in this field are completely dependent on the touch input provider. + /// + public int Time; + + /// + /// An additional value associated with the touch event. + /// + public int ExtraInfo; + + /// + /// The width of the touch contact area in hundredths of a pixel in physical screen coordinates. This value is only valid if the Mask member has the TOUCHEVENTFMASK_CONTACTAREA flag set. + /// + public uint AreaWidth; + + /// + /// The height of the touch contact area in hundredths of a pixel in physical screen coordinates. This value is only valid if the Mask member has the TOUCHEVENTFMASK_CONTACTAREA flag set. + /// + public uint AreaHeight; + } + + /// + /// Enumeration containing flags for raw touch input. + /// + [Flags] + public enum RawTouchFlags : uint + { + /// + /// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN. + /// + Move = 0x0001, + + /// + /// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP. + /// + Down = 0x0002, + + /// + /// A touch point was removed. + /// + Up = 0x0004, + + /// + /// A touch point is in range. This flag is used to enable touch hover support on compatible hardware. Applications that do not want support for hover can ignore this flag. + /// + InRange = 0x0008, + + /// + /// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following FontText for more information on primary touch points. + /// + Primary = 0x0010, + + /// + /// When received using GetTouchInputInfo, this input was not coalesced. + /// + NoCoalesce = 0x0020, + + /// + /// The touch event came from the user's palm. + /// + Palm = 0x0080, + } + + /// + /// Enumeration containing mask flags for raw touch input. + /// + [Flags] + public enum RawTouchMaskFlags : uint + { + /// + /// AreaWidth and AreaHeight are valid. + /// + ContactArea = 0x0004, + + /// + /// ExtraInfo is valid. + /// + ExtraInfo = 0x0002, + + /// + /// The system time was set in the TOUCHINPUT structure. + /// + TimeFromSystem = 0x0001, + } + + /// + /// Value type for raw input. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct RawInput + { + public RawInputHeader Header; + public RawInputData Data; + + public static readonly int SizeInBytes = + BlittableValueType.Stride; + } + + [StructLayout(LayoutKind.Explicit)] + public struct RawInputData + { + [FieldOffset(0)] public RawMouse Mouse; + [FieldOffset(0)] public RawKeyboard Keyboard; + [FieldOffset(0)] public RawInputHid HID; + } + + //unused structs + /// + /// Value type for raw input from a keyboard. + /// + [StructLayout(LayoutKind.Sequential)] + public struct RawKeyboard + { + /// Scan code for key depression. + public short MakeCode; + + /// Scan code information. + public RawKeyboardFlags Flags; + + /// Reserved. + public short Reserved; + + /// Virtual key code. + public VirtualKeys VirtualKey; + + /// Corresponding window message. + public WindowsMessages Message; + + /// Extra information. + public int ExtraInformation; + } + + /// + /// Enumeration containing flags for raw keyboard input. + /// + [Flags] + public enum RawKeyboardFlags : ushort + { + /// + KeyMake = 0, + + /// + KeyBreak = 1, + + /// + KeyE0 = 2, + + /// + KeyE1 = 4, + + /// + TerminalServerSetLED = 8, + + /// + TerminalServerShadow = 0x10, + + /// + TerminalServerVKPACKET = 0x20 + } + + public struct RawInputHid + { + } + + /// + /// Contains information about the state of the mouse. + /// + [StructLayout(LayoutKind.Explicit)] + public struct RawMouse + { + /// + /// The mouse state. + /// + [FieldOffset(0)] public RawMouseFlags Flags; + + /// + /// Flags for the event. + /// + [FieldOffset(4)] public RawMouseButtons ButtonFlags; + + /// + /// If the mouse wheel is moved, this will contain the delta amount. + /// + [FieldOffset(6)] public short ButtonData; + + /// + /// Raw button data. + /// + [FieldOffset(8)] public uint RawButtons; + + /// + /// The motion in the X direction. This is signed relative motion or + /// absolute motion, depending on the value of usFlags. + /// + [FieldOffset(12)] public int LastX; + + /// + /// The motion in the Y direction. This is signed relative motion or absolute motion, + /// depending on the value of usFlags. + /// + [FieldOffset(16)] public int LastY; + + /// + /// The device-specific additional information for the event. + /// + [FieldOffset(20)] public uint ExtraInformation; + } + + /// + /// Enumeration containing the flags for raw mouse data. + /// + [Flags] + public enum RawMouseFlags + : ushort + { + /// Relative to the last position. + MoveRelative = 0, + + /// Absolute positioning. + MoveAbsolute = 1, + + /// Coordinate data is mapped to a virtual desktop. + VirtualDesktop = 2, + + /// Attributes for the mouse have changed. + AttributesChanged = 4, + + /// WM_MOUSEMOVE and WM_INPUT don't coalesce + MoveNoCoalesce = 8, + } + + /// + /// Enumeration containing the button data for raw mouse input. + /// + [Flags] + public enum RawMouseButtons + : ushort + { + /// No button. + None = 0, + + /// Left (button 1) down. + LeftDown = 0x0001, + + /// Left (button 1) up. + LeftUp = 0x0002, + + /// Right (button 2) down. + RightDown = 0x0004, + + /// Right (button 2) up. + RightUp = 0x0008, + + /// Middle (button 3) down. + MiddleDown = 0x0010, + + /// Middle (button 3) up. + MiddleUp = 0x0020, + + /// Button 4 down. + Button4Down = 0x0040, + + /// Button 4 up. + Button4Up = 0x0080, + + /// Button 5 down. + Button5Down = 0x0100, + + /// Button 5 up. + Button5Up = 0x0200, + + /// Mouse wheel moved. + MouseWheel = 0x0400 + } + + /// + /// Enumeration contanining the command types to issue. + /// + public enum RawInputCommand + { + /// + /// Get input data. + /// + Input = 0x10000003, + + /// + /// Get header data. + /// + Header = 0x10000005 + } + + /// + /// Enumeration containing the type device the raw input is coming from. + /// + public enum RawInputType + { + /// + /// Mouse input. + /// + Mouse = 0, + + /// + /// Keyboard input. + /// + Keyboard = 1, + + /// + /// Another device that is not the keyboard or the mouse. + /// + HID = 2 + } + + /// + /// Value type for a raw input header. + /// + [StructLayout(LayoutKind.Sequential)] + public struct RawInputHeader + { + /// Type of device the input is coming from. + public RawInputType Type; + + /// Size of the packet of data. + public int Size; + + /// Handle to the device sending the data. + public IntPtr Device; + + /// wParam from the window message. + public IntPtr wParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RawInputDevice + { + /// Top level collection Usage page for the raw input device. + public HIDUsagePage UsagePage; + + /// Top level collection Usage for the raw input device. + public HIDUsage Usage; + + /// Mode flag that specifies how to interpret the information provided by UsagePage and Usage. + public RawInputDeviceFlags Flags; + + /// Handle to the target device. If NULL, it follows the keyboard focus. + public IntPtr WindowHandle; + } + + /// Enumeration containing flags for a raw input device. + [Flags] + public enum RawInputDeviceFlags + { + /// No flags. + None = 0, + + /// If set, this removes the top level collection from the inclusion list. This tells the operating system to stop reading from a device which matches the top level collection. + Remove = 0x00000001, + + /// If set, this specifies the top level collections to exclude when reading a complete usage page. This flag only affects a TLC whose usage page is already specified with PageOnly. + Exclude = 0x00000010, + + /// If set, this specifies all devices whose top level collection is from the specified usUsagePage. Note that Usage must be zero. To exclude a particular top level collection, use Exclude. + PageOnly = 0x00000020, + + /// If set, this prevents any devices specified by UsagePage or Usage from generating legacy messages. This is only for the mouse and keyboard. + NoLegacy = 0x00000030, + + /// If set, this enables the caller to receive the input even when the caller is not in the foreground. Note that WindowHandle must be specified. + InputSink = 0x00000100, + + /// If set, the mouse button click does not activate the other window. + CaptureMouse = 0x00000200, + + /// If set, the application-defined keyboard device hotkeys are not handled. However, the system hotkeys; for example, ALT+TAB and CTRL+ALT+DEL, are still handled. By default, all keyboard hotkeys are handled. NoHotKeys can be specified even if NoLegacy is not specified and WindowHandle is NULL. + NoHotKeys = 0x00000200, + + /// If set, application keys are handled. NoLegacy must be specified. Keyboard only. + AppKeys = 0x00000400 + } + + /// + /// Enumeration for virtual keys. + /// + public enum VirtualKeys + : ushort + { + /// + LeftButton = 0x01, + + /// + RightButton = 0x02, + + /// + Cancel = 0x03, + + /// + MiddleButton = 0x04, + + /// + ExtraButton1 = 0x05, + + /// + ExtraButton2 = 0x06, + + /// + Back = 0x08, + + /// + Tab = 0x09, + + /// + Clear = 0x0C, + + /// + Return = 0x0D, + + /// + Shift = 0x10, + + /// + Control = 0x11, + + /// + Menu = 0x12, + + /// + Pause = 0x13, + + /// + CapsLock = 0x14, + + /// + Kana = 0x15, + + /// + Hangeul = 0x15, + + /// + Hangul = 0x15, + + /// + Junja = 0x17, + + /// + Final = 0x18, + + /// + Hanja = 0x19, + + /// + Kanji = 0x19, + + /// + Escape = 0x1B, + + /// + Convert = 0x1C, + + /// + NonConvert = 0x1D, + + /// + Accept = 0x1E, + + /// + ModeChange = 0x1F, + + /// + Space = 0x20, + + /// + Prior = 0x21, + + /// + Next = 0x22, + + /// + End = 0x23, + + /// + Home = 0x24, + + /// + Left = 0x25, + + /// + Up = 0x26, + + /// + Right = 0x27, + + /// + Down = 0x28, + + /// + Select = 0x29, + + /// + Print = 0x2A, + + /// + Execute = 0x2B, + + /// + Snapshot = 0x2C, + + /// + Insert = 0x2D, + + /// + Delete = 0x2E, + + /// + Help = 0x2F, + + /// + N0 = 0x30, + + /// + N1 = 0x31, + + /// + N2 = 0x32, + + /// + N3 = 0x33, + + /// + N4 = 0x34, + + /// + N5 = 0x35, + + /// + N6 = 0x36, + + /// + N7 = 0x37, + + /// + N8 = 0x38, + + /// + N9 = 0x39, + + /// + A = 0x41, + + /// + B = 0x42, + + /// + C = 0x43, + + /// + D = 0x44, + + /// + E = 0x45, + + /// + F = 0x46, + + /// + G = 0x47, + + /// + H = 0x48, + + /// + I = 0x49, + + /// + J = 0x4A, + + /// + K = 0x4B, + + /// + L = 0x4C, + + /// + M = 0x4D, + + /// + N = 0x4E, + + /// + O = 0x4F, + + /// + P = 0x50, + + /// + Q = 0x51, + + /// + R = 0x52, + + /// + S = 0x53, + + /// + T = 0x54, + + /// + U = 0x55, + + /// + V = 0x56, + + /// + W = 0x57, + + /// + X = 0x58, + + /// + Y = 0x59, + + /// + Z = 0x5A, + + /// + LeftWindows = 0x5B, + + /// + RightWindows = 0x5C, + + /// + Application = 0x5D, + + /// + Sleep = 0x5F, + + /// + Numpad0 = 0x60, + + /// + Numpad1 = 0x61, + + /// + Numpad2 = 0x62, + + /// + Numpad3 = 0x63, + + /// + Numpad4 = 0x64, + + /// + Numpad5 = 0x65, + + /// + Numpad6 = 0x66, + + /// + Numpad7 = 0x67, + + /// + Numpad8 = 0x68, + + /// + Numpad9 = 0x69, + + /// + Multiply = 0x6A, + + /// + Add = 0x6B, + + /// + Separator = 0x6C, + + /// + Subtract = 0x6D, + + /// + Decimal = 0x6E, + + /// + Divide = 0x6F, + + /// + F1 = 0x70, + + /// + F2 = 0x71, + + /// + F3 = 0x72, + + /// + F4 = 0x73, + + /// + F5 = 0x74, + + /// + F6 = 0x75, + + /// + F7 = 0x76, + + /// + F8 = 0x77, + + /// + F9 = 0x78, + + /// + F10 = 0x79, + + /// + F11 = 0x7A, + + /// + F12 = 0x7B, + + /// + F13 = 0x7C, + + /// + F14 = 0x7D, + + /// + F15 = 0x7E, + + /// + F16 = 0x7F, + + /// + F17 = 0x80, + + /// + F18 = 0x81, + + /// + F19 = 0x82, + + /// + F20 = 0x83, + + /// + F21 = 0x84, + + /// + F22 = 0x85, + + /// + F23 = 0x86, + + /// + F24 = 0x87, + + /// + NumLock = 0x90, + + /// + ScrollLock = 0x91, + + /// + NEC_Equal = 0x92, + + /// + Fujitsu_Jisho = 0x92, + + /// + Fujitsu_Masshou = 0x93, + + /// + Fujitsu_Touroku = 0x94, + + /// + Fujitsu_Loya = 0x95, + + /// + Fujitsu_Roya = 0x96, + + /// + LeftShift = 0xA0, + + /// + RightShift = 0xA1, + + /// + LeftControl = 0xA2, + + /// + RightControl = 0xA3, + + /// + LeftMenu = 0xA4, + + /// + RightMenu = 0xA5, + + /// + BrowserBack = 0xA6, + + /// + BrowserForward = 0xA7, + + /// + BrowserRefresh = 0xA8, + + /// + BrowserStop = 0xA9, + + /// + BrowserSearch = 0xAA, + + /// + BrowserFavorites = 0xAB, + + /// + BrowserHome = 0xAC, + + /// + VolumeMute = 0xAD, + + /// + VolumeDown = 0xAE, + + /// + VolumeUp = 0xAF, + + /// + MediaNextTrack = 0xB0, + + /// + MediaPrevTrack = 0xB1, + + /// + MediaStop = 0xB2, + + /// + MediaPlayPause = 0xB3, + + /// + LaunchMail = 0xB4, + + /// + LaunchMediaSelect = 0xB5, + + /// + LaunchApplication1 = 0xB6, + + /// + LaunchApplication2 = 0xB7, + + /// + OEM1 = 0xBA, + + /// + OEMPlus = 0xBB, + + /// + OEMComma = 0xBC, + + /// + OEMMinus = 0xBD, + + /// + OEMPeriod = 0xBE, + + /// + OEM2 = 0xBF, + + /// + OEM3 = 0xC0, + + /// + OEM4 = 0xDB, + + /// + OEM5 = 0xDC, + + /// + OEM6 = 0xDD, + + /// + OEM7 = 0xDE, + + /// + OEM8 = 0xDF, + + /// + OEMAX = 0xE1, + + /// + OEM102 = 0xE2, + + /// + ICOHelp = 0xE3, + + /// + ICO00 = 0xE4, + + /// + ProcessKey = 0xE5, + + /// + ICOClear = 0xE6, + + /// + Packet = 0xE7, + + /// + OEMReset = 0xE9, + + /// + OEMJump = 0xEA, + + /// + OEMPA1 = 0xEB, + + /// + OEMPA2 = 0xEC, + + /// + OEMPA3 = 0xED, + + /// + OEMWSCtrl = 0xEE, + + /// + OEMCUSel = 0xEF, + + /// + OEMATTN = 0xF0, + + /// + OEMFinish = 0xF1, + + /// + OEMCopy = 0xF2, + + /// + OEMAuto = 0xF3, + + /// + OEMENLW = 0xF4, + + /// + OEMBackTab = 0xF5, + + /// + ATTN = 0xF6, + + /// + CRSel = 0xF7, + + /// + EXSel = 0xF8, + + /// + EREOF = 0xF9, + + /// + Play = 0xFA, + + /// + Zoom = 0xFB, + + /// + Noname = 0xFC, + + /// + PA1 = 0xFD, + + /// + OEMClear = 0xFE + } + + public enum HIDUsage : ushort + { + Pointer = 0x01, + Mouse = 0x02, + Joystick = 0x04, + Gamepad = 0x05, + Keyboard = 0x06, + Keypad = 0x07, + SystemControl = 0x80, + X = 0x30, + Y = 0x31, + Z = 0x32, + RelativeX = 0x33, + RelativeY = 0x34, + RelativeZ = 0x35, + Slider = 0x36, + Dial = 0x37, + Wheel = 0x38, + HatSwitch = 0x39, + CountedBuffer = 0x3A, + ByteCount = 0x3B, + MotionWakeup = 0x3C, + VX = 0x40, + VY = 0x41, + VZ = 0x42, + VBRX = 0x43, + VBRY = 0x44, + VBRZ = 0x45, + VNO = 0x46, + SystemControlPower = 0x81, + SystemControlSleep = 0x82, + SystemControlWake = 0x83, + SystemControlContextMenu = 0x84, + SystemControlMainMenu = 0x85, + SystemControlApplicationMenu = 0x86, + SystemControlHelpMenu = 0x87, + SystemControlMenuExit = 0x88, + SystemControlMenuSelect = 0x89, + SystemControlMenuRight = 0x8A, + SystemControlMenuLeft = 0x8B, + SystemControlMenuUp = 0x8C, + SystemControlMenuDown = 0x8D, + KeyboardNoEvent = 0x00, + KeyboardRollover = 0x01, + KeyboardPostFail = 0x02, + KeyboardUndefined = 0x03, + KeyboardaA = 0x04, + KeyboardzZ = 0x1D, + Keyboard1 = 0x1E, + Keyboard0 = 0x27, + KeyboardLeftControl = 0xE0, + KeyboardLeftShift = 0xE1, + KeyboardLeftALT = 0xE2, + KeyboardLeftGUI = 0xE3, + KeyboardRightControl = 0xE4, + KeyboardRightShift = 0xE5, + KeyboardRightALT = 0xE6, + KeyboardRightGUI = 0xE7, + KeyboardScrollLock = 0x47, + KeyboardNumLock = 0x53, + KeyboardCapsLock = 0x39, + KeyboardF1 = 0x3A, + KeyboardF12 = 0x45, + KeyboardReturn = 0x28, + KeyboardEscape = 0x29, + KeyboardDelete = 0x2A, + KeyboardPrintScreen = 0x46, + LEDNumLock = 0x01, + LEDCapsLock = 0x02, + LEDScrollLock = 0x03, + LEDCompose = 0x04, + LEDKana = 0x05, + LEDPower = 0x06, + LEDShift = 0x07, + LEDDoNotDisturb = 0x08, + LEDMute = 0x09, + LEDToneEnable = 0x0A, + LEDHighCutFilter = 0x0B, + LEDLowCutFilter = 0x0C, + LEDEqualizerEnable = 0x0D, + LEDSoundFieldOn = 0x0E, + LEDSurroundFieldOn = 0x0F, + LEDRepeat = 0x10, + LEDStereo = 0x11, + LEDSamplingRateDirect = 0x12, + LEDSpinning = 0x13, + LEDCAV = 0x14, + LEDCLV = 0x15, + LEDRecordingFormatDet = 0x16, + LEDOffHook = 0x17, + LEDRing = 0x18, + LEDMessageWaiting = 0x19, + LEDDataMode = 0x1A, + LEDBatteryOperation = 0x1B, + LEDBatteryOK = 0x1C, + LEDBatteryLow = 0x1D, + LEDSpeaker = 0x1E, + LEDHeadset = 0x1F, + LEDHold = 0x20, + LEDMicrophone = 0x21, + LEDCoverage = 0x22, + LEDNightMode = 0x23, + LEDSendCalls = 0x24, + LEDCallPickup = 0x25, + LEDConference = 0x26, + LEDStandBy = 0x27, + LEDCameraOn = 0x28, + LEDCameraOff = 0x29, + LEDOnLine = 0x2A, + LEDOffLine = 0x2B, + LEDBusy = 0x2C, + LEDReady = 0x2D, + LEDPaperOut = 0x2E, + LEDPaperJam = 0x2F, + LEDRemote = 0x30, + LEDForward = 0x31, + LEDReverse = 0x32, + LEDStop = 0x33, + LEDRewind = 0x34, + LEDFastForward = 0x35, + LEDPlay = 0x36, + LEDPause = 0x37, + LEDRecord = 0x38, + LEDError = 0x39, + LEDSelectedIndicator = 0x3A, + LEDInUseIndicator = 0x3B, + LEDMultiModeIndicator = 0x3C, + LEDIndicatorOn = 0x3D, + LEDIndicatorFlash = 0x3E, + LEDIndicatorSlowBlink = 0x3F, + LEDIndicatorFastBlink = 0x40, + LEDIndicatorOff = 0x41, + LEDFlashOnTime = 0x42, + LEDSlowBlinkOnTime = 0x43, + LEDSlowBlinkOffTime = 0x44, + LEDFastBlinkOnTime = 0x45, + LEDFastBlinkOffTime = 0x46, + LEDIndicatorColor = 0x47, + LEDRed = 0x48, + LEDGreen = 0x49, + LEDAmber = 0x4A, + LEDGenericIndicator = 0x3B, + TelephonyPhone = 0x01, + TelephonyAnsweringMachine = 0x02, + TelephonyMessageControls = 0x03, + TelephonyHandset = 0x04, + TelephonyHeadset = 0x05, + TelephonyKeypad = 0x06, + TelephonyProgrammableButton = 0x07, + SimulationRudder = 0xBA, + SimulationThrottle = 0xBB + } + + public enum HIDUsagePage : ushort + { + Undefined = 0x00, + Generic = 0x01, + Simulation = 0x02, + VR = 0x03, + Sport = 0x04, + Game = 0x05, + Keyboard = 0x07, + LED = 0x08, + Button = 0x09, + Ordinal = 0x0A, + Telephony = 0x0B, + Consumer = 0x0C, + Digitizer = 0x0D, + PID = 0x0F, + Unicode = 0x10, + AlphaNumeric = 0x14, + Medical = 0x40, + MonitorPage0 = 0x80, + MonitorPage1 = 0x81, + MonitorPage2 = 0x82, + MonitorPage3 = 0x83, + PowerPage0 = 0x84, + PowerPage1 = 0x85, + PowerPage2 = 0x86, + PowerPage3 = 0x87, + BarCode = 0x8C, + Scale = 0x8D, + MSR = 0x8E + } + + /// + /// Windows Messages + /// Defined in winuser.h from Windows SDK v6.1 + /// Documentation pulled from MSDN. + /// + public enum WindowsMessages : uint + { + /// + /// The WM_NULL message performs no operation. An application sends the WM_NULL message if it wants to post a message that the recipient window will ignore. + /// + NULL = 0x0000, + + /// + /// The WM_CREATE message is sent when an application requests that a window be created by calling the CreateWindowEx or CreateWindow function. (The message is sent before the function returns.) The window procedure of the new window receives this message after the window is created, but before the window becomes visible. + /// + CREATE = 0x0001, + + /// + /// The WM_DESTROY message is sent when a window is being destroyed. It is sent to the window procedure of the window being destroyed after the window is removed from the screen. + /// This message is sent first to the window being destroyed and then to the child windows (if any) as they are destroyed. During the processing of the message, it can be assumed that all child windows still exist. + /// /// + DESTROY = 0x0002, + + /// + /// The WM_MOVE message is sent after a window has been moved. + /// + MOVE = 0x0003, + + /// + /// The WM_SIZE message is sent to a window after its size has changed. + /// + SIZE = 0x0005, + + /// + /// The WM_ACTIVATE message is sent to both the window being activated and the window being deactivated. If the windows use the same input queue, the message is sent synchronously, first to the window procedure of the top-level window being deactivated, then to the window procedure of the top-level window being activated. If the windows use different input queues, the message is sent asynchronously, so the window is activated immediately. + /// + ACTIVATE = 0x0006, + + /// + /// The WM_SETFOCUS message is sent to a window after it has gained the keyboard focus. + /// + SETFOCUS = 0x0007, + + /// + /// The WM_KILLFOCUS message is sent to a window immediately before it loses the keyboard focus. + /// + KILLFOCUS = 0x0008, + + /// + /// The WM_ENABLE message is sent when an application changes the enabled state of a window. It is sent to the window whose enabled state is changing. This message is sent before the EnableWindow function returns, but after the enabled state (WS_DISABLED style bit) of the window has changed. + /// + ENABLE = 0x000A, + + /// + /// An application sends the WM_SETREDRAW message to a window to allow changes in that window to be redrawn or to prevent changes in that window from being redrawn. + /// + SETREDRAW = 0x000B, + + /// + /// An application sends a WM_SETTEXT message to set the FontText of a window. + /// + SETTEXT = 0x000C, + + /// + /// An application sends a WM_GETTEXT message to copy the FontText that corresponds to a window into a buffer provided by the caller. + /// + GETTEXT = 0x000D, + + /// + /// An application sends a WM_GETTEXTLENGTH message to determine the length, in characters, of the FontText associated with a window. + /// + GETTEXTLENGTH = 0x000E, + + /// + /// The WM_PAINT message is sent when the system or another application makes a request to paint a portion of an application's window. The message is sent when the UpdateWindow or RedrawWindow function is called, or by the DispatchMessage function when the application obtains a WM_PAINT message by using the GetMessage or PeekMessage function. + /// + PAINT = 0x000F, + + /// + /// The WM_CLOSE message is sent as a signal that a window or an application should terminate. + /// + CLOSE = 0x0010, + + /// + /// The WM_QUERYENDSESSION message is sent when the user chooses to end the session or when an application calls one of the system shutdown functions. If any application returns zero, the session is not ended. The system stops sending WM_QUERYENDSESSION messages as soon as one application returns zero. + /// After processing this message, the system sends the WM_ENDSESSION message with the wParam parameter set to the results of the WM_QUERYENDSESSION message. + /// + QUERYENDSESSION = 0x0011, + + /// + /// The WM_QUERYOPEN message is sent to an icon when the user requests that the window be restored to its previous size and position. + /// + QUERYOPEN = 0x0013, + + /// + /// The WM_ENDSESSION message is sent to an application after the system processes the results of the WM_QUERYENDSESSION message. The WM_ENDSESSION message informs the application whether the session is ending. + /// + ENDSESSION = 0x0016, + + /// + /// The WM_QUIT message indicates a request to terminate an application and is generated when the application calls the PostQuitMessage function. It causes the GetMessage function to return zero. + /// + QUIT = 0x0012, + + /// + /// The WM_ERASEBKGND message is sent when the window background must be erased (for example, when a window is resized). The message is sent to prepare an invalidated portion of a window for painting. + /// + ERASEBKGND = 0x0014, + + /// + /// This message is sent to all top-level windows when a change is made to a system color setting. + /// + SYSCOLORCHANGE = 0x0015, + + /// + /// The WM_SHOWWINDOW message is sent to a window when the window is about to be hidden or shown. + /// + SHOWWINDOW = 0x0018, + + /// + /// An application sends the WM_WININICHANGE message to all top-level windows after making a change to the WIN.INI file. The SystemParametersInfo function sends this message after an application uses the function to change a setting in WIN.INI. + /// Note The WM_WININICHANGE message is provided only for compatibility with earlier versions of the system. Applications should use the WM_SETTINGCHANGE message. + /// + WININICHANGE = 0x001A, + + /// + /// An application sends the WM_WININICHANGE message to all top-level windows after making a change to the WIN.INI file. The SystemParametersInfo function sends this message after an application uses the function to change a setting in WIN.INI. + /// Note The WM_WININICHANGE message is provided only for compatibility with earlier versions of the system. Applications should use the WM_SETTINGCHANGE message. + /// + SETTINGCHANGE = WININICHANGE, + + /// + /// The WM_DEVMODECHANGE message is sent to all top-level windows whenever the user changes device-mode settings. + /// + DEVMODECHANGE = 0x001B, + + /// + /// The WM_ACTIVATEAPP message is sent when a window belonging to a different application than the active window is about to be activated. The message is sent to the application whose window is being activated and to the application whose window is being deactivated. + /// + ACTIVATEAPP = 0x001C, + + /// + /// An application sends the WM_FONTCHANGE message to all top-level windows in the system after changing the pool of font resources. + /// + FONTCHANGE = 0x001D, + + /// + /// A message that is sent whenever there is a change in the system time. + /// + TIMECHANGE = 0x001E, + + /// + /// The WM_CANCELMODE message is sent to cancel certain modes, such as mouse capture. For example, the system sends this message to the active window when a dialog box or message box is displayed. Certain functions also send this message explicitly to the specified window regardless of whether it is the active window. For example, the EnableWindow function sends this message when disabling the specified window. + /// + CANCELMODE = 0x001F, + + /// + /// The WM_SETCURSOR message is sent to a window if the mouse causes the cursor to move within a window and mouse input is not captured. + /// + SETCURSOR = 0x0020, + + /// + /// The WM_MOUSEACTIVATE message is sent when the cursor is in an inactive window and the user presses a mouse button. The parent window receives this message only if the child window passes it to the DefWindowProc function. + /// + MOUSEACTIVATE = 0x0021, + + /// + /// The WM_CHILDACTIVATE message is sent to a child window when the user clicks the window's title bar or when the window is activated, moved, or sized. + /// + CHILDACTIVATE = 0x0022, + + /// + /// The WM_QUEUESYNC message is sent by a computer-based training (CBT) application to separate user-input messages from other messages sent through the WH_JOURNALPLAYBACK Hook procedure. + /// + QUEUESYNC = 0x0023, + + /// + /// The WM_GETMINMAXINFO message is sent to a window when the size or position of the window is about to change. An application can use this message to override the window's default maximized size and position, or its default minimum or maximum tracking size. + /// + GETMINMAXINFO = 0x0024, + + /// + /// Windows NT 3.51 and earlier: The WM_PAINTICON message is sent to a minimized window when the icon is to be painted. This message is not sent by newer versions of Microsoft Windows, except in unusual circumstances explained in the Remarks. + /// + PAINTICON = 0x0026, + + /// + /// Windows NT 3.51 and earlier: The WM_ICONERASEBKGND message is sent to a minimized window when the background of the icon must be filled before painting the icon. A window receives this message only if a class icon is defined for the window; otherwise, WM_ERASEBKGND is sent. This message is not sent by newer versions of Windows. + /// + ICONERASEBKGND = 0x0027, + + /// + /// The WM_NEXTDLGCTL message is sent to a dialog box procedure to set the keyboard focus to a different control in the dialog box. + /// + NEXTDLGCTL = 0x0028, + + /// + /// The WM_SPOOLERSTATUS message is sent from Print Manager whenever a job is added to or removed from the Print Manager queue. + /// + SPOOLERSTATUS = 0x002A, + + /// + /// The WM_DRAWITEM message is sent to the parent window of an owner-drawn button, combo box, list box, or menu when a visual aspect of the button, combo box, list box, or menu has changed. + /// + DRAWITEM = 0x002B, + + /// + /// The WM_MEASUREITEM message is sent to the owner window of a combo box, list box, list view control, or menu item when the control or menu is created. + /// + MEASUREITEM = 0x002C, + + /// + /// Sent to the owner of a list box or combo box when the list box or combo box is destroyed or when items are removed by the LB_DELETESTRING, LB_RESETCONTENT, CB_DELETESTRING, or CB_RESETCONTENT message. The system sends a WM_DELETEITEM message for each deleted item. The system sends the WM_DELETEITEM message for any deleted list box or combo box item with nonzero item data. + /// + DELETEITEM = 0x002D, + + /// + /// Sent by a list box with the LBS_WANTKEYBOARDINPUT style to its owner in response to a WM_KEYDOWN message. + /// + VKEYTOITEM = 0x002E, + + /// + /// Sent by a list box with the LBS_WANTKEYBOARDINPUT style to its owner in response to a WM_CHAR message. + /// + CHARTOITEM = 0x002F, + + /// + /// An application sends a WM_SETFONT message to specify the font that a control is to use when drawing FontText. + /// + SETFONT = 0x0030, + + /// + /// An application sends a WM_GETFONT message to a control to retrieve the font with which the control is currently drawing its FontText. + /// + GETFONT = 0x0031, + + /// + /// An application sends a WM_SETHOTKEY message to a window to associate a hot key with the window. When the user presses the hot key, the system activates the window. + /// + SETHOTKEY = 0x0032, + + /// + /// An application sends a WM_GETHOTKEY message to determine the hot key associated with a window. + /// + GETHOTKEY = 0x0033, + + /// + /// The WM_QUERYDRAGICON message is sent to a minimized (iconic) window. The window is about to be dragged by the user but does not have an icon defined for its class. An application can return a handle to an icon or cursor. The system displays this cursor or icon while the user drags the icon. + /// + QUERYDRAGICON = 0x0037, + + /// + /// The system sends the WM_COMPAREITEM message to determine the relative position of a new item in the sorted list of an owner-drawn combo box or list box. Whenever the application adds a new item, the system sends this message to the owner of a combo box or list box created with the CBS_SORT or LBS_SORT style. + /// + COMPAREITEM = 0x0039, + + /// + /// Active Accessibility sends the WM_GETOBJECT message to obtain information about an accessible object contained in a server application. + /// Applications never send this message directly. It is sent only by Active Accessibility in response to calls to AccessibleObjectFromPoint, AccessibleObjectFromEvent, or AccessibleObjectFromWindow. However, server applications handle this message. + /// + GETOBJECT = 0x003D, + + /// + /// The WM_COMPACTING message is sent to all top-level windows when the system detects more than 12.5 percent of system time over a 30- to 60-second interval is being spent compacting memory. This indicates that system memory is low. + /// + COMPACTING = 0x0041, + + /// + /// WM_COMMNOTIFY is Obsolete for Win32-Based Applications + /// + [Obsolete] COMMNOTIFY = 0x0044, + + /// + /// The WM_WINDOWPOSCHANGING message is sent to a window whose size, position, or place in the Z order is about to change as a result of a call to the SetWindowPos function or another window-management function. + /// + WINDOWPOSCHANGING = 0x0046, + + /// + /// The WM_WINDOWPOSCHANGED message is sent to a window whose size, position, or place in the Z order has changed as a result of a call to the SetWindowPos function or another window-management function. + /// + WINDOWPOSCHANGED = 0x0047, + + /// + /// Notifies applications that the system, typically a battery-powered personal computer, is about to enter a suspended mode. + /// Use: POWERBROADCAST + /// + [Obsolete] POWER = 0x0048, + + /// + /// An application sends the WM_COPYDATA message to pass data to another application. + /// + COPYDATA = 0x004A, + + /// + /// The WM_CANCELJOURNAL message is posted to an application when a user cancels the application's journaling activities. The message is posted with a NULL window handle. + /// + CANCELJOURNAL = 0x004B, + + /// + /// Sent by a common control to its parent window when an event has occurred or the control requires some information. + /// + NOTIFY = 0x004E, + + /// + /// The WM_INPUTLANGCHANGEREQUEST message is posted to the window with the focus when the user chooses a new input language, either with the hotkey (specified in the Keyboard control panel application) or from the indicator on the system taskbar. An application can accept the change by passing the message to the DefWindowProc function or reject the change (and prevent it from taking place) by returning immediately. + /// + INPUTLANGCHANGEREQUEST = 0x0050, + + /// + /// The WM_INPUTLANGCHANGE message is sent to the topmost affected window after an application's input language has been changed. You should make any application-specific settings and pass the message to the DefWindowProc function, which passes the message to all first-level child windows. These child windows can pass the message to DefWindowProc to have it pass the message to their child windows, and so on. + /// + INPUTLANGCHANGE = 0x0051, + + /// + /// Sent to an application that has initiated a training card with Microsoft Windows Help. The message informs the application when the user clicks an authorable button. An application initiates a training card by specifying the HELP_TCARD command in a call to the WinHelp function. + /// + TCARD = 0x0052, + + /// + /// Indicates that the user pressed the F1 key. If a menu is active when F1 is pressed, WM_HELP is sent to the window associated with the menu; otherwise, WM_HELP is sent to the window that has the keyboard focus. If no window has the keyboard focus, WM_HELP is sent to the currently active window. + /// + HELP = 0x0053, + + /// + /// The WM_USERCHANGED message is sent to all windows after the user has logged on or off. When the user logs on or off, the system updates the user-specific settings. The system sends this message immediately after updating the settings. + /// + USERCHANGED = 0x0054, + + /// + /// Determines if a window accepts ANSI or Unicode structures in the WM_NOTIFY notification message. WM_NOTIFYFORMAT messages are sent from a common control to its parent window and from the parent window to the common control. + /// + NOTIFYFORMAT = 0x0055, + + /// + /// The WM_CONTEXTMENU message notifies a window that the user clicked the right mouse button (right-clicked) in the window. + /// + CONTEXTMENU = 0x007B, + + /// + /// The WM_STYLECHANGING message is sent to a window when the SetWindowLong function is about to change one or more of the window's styles. + /// + STYLECHANGING = 0x007C, + + /// + /// The WM_STYLECHANGED message is sent to a window after the SetWindowLong function has changed one or more of the window's styles + /// + STYLECHANGED = 0x007D, + + /// + /// The WM_DISPLAYCHANGE message is sent to all windows when the display resolution has changed. + /// + DISPLAYCHANGE = 0x007E, + + /// + /// The WM_GETICON message is sent to a window to retrieve a handle to the large or small icon associated with a window. The system displays the large icon in the ALT+TAB dialog, and the small icon in the window caption. + /// + GETICON = 0x007F, + + /// + /// An application sends the WM_SETICON message to associate a new large or small icon with a window. The system displays the large icon in the ALT+TAB dialog box, and the small icon in the window caption. + /// + SETICON = 0x0080, + + /// + /// The WM_NCCREATE message is sent prior to the WM_CREATE message when a window is first created. + /// + NCCREATE = 0x0081, + + /// + /// The WM_NCDESTROY message informs a window that its nonclient area is being destroyed. The DestroyWindow function sends the WM_NCDESTROY message to the window following the WM_DESTROY message. WM_DESTROY is used to free the allocated memory object associated with the window. + /// The WM_NCDESTROY message is sent after the child windows have been destroyed. In contrast, WM_DESTROY is sent before the child windows are destroyed. + /// + NCDESTROY = 0x0082, + + /// + /// The WM_NCCALCSIZE message is sent when the size and position of a window's client area must be calculated. By processing this message, an application can control the content of the window's client area when the size or position of the window changes. + /// + NCCALCSIZE = 0x0083, + + /// + /// The WM_NCHITTEST message is sent to a window when the cursor moves, or when a mouse button is pressed or released. If the mouse is not captured, the message is sent to the window beneath the cursor. Otherwise, the message is sent to the window that has captured the mouse. + /// + NCHITTEST = 0x0084, + + /// + /// The WM_NCPAINT message is sent to a window when its frame must be painted. + /// + NCPAINT = 0x0085, + + /// + /// The WM_NCACTIVATE message is sent to a window when its nonclient area needs to be changed to indicate an active or inactive state. + /// + NCACTIVATE = 0x0086, + + /// + /// The WM_GETDLGCODE message is sent to the window procedure associated with a control. By default, the system handles all keyboard input to the control; the system interprets certain types of keyboard input as dialog box navigation keys. To override this default behavior, the control can respond to the WM_GETDLGCODE message to indicate the types of input it wants to process itself. + /// + GETDLGCODE = 0x0087, + + /// + /// The WM_SYNCPAINT message is used to synchronize painting while avoiding linking independent GUI threads. + /// + SYNCPAINT = 0x0088, + + /// + /// The WM_NCMOUSEMOVE message is posted to a window when the cursor is moved within the nonclient area of the window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCMOUSEMOVE = 0x00A0, + + /// + /// The WM_NCLBUTTONDOWN message is posted when the user presses the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCLBUTTONDOWN = 0x00A1, + + /// + /// The WM_NCLBUTTONUP message is posted when the user releases the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCLBUTTONUP = 0x00A2, + + /// + /// The WM_NCLBUTTONDBLCLK message is posted when the user double-clicks the left mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCLBUTTONDBLCLK = 0x00A3, + + /// + /// The WM_NCRBUTTONDOWN message is posted when the user presses the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCRBUTTONDOWN = 0x00A4, + + /// + /// The WM_NCRBUTTONUP message is posted when the user releases the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCRBUTTONUP = 0x00A5, + + /// + /// The WM_NCRBUTTONDBLCLK message is posted when the user double-clicks the right mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCRBUTTONDBLCLK = 0x00A6, + + /// + /// The WM_NCMBUTTONDOWN message is posted when the user presses the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCMBUTTONDOWN = 0x00A7, + + /// + /// The WM_NCMBUTTONUP message is posted when the user releases the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCMBUTTONUP = 0x00A8, + + /// + /// The WM_NCMBUTTONDBLCLK message is posted when the user double-clicks the middle mouse button while the cursor is within the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCMBUTTONDBLCLK = 0x00A9, + + /// + /// The WM_NCXBUTTONDOWN message is posted when the user presses the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCXBUTTONDOWN = 0x00AB, + + /// + /// The WM_NCXBUTTONUP message is posted when the user releases the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCXBUTTONUP = 0x00AC, + + /// + /// The WM_NCXBUTTONDBLCLK message is posted when the user double-clicks the first or second X button while the cursor is in the nonclient area of a window. This message is posted to the window that contains the cursor. If a window has captured the mouse, this message is not posted. + /// + NCXBUTTONDBLCLK = 0x00AD, + + /// + /// The WM_INPUT_DEVICE_CHANGE message is sent to the window that registered to receive raw input. A window receives this message through its WindowProc function. + /// + INPUT_DEVICE_CHANGE = 0x00FE, + + /// + /// The WM_INPUT message is sent to the window that is getting raw input. + /// + INPUT = 0x00FF, + + /// + /// This message filters for keyboard messages. + /// + KEYFIRST = 0x0100, + + /// + /// The WM_KEYDOWN message is posted to the window with the keyboard focus when a nonsystem key is pressed. A nonsystem key is a key that is pressed when the ALT key is not pressed. + /// + KEYDOWN = 0x0100, + + /// + /// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus. + /// + KEYUP = 0x0101, + + /// + /// The WM_CHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed. + /// + CHAR = 0x0102, + + /// + /// The WM_DEADCHAR message is posted to the window with the keyboard focus when a WM_KEYUP message is translated by the TranslateMessage function. WM_DEADCHAR specifies a character code generated by a dead key. A dead key is a key that generates a character, such as the umlaut (double-dot), that is combined with another character to form a composite character. For example, the umlaut-O character (Ö) is generated by typing the dead key for the umlaut character, and then typing the O key. + /// + DEADCHAR = 0x0103, + + /// + /// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when the user presses the F10 key (which activates the menu bar) or holds down the ALT key and then presses another key. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYDOWN message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter. + /// + SYSKEYDOWN = 0x0104, + + /// + /// The WM_SYSKEYUP message is posted to the window with the keyboard focus when the user releases a key that was pressed while the ALT key was held down. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYUP message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter. + /// + SYSKEYUP = 0x0105, + + /// + /// The WM_SYSCHAR message is posted to the window with the keyboard focus when a WM_SYSKEYDOWN message is translated by the TranslateMessage function. It specifies the character code of a system character key — that is, a character key that is pressed while the ALT key is down. + /// + SYSCHAR = 0x0106, + + /// + /// The WM_SYSDEADCHAR message is sent to the window with the keyboard focus when a WM_SYSKEYDOWN message is translated by the TranslateMessage function. WM_SYSDEADCHAR specifies the character code of a system dead key — that is, a dead key that is pressed while holding down the ALT key. + /// + SYSDEADCHAR = 0x0107, + + /// + /// The WM_UNICHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_UNICHAR message contains the character code of the key that was pressed. + /// The WM_UNICHAR message is equivalent to WM_CHAR, but it uses Unicode Transformation Format (UTF)-32, whereas WM_CHAR uses UTF-16. It is designed to send or post Unicode characters to ANSI windows and it can can handle Unicode Supplementary Plane characters. + /// + UNICHAR = 0x0109, + + /// + /// This message filters for keyboard messages. + /// + KEYLAST = 0x0109, + + /// + /// Sent immediately before the IME generates the composition string as a result of a keystroke. A window receives this message through its WindowProc function. + /// + IME_STARTCOMPOSITION = 0x010D, + + /// + /// Sent to an application when the IME ends composition. A window receives this message through its WindowProc function. + /// + IME_ENDCOMPOSITION = 0x010E, + + /// + /// Sent to an application when the IME changes composition status as a result of a keystroke. A window receives this message through its WindowProc function. + /// + IME_COMPOSITION = 0x010F, + IME_KEYLAST = 0x010F, + + /// + /// The WM_INITDIALOG message is sent to the dialog box procedure immediately before a dialog box is displayed. Dialog box procedures typically use this message to initialize controls and carry out any other initialization tasks that affect the appearance of the dialog box. + /// + INITDIALOG = 0x0110, + + /// + /// The WM_COMMAND message is sent when the user selects a command item from a menu, when a control sends a notification message to its parent window, or when an accelerator keystroke is translated. + /// + COMMAND = 0x0111, + + /// + /// A window receives this message when the user chooses a command from the Window menu, clicks the maximize button, minimize button, restore button, close button, or moves the form. You can stop the form from moving by filtering this out. + /// + SYSCOMMAND = 0x0112, + + /// + /// The WM_TIMER message is posted to the installing thread's message queue when a timer expires. The message is posted by the GetMessage or PeekMessage function. + /// + TIMER = 0x0113, + + /// + /// The WM_HSCROLL message is sent to a window when a scroll event occurs in the window's standard horizontal scroll bar. This message is also sent to the owner of a horizontal scroll bar control when a scroll event occurs in the control. + /// + HSCROLL = 0x0114, + + /// + /// The WM_VSCROLL message is sent to a window when a scroll event occurs in the window's standard vertical scroll bar. This message is also sent to the owner of a vertical scroll bar control when a scroll event occurs in the control. + /// + VSCROLL = 0x0115, + + /// + /// The WM_INITMENU message is sent when a menu is about to become active. It occurs when the user clicks an item on the menu bar or presses a menu key. This allows the application to modify the menu before it is displayed. + /// + INITMENU = 0x0116, + + /// + /// The WM_INITMENUPOPUP message is sent when a drop-down menu or submenu is about to become active. This allows an application to modify the menu before it is displayed, without changing the entire menu. + /// + INITMENUPOPUP = 0x0117, + + /// + /// The WM_MENUSELECT message is sent to a menu's owner window when the user selects a menu item. + /// + MENUSELECT = 0x011F, + + /// + /// The WM_MENUCHAR message is sent when a menu is active and the user presses a key that does not correspond to any mnemonic or accelerator key. This message is sent to the window that owns the menu. + /// + MENUCHAR = 0x0120, + + /// + /// The WM_ENTERIDLE message is sent to the owner window of a modal dialog box or menu that is entering an idle state. A modal dialog box or menu enters an idle state when no messages are waiting in its queue after it has processed one or more previous messages. + /// + ENTERIDLE = 0x0121, + + /// + /// The WM_MENURBUTTONUP message is sent when the user releases the right mouse button while the cursor is on a menu item. + /// + MENURBUTTONUP = 0x0122, + + /// + /// The WM_MENUDRAG message is sent to the owner of a drag-and-drop menu when the user drags a menu item. + /// + MENUDRAG = 0x0123, + + /// + /// The WM_MENUGETOBJECT message is sent to the owner of a drag-and-drop menu when the mouse cursor enters a menu item or moves from the center of the item to the top or bottom of the item. + /// + MENUGETOBJECT = 0x0124, + + /// + /// The WM_UNINITMENUPOPUP message is sent when a drop-down menu or submenu has been destroyed. + /// + UNINITMENUPOPUP = 0x0125, + + /// + /// The WM_MENUCOMMAND message is sent when the user makes a selection from a menu. + /// + MENUCOMMAND = 0x0126, + + /// + /// An application sends the WM_CHANGEUISTATE message to indicate that the user interface (UI) state should be changed. + /// + CHANGEUISTATE = 0x0127, + + /// + /// An application sends the WM_UPDATEUISTATE message to change the user interface (UI) state for the specified window and all its child windows. + /// + UPDATEUISTATE = 0x0128, + + /// + /// An application sends the WM_QUERYUISTATE message to retrieve the user interface (UI) state for a window. + /// + QUERYUISTATE = 0x0129, + + /// + /// The WM_CTLCOLORMSGBOX message is sent to the owner window of a message box before Windows draws the message box. By responding to this message, the owner window can set the FontText and background colors of the message box by using the given display device context handle. + /// + CTLCOLORMSGBOX = 0x0132, + + /// + /// An edit control that is not read-only or disabled sends the WM_CTLCOLOREDIT message to its parent window when the control is about to be drawn. By responding to this message, the parent window can use the specified device context handle to set the FontText and background colors of the edit control. + /// + CTLCOLOREDIT = 0x0133, + + /// + /// Sent to the parent window of a list box before the system draws the list box. By responding to this message, the parent window can set the FontText and background colors of the list box by using the specified display device context handle. + /// + CTLCOLORLISTBOX = 0x0134, + + /// + /// The WM_CTLCOLORBTN message is sent to the parent window of a button before drawing the button. The parent window can change the button's FontText and background colors. However, only owner-drawn buttons respond to the parent window processing this message. + /// + CTLCOLORBTN = 0x0135, + + /// + /// The WM_CTLCOLORDLG message is sent to a dialog box before the system draws the dialog box. By responding to this message, the dialog box can set its FontText and background colors using the specified display device context handle. + /// + CTLCOLORDLG = 0x0136, + + /// + /// The WM_CTLCOLORSCROLLBAR message is sent to the parent window of a scroll bar control when the control is about to be drawn. By responding to this message, the parent window can use the display context handle to set the background color of the scroll bar control. + /// + CTLCOLORSCROLLBAR = 0x0137, + + /// + /// A static control, or an edit control that is read-only or disabled, sends the WM_CTLCOLORSTATIC message to its parent window when the control is about to be drawn. By responding to this message, the parent window can use the specified device context handle to set the FontText and background colors of the static control. + /// + CTLCOLORSTATIC = 0x0138, + + /// + /// Use WM_MOUSEFIRST to specify the first mouse message. Use the PeekMessage() Function. + /// + MOUSEFIRST = 0x0200, + + /// + /// The WM_MOUSEMOVE message is posted to a window when the cursor moves. If the mouse is not captured, the message is posted to the window that contains the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + MOUSEMOVE = 0x0200, + + /// + /// The WM_LBUTTONDOWN message is posted when the user presses the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + LBUTTONDOWN = 0x0201, + + /// + /// The WM_LBUTTONUP message is posted when the user releases the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + LBUTTONUP = 0x0202, + + /// + /// The WM_LBUTTONDBLCLK message is posted when the user double-clicks the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + LBUTTONDBLCLK = 0x0203, + + /// + /// The WM_RBUTTONDOWN message is posted when the user presses the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + RBUTTONDOWN = 0x0204, + + /// + /// The WM_RBUTTONUP message is posted when the user releases the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + RBUTTONUP = 0x0205, + + /// + /// The WM_RBUTTONDBLCLK message is posted when the user double-clicks the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + RBUTTONDBLCLK = 0x0206, + + /// + /// The WM_MBUTTONDOWN message is posted when the user presses the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + MBUTTONDOWN = 0x0207, + + /// + /// The WM_MBUTTONUP message is posted when the user releases the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + MBUTTONUP = 0x0208, + + /// + /// The WM_MBUTTONDBLCLK message is posted when the user double-clicks the middle mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + MBUTTONDBLCLK = 0x0209, + + /// + /// The WM_MOUSEWHEEL message is sent to the focus window when the mouse wheel is rotated. The DefWindowProc function propagates the message to the window's parent. There should be no public forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it. + /// + MOUSEWHEEL = 0x020A, + + /// + /// The WM_XBUTTONDOWN message is posted when the user presses the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + XBUTTONDOWN = 0x020B, + + /// + /// The WM_XBUTTONUP message is posted when the user releases the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + XBUTTONUP = 0x020C, + + /// + /// The WM_XBUTTONDBLCLK message is posted when the user double-clicks the first or second X button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse. + /// + XBUTTONDBLCLK = 0x020D, + + /// + /// The WM_MOUSEHWHEEL message is sent to the focus window when the mouse's horizontal scroll wheel is tilted or rotated. The DefWindowProc function propagates the message to the window's parent. There should be no public forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it. + /// + MOUSEHWHEEL = 0x020E, + + /// + /// Use WM_MOUSELAST to specify the last mouse message. Used with PeekMessage() Function. + /// + MOUSELAST = 0x020E, + + /// + /// The WM_PARENTNOTIFY message is sent to the parent of a child window when the child window is created or destroyed, or when the user clicks a mouse button while the cursor is over the child window. When the child window is being created, the system sends WM_PARENTNOTIFY just before the CreateWindow or CreateWindowEx function that creates the window returns. When the child window is being destroyed, the system sends the message before any processing to destroy the window takes place. + /// + PARENTNOTIFY = 0x0210, + + /// + /// The WM_ENTERMENULOOP message informs an application's main window procedure that a menu modal loop has been entered. + /// + ENTERMENULOOP = 0x0211, + + /// + /// The WM_EXITMENULOOP message informs an application's main window procedure that a menu modal loop has been exited. + /// + EXITMENULOOP = 0x0212, + + /// + /// The WM_NEXTMENU message is sent to an application when the right or left arrow key is used to switch between the menu bar and the system menu. + /// + NEXTMENU = 0x0213, + + /// + /// The WM_SIZING message is sent to a window that the user is resizing. By processing this message, an application can monitor the size and position of the drag rectangle and, if needed, change its size or position. + /// + SIZING = 0x0214, + + /// + /// The WM_CAPTURECHANGED message is sent to the window that is losing the mouse capture. + /// + CAPTURECHANGED = 0x0215, + + /// + /// The WM_MOVING message is sent to a window that the user is moving. By processing this message, an application can monitor the position of the drag rectangle and, if needed, change its position. + /// + MOVING = 0x0216, + + /// + /// Notifies applications that a power-management event has occurred. + /// + POWERBROADCAST = 0x0218, + + /// + /// Notifies an application of a change to the hardware configuration of a device or the computer. + /// + DEVICECHANGE = 0x0219, + + /// + /// An application sends the WM_MDICREATE message to a multiple-document interface (MDI) client window to create an MDI child window. + /// + MDICREATE = 0x0220, + + /// + /// An application sends the WM_MDIDESTROY message to a multiple-document interface (MDI) client window to close an MDI child window. + /// + MDIDESTROY = 0x0221, + + /// + /// An application sends the WM_MDIACTIVATE message to a multiple-document interface (MDI) client window to instruct the client window to activate a different MDI child window. + /// + MDIACTIVATE = 0x0222, + + /// + /// An application sends the WM_MDIRESTORE message to a multiple-document interface (MDI) client window to restore an MDI child window from maximized or minimized size. + /// + MDIRESTORE = 0x0223, + + /// + /// An application sends the WM_MDINEXT message to a multiple-document interface (MDI) client window to activate the next or previous child window. + /// + MDINEXT = 0x0224, + + /// + /// An application sends the WM_MDIMAXIMIZE message to a multiple-document interface (MDI) client window to maximize an MDI child window. The system resizes the child window to make its client area fill the client window. The system places the child window's window menu icon in the rightmost position of the frame window's menu bar, and places the child window's restore icon in the leftmost position. The system also appends the title bar FontText of the child window to that of the frame window. + /// + MDIMAXIMIZE = 0x0225, + + /// + /// An application sends the WM_MDITILE message to a multiple-document interface (MDI) client window to arrange all of its MDI child windows in a tile format. + /// + MDITILE = 0x0226, + + /// + /// An application sends the WM_MDICASCADE message to a multiple-document interface (MDI) client window to arrange all its child windows in a cascade format. + /// + MDICASCADE = 0x0227, + + /// + /// An application sends the WM_MDIICONARRANGE message to a multiple-document interface (MDI) client window to arrange all minimized MDI child windows. It does not affect child windows that are not minimized. + /// + MDIICONARRANGE = 0x0228, + + /// + /// An application sends the WM_MDIGETACTIVE message to a multiple-document interface (MDI) client window to retrieve the handle to the active MDI child window. + /// + MDIGETACTIVE = 0x0229, + + /// + /// An application sends the WM_MDISETMENU message to a multiple-document interface (MDI) client window to replace the entire menu of an MDI frame window, to replace the window menu of the frame window, or both. + /// + MDISETMENU = 0x0230, + + /// + /// The WM_ENTERSIZEMOVE message is sent one time to a window after it enters the moving or sizing modal loop. The window enters the moving or sizing modal loop when the user clicks the window's title bar or sizing border, or when the window passes the WM_SYSCOMMAND message to the DefWindowProc function and the wParam parameter of the message specifies the SC_MOVE or SC_SIZE value. The operation is complete when DefWindowProc returns. + /// The system sends the WM_ENTERSIZEMOVE message regardless of whether the dragging of full windows is enabled. + /// + ENTERSIZEMOVE = 0x0231, + + /// + /// The WM_EXITSIZEMOVE message is sent one time to a window, after it has exited the moving or sizing modal loop. The window enters the moving or sizing modal loop when the user clicks the window's title bar or sizing border, or when the window passes the WM_SYSCOMMAND message to the DefWindowProc function and the wParam parameter of the message specifies the SC_MOVE or SC_SIZE value. The operation is complete when DefWindowProc returns. + /// + EXITSIZEMOVE = 0x0232, + + /// + /// Sent when the user drops a file on the window of an application that has registered itself as a recipient of dropped files. + /// + DROPFILES = 0x0233, + + /// + /// An application sends the WM_MDIREFRESHMENU message to a multiple-document interface (MDI) client window to refresh the window menu of the MDI frame window. + /// + MDIREFRESHMENU = 0x0234, + + /// + /// Sent to an application when a window is activated. A window receives this message through its WindowProc function. + /// + IME_SETCONTEXT = 0x0281, + + /// + /// Sent to an application to notify it of changes to the IME window. A window receives this message through its WindowProc function. + /// + IME_NOTIFY = 0x0282, + + /// + /// Sent by an application to direct the IME window to carry out the requested command. The application uses this message to control the IME window that it has created. To send this message, the application calls the SendMessage function with the following parameters. + /// + IME_CONTROL = 0x0283, + + /// + /// Sent to an application when the IME window finds no space to extend the area for the composition window. A window receives this message through its WindowProc function. + /// + IME_COMPOSITIONFULL = 0x0284, + + /// + /// Sent to an application when the operating system is about to change the current IME. A window receives this message through its WindowProc function. + /// + IME_SELECT = 0x0285, + + /// + /// Sent to an application when the IME gets a character of the conversion result. A window receives this message through its WindowProc function. + /// + IME_CHAR = 0x0286, + + /// + /// Sent to an application to provide commands and request information. A window receives this message through its WindowProc function. + /// + IME_REQUEST = 0x0288, + + /// + /// Sent to an application by the IME to notify the application of a key press and to keep message order. A window receives this message through its WindowProc function. + /// + IME_KEYDOWN = 0x0290, + + /// + /// Sent to an application by the IME to notify the application of a key release and to keep message order. A window receives this message through its WindowProc function. + /// + IME_KEYUP = 0x0291, + + /// + /// The WM_MOUSEHOVER message is posted to a window when the cursor hovers over the client area of the window for the period of time specified in a prior call to TrackMouseEvent. + /// + MOUSEHOVER = 0x02A1, + + /// + /// The WM_MOUSELEAVE message is posted to a window when the cursor leaves the client area of the window specified in a prior call to TrackMouseEvent. + /// + MOUSELEAVE = 0x02A3, + + /// + /// The WM_NCMOUSEHOVER message is posted to a window when the cursor hovers over the nonclient area of the window for the period of time specified in a prior call to TrackMouseEvent. + /// + NCMOUSEHOVER = 0x02A0, + + /// + /// The WM_NCMOUSELEAVE message is posted to a window when the cursor leaves the nonclient area of the window specified in a prior call to TrackMouseEvent. + /// + NCMOUSELEAVE = 0x02A2, + + /// + /// The WM_WTSSESSION_CHANGE message notifies applications of changes in session state. + /// + WTSSESSION_CHANGE = 0x02B1, + TABLET_FIRST = 0x02c0, + TABLET_LAST = 0x02df, + + /// + /// An application sends a WM_CUT message to an edit control or combo box to delete (cut) the current selection, if any, in the edit control and copy the deleted FontText to the clipboard in CF_TEXT format. + /// + CUT = 0x0300, + + /// + /// An application sends the WM_COPY message to an edit control or combo box to copy the current selection to the clipboard in CF_TEXT format. + /// + COPY = 0x0301, + + /// + /// An application sends a WM_PASTE message to an edit control or combo box to copy the current content of the clipboard to the edit control at the current caret position. Data is inserted only if the clipboard contains data in CF_TEXT format. + /// + PASTE = 0x0302, + + /// + /// An application sends a WM_CLEAR message to an edit control or combo box to delete (clear) the current selection, if any, from the edit control. + /// + CLEAR = 0x0303, + + /// + /// An application sends a WM_UNDO message to an edit control to undo the last operation. When this message is sent to an edit control, the previously deleted FontText is restored or the previously added FontText is deleted. + /// + UNDO = 0x0304, + + /// + /// The WM_RENDERFORMAT message is sent to the clipboard owner if it has delayed rendering a specific clipboard format and if an application has requested data in that format. The clipboard owner must render data in the specified format and place it on the clipboard by calling the SetClipboardData function. + /// + RENDERFORMAT = 0x0305, + + /// + /// The WM_RENDERALLFORMATS message is sent to the clipboard owner before it is destroyed, if the clipboard owner has delayed rendering one or more clipboard formats. For the content of the clipboard to remain available to other applications, the clipboard owner must render data in all the formats it is capable of generating, and place the data on the clipboard by calling the SetClipboardData function. + /// + RENDERALLFORMATS = 0x0306, + + /// + /// The WM_DESTROYCLIPBOARD message is sent to the clipboard owner when a call to the EmptyClipboard function empties the clipboard. + /// + DESTROYCLIPBOARD = 0x0307, + + /// + /// The WM_DRAWCLIPBOARD message is sent to the first window in the clipboard viewer chain when the content of the clipboard changes. This enables a clipboard viewer window to display the new content of the clipboard. + /// + DRAWCLIPBOARD = 0x0308, + + /// + /// The WM_PAINTCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and the clipboard viewer's client area needs repainting. + /// + PAINTCLIPBOARD = 0x0309, + + /// + /// The WM_VSCROLLCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and an event occurs in the clipboard viewer's vertical scroll bar. The owner should scroll the clipboard image and update the scroll bar values. + /// + VSCROLLCLIPBOARD = 0x030A, + + /// + /// The WM_SIZECLIPBOARD message is sent to the clipboard owner by a clipboard viewer window when the clipboard contains data in the CF_OWNERDISPLAY format and the clipboard viewer's client area has changed size. + /// + SIZECLIPBOARD = 0x030B, + + /// + /// The WM_ASKCBFORMATNAME message is sent to the clipboard owner by a clipboard viewer window to request the name of a CF_OWNERDISPLAY clipboard format. + /// + ASKCBFORMATNAME = 0x030C, + + /// + /// The WM_CHANGECBCHAIN message is sent to the first window in the clipboard viewer chain when a window is being removed from the chain. + /// + CHANGECBCHAIN = 0x030D, + + /// + /// The WM_HSCROLLCLIPBOARD message is sent to the clipboard owner by a clipboard viewer window. This occurs when the clipboard contains data in the CF_OWNERDISPLAY format and an event occurs in the clipboard viewer's horizontal scroll bar. The owner should scroll the clipboard image and update the scroll bar values. + /// + HSCROLLCLIPBOARD = 0x030E, + + /// + /// This message informs a window that it is about to receive the keyboard focus, giving the window the opportunity to realize its logical palette when it receives the focus. + /// + QUERYNEWPALETTE = 0x030F, + + /// + /// The WM_PALETTEISCHANGING message informs applications that an application is going to realize its logical palette. + /// + PALETTEISCHANGING = 0x0310, + + /// + /// This message is sent by the OS to all top-level and overlapped windows after the window with the keyboard focus realizes its logical palette. + /// This message enables windows that do not have the keyboard focus to realize their logical palettes and update their client areas. + /// + PALETTECHANGED = 0x0311, + + /// + /// The WM_HOTKEY message is posted when the user presses a hot key registered by the RegisterHotKey function. The message is placed at the top of the message queue associated with the thread that registered the hot key. + /// + HOTKEY = 0x0312, + + /// + /// The WM_PRINT message is sent to a window to request that it draw itself in the specified device context, most commonly in a printer device context. + /// + PRINT = 0x0317, + + /// + /// The WM_PRINTCLIENT message is sent to a window to request that it draw its client area in the specified device context, most commonly in a printer device context. + /// + PRINTCLIENT = 0x0318, + + /// + /// The WM_APPCOMMAND message notifies a window that the user generated an application command event, for example, by clicking an application command button using the mouse or typing an application command key on the keyboard. + /// + APPCOMMAND = 0x0319, + + /// + /// The WM_THEMECHANGED message is broadcast to every window following a theme change event. Examples of theme change events are the activation of a theme, the deactivation of a theme, or a transition from one theme to another. + /// + THEMECHANGED = 0x031A, + + /// + /// Sent when the contents of the clipboard have changed. + /// + CLIPBOARDUPDATE = 0x031D, + + /// + /// The system will send a window the WM_DWMCOMPOSITIONCHANGED message to indicate that the availability of desktop composition has changed. + /// + DWMCOMPOSITIONCHANGED = 0x031E, + + /// + /// WM_DWMNCRENDERINGCHANGED is called when the non-client area rendering status of a window has changed. Only windows that have set the flag DWM_BLURBEHIND.fTransitionOnMaximized to true will get this message. + /// + DWMNCRENDERINGCHANGED = 0x031F, + + /// + /// Sent to all top-level windows when the colorization color has changed. + /// + DWMCOLORIZATIONCOLORCHANGED = 0x0320, + + /// + /// WM_DWMWINDOWMAXIMIZEDCHANGE will let you know when a DWM composed window is maximized. You also have to register for this message as well. You'd have other windowd go opaque when this message is sent. + /// + DWMWINDOWMAXIMIZEDCHANGE = 0x0321, + + /// + /// Sent to request extended title bar information. A window receives this message through its WindowProc function. + /// + GETTITLEBARINFOEX = 0x033F, + HANDHELDFIRST = 0x0358, + HANDHELDLAST = 0x035F, + AFXFIRST = 0x0360, + AFXLAST = 0x037F, + PENWINFIRST = 0x0380, + PENWINLAST = 0x038F, + + /// + /// The WM_APP constant is used by applications to help define private messages, usually of the form WM_APP+X, where X is an integer value. + /// + APP = 0x8000, + + /// + /// The WM_USER constant is used by applications to help define private messages for use by private window classes, usually of the form WM_USER+X, where X is an integer value. + /// + USER = 0x0400, + + /// + /// An application sends the WM_CPL_LAUNCH message to Windows Control Panel to request that a Control Panel application be started. + /// + CPL_LAUNCH = USER + 0x1000, + + /// + /// The WM_CPL_LAUNCHED message is sent when a Control Panel application, started by the WM_CPL_LAUNCH message, has closed. The WM_CPL_LAUNCHED message is sent to the window identified by the wParam parameter of the WM_CPL_LAUNCH message that started the application. + /// + CPL_LAUNCHED = USER + 0x1001, + + /// + /// WM_SYSTIMER is a well-known yet still undocumented message. Windows uses WM_SYSTIMER for public actions like scrolling. + /// + SYSTIMER = 0x118, + + /// + /// The accessibility state has changed. + /// + HSHELL_ACCESSIBILITYSTATE = 11, + + /// + /// The shell should activate its main window. + /// + HSHELL_ACTIVATESHELLWINDOW = 3, + + /// + /// The user completed an input event (for example, pressed an application command button on the mouse or an application command key on the keyboard), and the application did not handle the WM_APPCOMMAND message generated by that input. + /// If the Shell procedure handles the WM_COMMAND message, it should not call CallNextHookEx. See the Return Value section for more information. + /// + HSHELL_APPCOMMAND = 12, + + /// + /// A window is being minimized or maximized. The system needs the coordinates of the minimized rectangle for the window. + /// + HSHELL_GETMINRECT = 5, + + /// + /// Keyboard language was changed or a new keyboard layout was loaded. + /// + HSHELL_LANGUAGE = 8, + + /// + /// The title of a window in the task bar has been redrawn. + /// + HSHELL_REDRAW = 6, + + /// + /// The user has selected the task list. A shell application that provides a task list should return TRUE to prevent Windows from starting its task list. + /// + HSHELL_TASKMAN = 7, + + /// + /// A top-level, unowned window has been created. The window exists when the system calls this hook. + /// + HSHELL_WINDOWCREATED = 1, + + /// + /// A top-level, unowned window is about to be destroyed. The window still exists when the system calls this hook. + /// + HSHELL_WINDOWDESTROYED = 2, + + /// + /// The activation has changed to a different top-level, unowned window. + /// + HSHELL_WINDOWACTIVATED = 4, + + /// + /// A top-level window is being replaced. The window exists when the system calls this hook. + /// + HSHELL_WINDOWREPLACED = 13 + } +} diff --git a/osu.Framework/Platform/Windows/TimePeriod.cs b/osu.Framework/Platform/Windows/TimePeriod.cs index 11ed0110d..56778254e 100644 --- a/osu.Framework/Platform/Windows/TimePeriod.cs +++ b/osu.Framework/Platform/Windows/TimePeriod.cs @@ -1,102 +1,102 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using OpenTK; - -namespace osu.Framework.Platform.Windows -{ - /// - /// Set the windows multimedia timer to a specific accuracy. - /// - internal class TimePeriod : IDisposable - { - private static TimeCaps timeCapabilities; - private readonly int period; - - [DllImport(@"winmm.dll", ExactSpelling = true)] - private static extern int timeGetDevCaps(ref TimeCaps ptc, int cbtc); - - [DllImport(@"winmm.dll", ExactSpelling = true)] - private static extern int timeBeginPeriod(int uPeriod); - - [DllImport(@"winmm.dll", ExactSpelling = true)] - private static extern int timeEndPeriod(int uPeriod); - - internal static int MinimumPeriod => timeCapabilities.wPeriodMin; - internal static int MaximumPeriod => timeCapabilities.wPeriodMax; - - private bool canAdjust = MaximumPeriod > 0; - - static TimePeriod() - { - timeGetDevCaps(ref timeCapabilities, Marshal.SizeOf(typeof(TimeCaps))); - } - - internal TimePeriod(int period) - { - this.period = period; - } - - private bool active; - - internal bool Active - { - get { return active; } - set - { - if (value == active || !canAdjust) return; - active = value; - - try - { - if (active) - { - canAdjust &= 0 == timeBeginPeriod(MathHelper.Clamp(period, MinimumPeriod, MaximumPeriod)); - } - else - { - timeEndPeriod(period); - } - } - catch - { - } - } - } - - #region IDisposable Support - - private bool disposedValue; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - Active = false; - disposedValue = true; - } - } - - ~TimePeriod() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - [StructLayout(LayoutKind.Sequential)] - private struct TimeCaps - { - internal readonly int wPeriodMin; - internal readonly int wPeriodMax; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using OpenTK; + +namespace osu.Framework.Platform.Windows +{ + /// + /// Set the windows multimedia timer to a specific accuracy. + /// + internal class TimePeriod : IDisposable + { + private static TimeCaps timeCapabilities; + private readonly int period; + + [DllImport(@"winmm.dll", ExactSpelling = true)] + private static extern int timeGetDevCaps(ref TimeCaps ptc, int cbtc); + + [DllImport(@"winmm.dll", ExactSpelling = true)] + private static extern int timeBeginPeriod(int uPeriod); + + [DllImport(@"winmm.dll", ExactSpelling = true)] + private static extern int timeEndPeriod(int uPeriod); + + internal static int MinimumPeriod => timeCapabilities.wPeriodMin; + internal static int MaximumPeriod => timeCapabilities.wPeriodMax; + + private bool canAdjust = MaximumPeriod > 0; + + static TimePeriod() + { + timeGetDevCaps(ref timeCapabilities, Marshal.SizeOf(typeof(TimeCaps))); + } + + internal TimePeriod(int period) + { + this.period = period; + } + + private bool active; + + internal bool Active + { + get { return active; } + set + { + if (value == active || !canAdjust) return; + active = value; + + try + { + if (active) + { + canAdjust &= 0 == timeBeginPeriod(MathHelper.Clamp(period, MinimumPeriod, MaximumPeriod)); + } + else + { + timeEndPeriod(period); + } + } + catch + { + } + } + } + + #region IDisposable Support + + private bool disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + Active = false; + disposedValue = true; + } + } + + ~TimePeriod() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + [StructLayout(LayoutKind.Sequential)] + private struct TimeCaps + { + internal readonly int wPeriodMin; + internal readonly int wPeriodMax; + } + } +} diff --git a/osu.Framework/Platform/Windows/WindowsClipboard.cs b/osu.Framework/Platform/Windows/WindowsClipboard.cs index 40c5b6e71..fa58a54b7 100644 --- a/osu.Framework/Platform/Windows/WindowsClipboard.cs +++ b/osu.Framework/Platform/Windows/WindowsClipboard.cs @@ -1,152 +1,152 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace osu.Framework.Platform.Windows -{ - public class WindowsClipboard : Clipboard - { - [DllImport("User32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool IsClipboardFormatAvailable(uint format); - - [DllImport("User32.dll")] - private static extern IntPtr GetClipboardData(uint uFormat); - - [DllImport("user32.dll")] - private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem); - - [DllImport("User32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool OpenClipboard(IntPtr hWndNewOwner); - - [DllImport("user32.dll")] - private static extern bool EmptyClipboard(); - - [DllImport("User32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CloseClipboard(); - - [DllImport("Kernel32.dll")] - private static extern IntPtr GlobalLock(IntPtr hMem); - - [DllImport("kernel32.dll")] - private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); - - [DllImport("Kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool GlobalUnlock(IntPtr hMem); - - [DllImport("Kernel32.dll")] - private static extern int GlobalSize(IntPtr hMem); - - [DllImport("kernel32.dll")] - private static extern IntPtr GlobalFree(IntPtr hMem); - - [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] - public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); - - private const uint cf_unicodetext = 13U; - - public override string GetText() - { - if (!IsClipboardFormatAvailable(cf_unicodetext)) - return null; - - try - { - if (!OpenClipboard(IntPtr.Zero)) - return null; - - IntPtr handle = GetClipboardData(cf_unicodetext); - if (handle == IntPtr.Zero) - return null; - - IntPtr pointer = IntPtr.Zero; - - try - { - pointer = GlobalLock(handle); - - if (pointer == IntPtr.Zero) - return null; - - int size = GlobalSize(handle); - byte[] buff = new byte[size]; - - Marshal.Copy(pointer, buff, 0, size); - - return Encoding.Unicode.GetString(buff).TrimEnd('\0'); - } - finally - { - if (pointer != IntPtr.Zero) - GlobalUnlock(handle); - } - } - finally - { - CloseClipboard(); - } - } - - public override void SetText(string selectedText) - { - try - { - if (!OpenClipboard(IntPtr.Zero)) - return; - - EmptyClipboard(); - - uint bytes = ((uint)selectedText.Length + 1) * 2; - - var source = Marshal.StringToHGlobalUni(selectedText); - - const int gmem_movable = 0x0002; - const int gmem_zeroinit = 0x0040; - const int ghnd = gmem_movable | gmem_zeroinit; - - // IMPORTANT: SetClipboardData requires memory that was acquired with GlobalAlloc using GMEM_MOVABLE. - var hGlobal = GlobalAlloc(ghnd, (UIntPtr)bytes); - - try - { - var target = GlobalLock(hGlobal); - if (target == IntPtr.Zero) - return; - - try - { - CopyMemory(target, source, bytes); - } - finally - { - if (target != IntPtr.Zero) - GlobalUnlock(target); - - Marshal.FreeHGlobal(source); - } - - if (SetClipboardData(cf_unicodetext, hGlobal).ToInt64() != 0) - { - // IMPORTANT: SetClipboardData takes ownership of hGlobal upon success. - hGlobal = IntPtr.Zero; - } - } - finally - { - if (hGlobal != IntPtr.Zero) - GlobalFree(hGlobal); - } - } - finally - { - CloseClipboard(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace osu.Framework.Platform.Windows +{ + public class WindowsClipboard : Clipboard + { + [DllImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsClipboardFormatAvailable(uint format); + + [DllImport("User32.dll")] + private static extern IntPtr GetClipboardData(uint uFormat); + + [DllImport("user32.dll")] + private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem); + + [DllImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool OpenClipboard(IntPtr hWndNewOwner); + + [DllImport("user32.dll")] + private static extern bool EmptyClipboard(); + + [DllImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseClipboard(); + + [DllImport("Kernel32.dll")] + private static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); + + [DllImport("Kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GlobalUnlock(IntPtr hMem); + + [DllImport("Kernel32.dll")] + private static extern int GlobalSize(IntPtr hMem); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalFree(IntPtr hMem); + + [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] + public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + private const uint cf_unicodetext = 13U; + + public override string GetText() + { + if (!IsClipboardFormatAvailable(cf_unicodetext)) + return null; + + try + { + if (!OpenClipboard(IntPtr.Zero)) + return null; + + IntPtr handle = GetClipboardData(cf_unicodetext); + if (handle == IntPtr.Zero) + return null; + + IntPtr pointer = IntPtr.Zero; + + try + { + pointer = GlobalLock(handle); + + if (pointer == IntPtr.Zero) + return null; + + int size = GlobalSize(handle); + byte[] buff = new byte[size]; + + Marshal.Copy(pointer, buff, 0, size); + + return Encoding.Unicode.GetString(buff).TrimEnd('\0'); + } + finally + { + if (pointer != IntPtr.Zero) + GlobalUnlock(handle); + } + } + finally + { + CloseClipboard(); + } + } + + public override void SetText(string selectedText) + { + try + { + if (!OpenClipboard(IntPtr.Zero)) + return; + + EmptyClipboard(); + + uint bytes = ((uint)selectedText.Length + 1) * 2; + + var source = Marshal.StringToHGlobalUni(selectedText); + + const int gmem_movable = 0x0002; + const int gmem_zeroinit = 0x0040; + const int ghnd = gmem_movable | gmem_zeroinit; + + // IMPORTANT: SetClipboardData requires memory that was acquired with GlobalAlloc using GMEM_MOVABLE. + var hGlobal = GlobalAlloc(ghnd, (UIntPtr)bytes); + + try + { + var target = GlobalLock(hGlobal); + if (target == IntPtr.Zero) + return; + + try + { + CopyMemory(target, source, bytes); + } + finally + { + if (target != IntPtr.Zero) + GlobalUnlock(target); + + Marshal.FreeHGlobal(source); + } + + if (SetClipboardData(cf_unicodetext, hGlobal).ToInt64() != 0) + { + // IMPORTANT: SetClipboardData takes ownership of hGlobal upon success. + hGlobal = IntPtr.Zero; + } + } + finally + { + if (hGlobal != IntPtr.Zero) + GlobalFree(hGlobal); + } + } + finally + { + CloseClipboard(); + } + } + } +} diff --git a/osu.Framework/Platform/Windows/WindowsGameHost.cs b/osu.Framework/Platform/Windows/WindowsGameHost.cs index 0c8062cc2..e500d7147 100644 --- a/osu.Framework/Platform/Windows/WindowsGameHost.cs +++ b/osu.Framework/Platform/Windows/WindowsGameHost.cs @@ -1,63 +1,63 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Platform.Windows.Native; - -namespace osu.Framework.Platform.Windows -{ - public class WindowsGameHost : DesktopGameHost - { - private readonly TimePeriod timePeriod; - - public override Clipboard GetClipboard() => new WindowsClipboard(); - - protected override Storage GetStorage(string baseName) => new WindowsStorage(baseName); - - public override bool CapsLockEnabled => Console.CapsLock; - - internal WindowsGameHost(string gameName, bool bindIPC = false) - : base(gameName, bindIPC) - { - // OnActivate / OnDeactivate may not fire, so the initial activity state may be unknown here. - // In order to be certain we have the correct activity state we are querying the Windows API here. - - timePeriod = new TimePeriod(1) { Active = true }; - - Window = new WindowsGameWindow(); - Window.WindowStateChanged += onWindowOnWindowStateChanged; - } - - private void onWindowOnWindowStateChanged(object sender, EventArgs e) - { - if (Window.WindowState != OpenTK.WindowState.Minimized) - OnActivated(); - else - OnDeactivated(); - } - - protected override void Dispose(bool isDisposing) - { - Window.WindowStateChanged -= onWindowOnWindowStateChanged; - - timePeriod?.Dispose(); - base.Dispose(isDisposing); - } - - protected override void OnActivated() - { - timePeriod.Active = true; - - Execution.SetThreadExecutionState(Execution.ExecutionState.Continuous | Execution.ExecutionState.SystemRequired | Execution.ExecutionState.DisplayRequired); - base.OnActivated(); - } - - protected override void OnDeactivated() - { - timePeriod.Active = false; - - Execution.SetThreadExecutionState(Execution.ExecutionState.Continuous); - base.OnDeactivated(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Platform.Windows.Native; + +namespace osu.Framework.Platform.Windows +{ + public class WindowsGameHost : DesktopGameHost + { + private readonly TimePeriod timePeriod; + + public override Clipboard GetClipboard() => new WindowsClipboard(); + + protected override Storage GetStorage(string baseName) => new WindowsStorage(baseName); + + public override bool CapsLockEnabled => Console.CapsLock; + + internal WindowsGameHost(string gameName, bool bindIPC = false) + : base(gameName, bindIPC) + { + // OnActivate / OnDeactivate may not fire, so the initial activity state may be unknown here. + // In order to be certain we have the correct activity state we are querying the Windows API here. + + timePeriod = new TimePeriod(1) { Active = true }; + + Window = new WindowsGameWindow(); + Window.WindowStateChanged += onWindowOnWindowStateChanged; + } + + private void onWindowOnWindowStateChanged(object sender, EventArgs e) + { + if (Window.WindowState != OpenTK.WindowState.Minimized) + OnActivated(); + else + OnDeactivated(); + } + + protected override void Dispose(bool isDisposing) + { + Window.WindowStateChanged -= onWindowOnWindowStateChanged; + + timePeriod?.Dispose(); + base.Dispose(isDisposing); + } + + protected override void OnActivated() + { + timePeriod.Active = true; + + Execution.SetThreadExecutionState(Execution.ExecutionState.Continuous | Execution.ExecutionState.SystemRequired | Execution.ExecutionState.DisplayRequired); + base.OnActivated(); + } + + protected override void OnDeactivated() + { + timePeriod.Active = false; + + Execution.SetThreadExecutionState(Execution.ExecutionState.Continuous); + base.OnDeactivated(); + } + } +} diff --git a/osu.Framework/Platform/Windows/WindowsGameWindow.cs b/osu.Framework/Platform/Windows/WindowsGameWindow.cs index a9382b00a..308a25442 100644 --- a/osu.Framework/Platform/Windows/WindowsGameWindow.cs +++ b/osu.Framework/Platform/Windows/WindowsGameWindow.cs @@ -1,51 +1,51 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Drawing; -using System.IO; -using System.Runtime.InteropServices; -using OpenTK.Input; - -namespace osu.Framework.Platform.Windows -{ - internal class WindowsGameWindow : DesktopGameWindow - { - private const int seticon_message = 0x0080; - - private Icon smallIcon; - private Icon largeIcon; - - protected override void OnKeyDown(object sender, KeyboardKeyEventArgs e) - { - if (e.Key == Key.F4 && e.Alt) - { - Implementation.Exit(); - return; - } - - base.OnKeyDown(sender, e); - } - - public override void SetIconFromStream(Stream stream) - { - if (WindowInfo.Handle == IntPtr.Zero) - throw new InvalidOperationException("Window must be created before an icon can be set."); - - var secondStream = new MemoryStream(); - stream.CopyTo(secondStream); - - stream.Position = 0; - secondStream.Position = 0; - - smallIcon = new Icon(stream, 24, 24); - largeIcon = new Icon(secondStream, 256, 256); - - SendMessage(WindowInfo.Handle, seticon_message, (IntPtr)0, smallIcon.Handle); - SendMessage(WindowInfo.Handle, seticon_message, (IntPtr)1, largeIcon.Handle); - } - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Drawing; +using System.IO; +using System.Runtime.InteropServices; +using OpenTK.Input; + +namespace osu.Framework.Platform.Windows +{ + internal class WindowsGameWindow : DesktopGameWindow + { + private const int seticon_message = 0x0080; + + private Icon smallIcon; + private Icon largeIcon; + + protected override void OnKeyDown(object sender, KeyboardKeyEventArgs e) + { + if (e.Key == Key.F4 && e.Alt) + { + Implementation.Exit(); + return; + } + + base.OnKeyDown(sender, e); + } + + public override void SetIconFromStream(Stream stream) + { + if (WindowInfo.Handle == IntPtr.Zero) + throw new InvalidOperationException("Window must be created before an icon can be set."); + + var secondStream = new MemoryStream(); + stream.CopyTo(secondStream); + + stream.Position = 0; + secondStream.Position = 0; + + smallIcon = new Icon(stream, 24, 24); + largeIcon = new Icon(secondStream, 256, 256); + + SendMessage(WindowInfo.Handle, seticon_message, (IntPtr)0, smallIcon.Handle); + SendMessage(WindowInfo.Handle, seticon_message, (IntPtr)1, largeIcon.Handle); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/osu.Framework/Platform/Windows/WindowsStorage.cs b/osu.Framework/Platform/Windows/WindowsStorage.cs index 528e32695..487e454ed 100644 --- a/osu.Framework/Platform/Windows/WindowsStorage.cs +++ b/osu.Framework/Platform/Windows/WindowsStorage.cs @@ -1,17 +1,17 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Platform.Windows -{ - public class WindowsStorage : DesktopStorage - { - public WindowsStorage(string baseName) - : base(baseName) - { - } - - protected override string LocateBasePath() => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Platform.Windows +{ + public class WindowsStorage : DesktopStorage + { + public WindowsStorage(string baseName) + : base(baseName) + { + } + + protected override string LocateBasePath() => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } +} diff --git a/osu.Framework/Properties/AssemblyInfo.cs b/osu.Framework/Properties/AssemblyInfo.cs index 143bd0f59..d17cc2f2d 100644 --- a/osu.Framework/Properties/AssemblyInfo.cs +++ b/osu.Framework/Properties/AssemblyInfo.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Runtime.CompilerServices; - -// We publish our internal attributes to other sub-projects of the framework. -// Note, that we omit visual tests as they are meant to test the framework -// behavior "in the wild". - -[assembly: InternalsVisibleTo("osu.Framework.Tests")] -[assembly: InternalsVisibleTo("osu.Framework.Tests.Dynamic")] +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Runtime.CompilerServices; + +// We publish our internal attributes to other sub-projects of the framework. +// Note, that we omit visual tests as they are meant to test the framework +// behavior "in the wild". + +[assembly: InternalsVisibleTo("osu.Framework.Tests")] +[assembly: InternalsVisibleTo("osu.Framework.Tests.Dynamic")] diff --git a/osu.Framework/RuntimeInfo.cs b/osu.Framework/RuntimeInfo.cs index 385e41ae5..0de6d7ab3 100644 --- a/osu.Framework/RuntimeInfo.cs +++ b/osu.Framework/RuntimeInfo.cs @@ -1,66 +1,66 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Runtime.InteropServices; - -namespace osu.Framework -{ - public static class RuntimeInfo - { - [DllImport(@"kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] - internal static extern IntPtr GetProcAddress(IntPtr hModule, string procName); - - [DllImport(@"kernel32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr GetModuleHandle(string lpModuleName); - - /// - /// Returns the absolute path of osu.Framework.dll. - /// - public static string GetFrameworkAssemblyPath() => - System.Reflection.Assembly.GetAssembly(typeof(RuntimeInfo)).Location; - - public static bool Is32Bit { get; } - public static bool Is64Bit { get; } - public static bool IsMono { get; } - public static Platform OS { get; } - public static bool IsUnix => OS == Platform.Linux || OS == Platform.MacOsx; - public static bool IsWine { get; } - - static RuntimeInfo() - { - IsMono = Type.GetType("Mono.Runtime") != null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - OS = Platform.Windows; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - OS = OS == 0 ? Platform.MacOsx : throw new InvalidOperationException($"Tried to set OS Platform to {nameof(Platform.MacOsx)}, but is already {Enum.GetName(typeof(Platform), OS)}"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - OS = OS == 0 ? Platform.Linux : throw new InvalidOperationException($"Tried to set OS Platform to {nameof(Platform.Linux)}, but is already {Enum.GetName(typeof(Platform), OS)}"); - - if (OS == 0) - throw new PlatformNotSupportedException("Operating system could not be detected correctly."); - - Is32Bit = IntPtr.Size == 4; - Is64Bit = IntPtr.Size == 8; - - if (OS == Platform.Windows) - { - IntPtr hModule = GetModuleHandle(@"ntdll.dll"); - if (hModule == IntPtr.Zero) - IsWine = false; - else - { - IntPtr fptr = GetProcAddress(hModule, @"wine_get_version"); - IsWine = fptr != IntPtr.Zero; - } - } - } - - public enum Platform - { - Windows = 1, - Linux = 2, - MacOsx = 3, - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Runtime.InteropServices; + +namespace osu.Framework +{ + public static class RuntimeInfo + { + [DllImport(@"kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + internal static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport(@"kernel32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + /// + /// Returns the absolute path of osu.Framework.dll. + /// + public static string GetFrameworkAssemblyPath() => + System.Reflection.Assembly.GetAssembly(typeof(RuntimeInfo)).Location; + + public static bool Is32Bit { get; } + public static bool Is64Bit { get; } + public static bool IsMono { get; } + public static Platform OS { get; } + public static bool IsUnix => OS == Platform.Linux || OS == Platform.MacOsx; + public static bool IsWine { get; } + + static RuntimeInfo() + { + IsMono = Type.GetType("Mono.Runtime") != null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + OS = Platform.Windows; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + OS = OS == 0 ? Platform.MacOsx : throw new InvalidOperationException($"Tried to set OS Platform to {nameof(Platform.MacOsx)}, but is already {Enum.GetName(typeof(Platform), OS)}"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + OS = OS == 0 ? Platform.Linux : throw new InvalidOperationException($"Tried to set OS Platform to {nameof(Platform.Linux)}, but is already {Enum.GetName(typeof(Platform), OS)}"); + + if (OS == 0) + throw new PlatformNotSupportedException("Operating system could not be detected correctly."); + + Is32Bit = IntPtr.Size == 4; + Is64Bit = IntPtr.Size == 8; + + if (OS == Platform.Windows) + { + IntPtr hModule = GetModuleHandle(@"ntdll.dll"); + if (hModule == IntPtr.Zero) + IsWine = false; + else + { + IntPtr fptr = GetProcAddress(hModule, @"wine_get_version"); + IsWine = fptr != IntPtr.Zero; + } + } + } + + public enum Platform + { + Windows = 1, + Linux = 2, + MacOsx = 3, + } + } +} diff --git a/osu.Framework/Screens/Screen.cs b/osu.Framework/Screens/Screen.cs index 77311afc4..a72efd40e 100644 --- a/osu.Framework/Screens/Screen.cs +++ b/osu.Framework/Screens/Screen.cs @@ -1,249 +1,249 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Screens -{ - public class Screen : Container - { - protected Screen ParentScreen; - public Screen ChildScreen; - - public bool IsCurrentScreen => !hasExited && hasEntered && ChildScreen == null; - - private readonly Container content; - private Container childModeContainer; - - protected Game Game; - - protected override Container Content => content; - - public event Action ModePushed; - - public event Action Exited; - - private bool hasExited; - private bool hasEntered; - - /// - /// Make this Screen directly exited when resuming from a child. - /// - public bool ValidForResume = true; - - public Screen() - { - RelativeSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - AddRangeInternal(new[] - { - content = new ContentContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - }); - } - - public override void Add(Drawable drawable) - { - if (drawable is Screen) - throw new InvalidOperationException("Use Push to add nested Screens."); - base.Add(drawable); - } - - public override bool DisposeOnDeathRemoval => true; - - // in the case we don't have a parent screen, we still want to handle input as we are also responsible for - // children inside childScreenContainer. - // this means the root screen always received input. - public override bool HandleKeyboardInput => IsCurrentScreen || !hasExited && ParentScreen == null; - public override bool HandleMouseInput => IsCurrentScreen || !hasExited && ParentScreen == null; - - /// - /// Called when this Screen is being entered. Only happens once, ever. - /// - /// The next Screen. - protected virtual void OnEntering(Screen last) - { - } - - /// - /// Called when this Screen is exiting. Only happens once, ever. - /// - /// The next Screen. - /// Return true to cancel the exit process. - protected virtual bool OnExiting(Screen next) => false; - - /// - /// Called when this Screen is being returned to from a child exiting. - /// - /// The next Screen. - protected virtual void OnResuming(Screen last) - { - } - - /// - /// Called when this Screen is being left to a new child. - /// - /// The new Screen - protected virtual void OnSuspending(Screen next) - { - } - - [BackgroundDependencyLoader] - private void load(Game game) - { - Game = game; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - //for the case where we are at the top of the mode stack, we still want to run our OnEntering method. - if (ParentScreen == null) - { - enter(null); - - AddInternal(childModeContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }); - } - else - { - childModeContainer = ParentScreen.childModeContainer; - } - } - - /// - /// Changes to a new Screen. - /// - /// The new Screen. - public virtual bool Push(Screen screen) - { - if (hasExited) - throw new InvalidOperationException("Cannot push to an already exited screen."); - - if (!IsCurrentScreen) - throw new InvalidOperationException("Cannot push a child screen to a non-current screen."); - - if (ChildScreen != null) - throw new InvalidOperationException("Can not push more than one child screen."); - - screen.ParentScreen = this; - childModeContainer.Add(screen); - - if (screen.hasExited) - { - screen.Expire(); - return false; - } - - startSuspend(screen); - - screen.enter(this); - - ModePushed?.Invoke(screen); - - Content.Expire(); - - return true; - } - - private void startSuspend(Screen next) - { - OnSuspending(next); - Content.Expire(); - - ChildScreen = next; - } - - /// - /// Exits this Screen. - /// - public void Exit() => ExitFrom(this); - - private void enter(Screen source) - { - hasEntered = true; - OnEntering(source); - } - - /// - /// Exits this Screen. - /// - /// Provides an exit source (used when skipping no-longer-valid modes upwards in stack). - protected void ExitFrom(Screen source) - { - if (hasExited) - return; - - if (OnExiting(ParentScreen)) - return; - - hasExited = true; - - if (ValidForResume || source == this) - Content.Expire(); - - //propagate down the LifetimeEnd from the exit source. - LifetimeEnd = source.Content.LifetimeEnd; - - Exited?.Invoke(ParentScreen); - ParentScreen?.startResume(source); - ParentScreen = null; - - Exited = null; - ModePushed = null; - } - - private void startResume(Screen source) - { - ChildScreen = null; - - if (ValidForResume) - { - OnResuming(source); - Content.LifetimeEnd = double.MaxValue; - } - else - { - ExitFrom(source); - } - } - - - public void MakeCurrent() - { - if (IsCurrentScreen) return; - - Screen c; - for (c = ChildScreen; c.ChildScreen != null; c = c.ChildScreen) - c.ValidForResume = false; - - //all the expired ones will exit - c.Exit(); - } - - protected class ContentContainer : Container - { - public override bool HandleKeyboardInput => LifetimeEnd == double.MaxValue; - public override bool HandleMouseInput => LifetimeEnd == double.MaxValue; - public override bool RemoveWhenNotAlive => false; - - public ContentContainer() - { - RelativeSizeAxes = Axes.Both; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Screens +{ + public class Screen : Container + { + protected Screen ParentScreen; + public Screen ChildScreen; + + public bool IsCurrentScreen => !hasExited && hasEntered && ChildScreen == null; + + private readonly Container content; + private Container childModeContainer; + + protected Game Game; + + protected override Container Content => content; + + public event Action ModePushed; + + public event Action Exited; + + private bool hasExited; + private bool hasEntered; + + /// + /// Make this Screen directly exited when resuming from a child. + /// + public bool ValidForResume = true; + + public Screen() + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + AddRangeInternal(new[] + { + content = new ContentContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + }); + } + + public override void Add(Drawable drawable) + { + if (drawable is Screen) + throw new InvalidOperationException("Use Push to add nested Screens."); + base.Add(drawable); + } + + public override bool DisposeOnDeathRemoval => true; + + // in the case we don't have a parent screen, we still want to handle input as we are also responsible for + // children inside childScreenContainer. + // this means the root screen always received input. + public override bool HandleKeyboardInput => IsCurrentScreen || !hasExited && ParentScreen == null; + public override bool HandleMouseInput => IsCurrentScreen || !hasExited && ParentScreen == null; + + /// + /// Called when this Screen is being entered. Only happens once, ever. + /// + /// The next Screen. + protected virtual void OnEntering(Screen last) + { + } + + /// + /// Called when this Screen is exiting. Only happens once, ever. + /// + /// The next Screen. + /// Return true to cancel the exit process. + protected virtual bool OnExiting(Screen next) => false; + + /// + /// Called when this Screen is being returned to from a child exiting. + /// + /// The next Screen. + protected virtual void OnResuming(Screen last) + { + } + + /// + /// Called when this Screen is being left to a new child. + /// + /// The new Screen + protected virtual void OnSuspending(Screen next) + { + } + + [BackgroundDependencyLoader] + private void load(Game game) + { + Game = game; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + //for the case where we are at the top of the mode stack, we still want to run our OnEntering method. + if (ParentScreen == null) + { + enter(null); + + AddInternal(childModeContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }); + } + else + { + childModeContainer = ParentScreen.childModeContainer; + } + } + + /// + /// Changes to a new Screen. + /// + /// The new Screen. + public virtual bool Push(Screen screen) + { + if (hasExited) + throw new InvalidOperationException("Cannot push to an already exited screen."); + + if (!IsCurrentScreen) + throw new InvalidOperationException("Cannot push a child screen to a non-current screen."); + + if (ChildScreen != null) + throw new InvalidOperationException("Can not push more than one child screen."); + + screen.ParentScreen = this; + childModeContainer.Add(screen); + + if (screen.hasExited) + { + screen.Expire(); + return false; + } + + startSuspend(screen); + + screen.enter(this); + + ModePushed?.Invoke(screen); + + Content.Expire(); + + return true; + } + + private void startSuspend(Screen next) + { + OnSuspending(next); + Content.Expire(); + + ChildScreen = next; + } + + /// + /// Exits this Screen. + /// + public void Exit() => ExitFrom(this); + + private void enter(Screen source) + { + hasEntered = true; + OnEntering(source); + } + + /// + /// Exits this Screen. + /// + /// Provides an exit source (used when skipping no-longer-valid modes upwards in stack). + protected void ExitFrom(Screen source) + { + if (hasExited) + return; + + if (OnExiting(ParentScreen)) + return; + + hasExited = true; + + if (ValidForResume || source == this) + Content.Expire(); + + //propagate down the LifetimeEnd from the exit source. + LifetimeEnd = source.Content.LifetimeEnd; + + Exited?.Invoke(ParentScreen); + ParentScreen?.startResume(source); + ParentScreen = null; + + Exited = null; + ModePushed = null; + } + + private void startResume(Screen source) + { + ChildScreen = null; + + if (ValidForResume) + { + OnResuming(source); + Content.LifetimeEnd = double.MaxValue; + } + else + { + ExitFrom(source); + } + } + + + public void MakeCurrent() + { + if (IsCurrentScreen) return; + + Screen c; + for (c = ChildScreen; c.ChildScreen != null; c = c.ChildScreen) + c.ValidForResume = false; + + //all the expired ones will exit + c.Exit(); + } + + protected class ContentContainer : Container + { + public override bool HandleKeyboardInput => LifetimeEnd == double.MaxValue; + public override bool HandleMouseInput => LifetimeEnd == double.MaxValue; + public override bool RemoveWhenNotAlive => false; + + public ContentContainer() + { + RelativeSizeAxes = Axes.Both; + } + } + } +} diff --git a/osu.Framework/Statistics/BackgroundStackTraceCollector.cs b/osu.Framework/Statistics/BackgroundStackTraceCollector.cs index 90073fe78..26b773169 100644 --- a/osu.Framework/Statistics/BackgroundStackTraceCollector.cs +++ b/osu.Framework/Statistics/BackgroundStackTraceCollector.cs @@ -1,134 +1,134 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Logging; -using osu.Framework.Timing; -using System.Diagnostics; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Diagnostics.Runtime; - -namespace osu.Framework.Statistics -{ - /// - /// Spwan a thread to collect real-time stack traces of the targeted thread. - /// - internal class BackgroundStackTraceCollector : IDisposable - { - private IList backgroundMonitorStackTrace; - - private readonly StopwatchClock clock; - - private readonly Logger logger; - private readonly Thread targetThread; - - internal double LastConsumptionTime; - - private double spikeRecordThreshold; - - private readonly CancellationTokenSource cancellationToken; - - public bool Enabled = true; - - public BackgroundStackTraceCollector(Thread targetThread, StopwatchClock clock) - { - if (Debugger.IsAttached) return; - - logger = Logger.GetLogger($"performance-{targetThread.Name?.ToLower() ?? "unknown"}"); - logger.OutputToListeners = false; - - this.clock = clock; - this.targetThread = targetThread; - - Task.Factory.StartNew(() => - { - while (!cancellationToken.IsCancellationRequested) - { - if (Enabled && targetThread.IsAlive && clock.ElapsedMilliseconds - LastConsumptionTime > spikeRecordThreshold / 2 && backgroundMonitorStackTrace == null) - backgroundMonitorStackTrace = getStackTrace(targetThread); - - Thread.Sleep(1); - } - }, (cancellationToken = new CancellationTokenSource()).Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - - internal void NewFrame(double elapsedFrameTime, double newSpikeThreshold) - { - if (targetThread == null) return; - - var frames = backgroundMonitorStackTrace; - backgroundMonitorStackTrace = null; - - var currentThreshold = spikeRecordThreshold; - - spikeRecordThreshold = newSpikeThreshold; - - if (!Enabled || elapsedFrameTime < currentThreshold || currentThreshold == 0) - return; - - StringBuilder logMessage = new StringBuilder(); - - logMessage.AppendLine($@"| Slow frame on thread ""{targetThread.Name}"""); - logMessage.AppendLine(@"|"); - logMessage.AppendLine($@"| * Thread time : {clock.CurrentTime:#0,#}ms"); - logMessage.AppendLine($@"| * Frame length : {elapsedFrameTime:#0,#}ms (allowable: {currentThreshold:#0,#}ms)"); - - logMessage.AppendLine(@"|"); - - if (frames != null) - { - logMessage.AppendLine(@"| Stack trace:"); - - foreach (var f in frames) - logMessage.AppendLine($@"|- {f.DisplayString}"); - } - else - logMessage.AppendLine(@"| Call stack was not recorded."); - - logger.Add(logMessage.ToString()); - } - - private static readonly Lazy clr_info = new Lazy(delegate - { - try - { - return DataTarget.AttachToProcess(Process.GetCurrentProcess().Id, 200, AttachFlag.Passive).ClrVersions[0]; - } - catch - { - return null; - } - }); - - private static IList getStackTrace(Thread targetThread) => clr_info.Value?.CreateRuntime().Threads.FirstOrDefault(t => t.ManagedThreadId == targetThread.ManagedThreadId)?.StackTrace; - - #region IDisposable Support - - ~BackgroundStackTraceCollector() - { - Dispose(false); - } - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (!isDisposed) - { - isDisposed = true; - cancellationToken?.Cancel(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Logging; +using osu.Framework.Timing; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.Runtime; + +namespace osu.Framework.Statistics +{ + /// + /// Spwan a thread to collect real-time stack traces of the targeted thread. + /// + internal class BackgroundStackTraceCollector : IDisposable + { + private IList backgroundMonitorStackTrace; + + private readonly StopwatchClock clock; + + private readonly Logger logger; + private readonly Thread targetThread; + + internal double LastConsumptionTime; + + private double spikeRecordThreshold; + + private readonly CancellationTokenSource cancellationToken; + + public bool Enabled = true; + + public BackgroundStackTraceCollector(Thread targetThread, StopwatchClock clock) + { + if (Debugger.IsAttached) return; + + logger = Logger.GetLogger($"performance-{targetThread.Name?.ToLower() ?? "unknown"}"); + logger.OutputToListeners = false; + + this.clock = clock; + this.targetThread = targetThread; + + Task.Factory.StartNew(() => + { + while (!cancellationToken.IsCancellationRequested) + { + if (Enabled && targetThread.IsAlive && clock.ElapsedMilliseconds - LastConsumptionTime > spikeRecordThreshold / 2 && backgroundMonitorStackTrace == null) + backgroundMonitorStackTrace = getStackTrace(targetThread); + + Thread.Sleep(1); + } + }, (cancellationToken = new CancellationTokenSource()).Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + internal void NewFrame(double elapsedFrameTime, double newSpikeThreshold) + { + if (targetThread == null) return; + + var frames = backgroundMonitorStackTrace; + backgroundMonitorStackTrace = null; + + var currentThreshold = spikeRecordThreshold; + + spikeRecordThreshold = newSpikeThreshold; + + if (!Enabled || elapsedFrameTime < currentThreshold || currentThreshold == 0) + return; + + StringBuilder logMessage = new StringBuilder(); + + logMessage.AppendLine($@"| Slow frame on thread ""{targetThread.Name}"""); + logMessage.AppendLine(@"|"); + logMessage.AppendLine($@"| * Thread time : {clock.CurrentTime:#0,#}ms"); + logMessage.AppendLine($@"| * Frame length : {elapsedFrameTime:#0,#}ms (allowable: {currentThreshold:#0,#}ms)"); + + logMessage.AppendLine(@"|"); + + if (frames != null) + { + logMessage.AppendLine(@"| Stack trace:"); + + foreach (var f in frames) + logMessage.AppendLine($@"|- {f.DisplayString}"); + } + else + logMessage.AppendLine(@"| Call stack was not recorded."); + + logger.Add(logMessage.ToString()); + } + + private static readonly Lazy clr_info = new Lazy(delegate + { + try + { + return DataTarget.AttachToProcess(Process.GetCurrentProcess().Id, 200, AttachFlag.Passive).ClrVersions[0]; + } + catch + { + return null; + } + }); + + private static IList getStackTrace(Thread targetThread) => clr_info.Value?.CreateRuntime().Threads.FirstOrDefault(t => t.ManagedThreadId == targetThread.ManagedThreadId)?.StackTrace; + + #region IDisposable Support + + ~BackgroundStackTraceCollector() + { + Dispose(false); + } + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + isDisposed = true; + cancellationToken?.Cancel(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/osu.Framework/Statistics/FrameStatistics.cs b/osu.Framework/Statistics/FrameStatistics.cs index 54fe284e1..bef7c6f55 100644 --- a/osu.Framework/Statistics/FrameStatistics.cs +++ b/osu.Framework/Statistics/FrameStatistics.cs @@ -1,69 +1,69 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Statistics -{ - internal class FrameStatistics - { - internal readonly Dictionary CollectedTimes = new Dictionary(NUM_STATISTICS_COUNTER_TYPES); - internal readonly Dictionary Counts = new Dictionary(NUM_STATISTICS_COUNTER_TYPES); - internal readonly List GarbageCollections = new List(); - - internal static readonly int NUM_STATISTICS_COUNTER_TYPES = Enum.GetValues(typeof(StatisticsCounterType)).Length; - internal static readonly int NUM_PERFORMANCE_COLLECTION_TYPES = Enum.GetValues(typeof(PerformanceCollectionType)).Length; - - internal static readonly long[] COUNTERS = new long[NUM_STATISTICS_COUNTER_TYPES]; - - internal void Clear() - { - CollectedTimes.Clear(); - GarbageCollections.Clear(); - Counts.Clear(); - } - - internal static void Increment(StatisticsCounterType type) => ++COUNTERS[(int)type]; - - internal static void Add(StatisticsCounterType type, long amount) => COUNTERS[(int)type] += amount; - } - - internal enum PerformanceCollectionType - { - Work = 0, - SwapBuffer, - WndProc, - Debug, - Sleep, - Scheduler, - IPC, - GLReset, - } - - internal enum StatisticsCounterType - { - Invalidations = 0, - Refreshes, - DrawNodeCtor, - DrawNodeAppl, - ScheduleInvk, - - VBufBinds, - VBufOverflow, - TextureBinds, - DrawCalls, - VerticesDraw, - VerticesUpl, - Pixels, - - TasksRun, - Tracks, - Samples, - SChannels, - Components, - - MouseEvents, - KeyEvents, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Statistics +{ + internal class FrameStatistics + { + internal readonly Dictionary CollectedTimes = new Dictionary(NUM_STATISTICS_COUNTER_TYPES); + internal readonly Dictionary Counts = new Dictionary(NUM_STATISTICS_COUNTER_TYPES); + internal readonly List GarbageCollections = new List(); + + internal static readonly int NUM_STATISTICS_COUNTER_TYPES = Enum.GetValues(typeof(StatisticsCounterType)).Length; + internal static readonly int NUM_PERFORMANCE_COLLECTION_TYPES = Enum.GetValues(typeof(PerformanceCollectionType)).Length; + + internal static readonly long[] COUNTERS = new long[NUM_STATISTICS_COUNTER_TYPES]; + + internal void Clear() + { + CollectedTimes.Clear(); + GarbageCollections.Clear(); + Counts.Clear(); + } + + internal static void Increment(StatisticsCounterType type) => ++COUNTERS[(int)type]; + + internal static void Add(StatisticsCounterType type, long amount) => COUNTERS[(int)type] += amount; + } + + internal enum PerformanceCollectionType + { + Work = 0, + SwapBuffer, + WndProc, + Debug, + Sleep, + Scheduler, + IPC, + GLReset, + } + + internal enum StatisticsCounterType + { + Invalidations = 0, + Refreshes, + DrawNodeCtor, + DrawNodeAppl, + ScheduleInvk, + + VBufBinds, + VBufOverflow, + TextureBinds, + DrawCalls, + VerticesDraw, + VerticesUpl, + Pixels, + + TasksRun, + Tracks, + Samples, + SChannels, + Components, + + MouseEvents, + KeyEvents, + } +} diff --git a/osu.Framework/Statistics/PerformanceMonitor.cs b/osu.Framework/Statistics/PerformanceMonitor.cs index 1e5879a73..1911826af 100644 --- a/osu.Framework/Statistics/PerformanceMonitor.cs +++ b/osu.Framework/Statistics/PerformanceMonitor.cs @@ -1,177 +1,177 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Timing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; - -namespace osu.Framework.Statistics -{ - internal class PerformanceMonitor : IDisposable - { - private readonly StopwatchClock ourClock = new StopwatchClock(true); - - private readonly Stack currentCollectionTypeStack = new Stack(); - - private readonly InvokeOnDisposal[] endCollectionDelegates = new InvokeOnDisposal[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES]; - - private readonly BackgroundStackTraceCollector traceCollector; - - private FrameStatistics currentFrame; - - private const int max_pending_frames = 100; - - internal readonly ConcurrentQueue PendingFrames = new ConcurrentQueue(); - internal readonly ObjectStack FramesHeap = new ObjectStack(max_pending_frames); - private readonly bool[] activeCounters = new bool[FrameStatistics.NUM_STATISTICS_COUNTER_TYPES]; - - internal bool[] ActiveCounters => (bool[])activeCounters.Clone(); - - public bool EnablePerformanceProfiling - { - get => traceCollector.Enabled; - set => traceCollector.Enabled = value; - } - - private double consumptionTime; - - internal ThrottledFrameClock Clock; - - public double FrameAimTime => 1000.0 / Clock?.MaximumUpdateHz ?? double.MaxValue; - - internal PerformanceMonitor(ThrottledFrameClock clock, Thread thread, IEnumerable counters) - { - Clock = clock; - currentFrame = FramesHeap.ReserveObject(); - - foreach (var c in counters) - activeCounters[(int)c] = true; - - for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++) - { - var t = (PerformanceCollectionType)i; - endCollectionDelegates[i] = new InvokeOnDisposal(() => endCollecting(t)); - } - - traceCollector = new BackgroundStackTraceCollector(thread, ourClock); - } - - /// - /// Start collecting a type of passing time. - /// - public InvokeOnDisposal BeginCollecting(PerformanceCollectionType type) - { - if (currentCollectionTypeStack.Count > 0) - { - PerformanceCollectionType t = currentCollectionTypeStack.Peek(); - - if (!currentFrame.CollectedTimes.ContainsKey(t)) currentFrame.CollectedTimes[t] = 0; - currentFrame.CollectedTimes[t] += consumeStopwatchElapsedTime(); - } - - currentCollectionTypeStack.Push(type); - - return endCollectionDelegates[(int)type]; - } - - /// - /// End collecting a type of passing time (that was previously started). - /// - /// - private void endCollecting(PerformanceCollectionType type) - { - currentCollectionTypeStack.Pop(); - - if (!currentFrame.CollectedTimes.ContainsKey(type)) currentFrame.CollectedTimes[type] = 0; - currentFrame.CollectedTimes[type] += consumeStopwatchElapsedTime(); - } - - private readonly int[] lastAmountGarbageCollects = new int[3]; - - public bool HandleGC = true; - - /// - /// Resets all frame statistics. Run exactly once per frame. - /// - public void NewFrame() - { - // Reset the counters we keep track of - for (int i = 0; i < activeCounters.Length; ++i) - if (activeCounters[i]) - { - currentFrame.Counts[(StatisticsCounterType)i] = FrameStatistics.COUNTERS[i]; - FrameStatistics.COUNTERS[i] = 0; - } - - PendingFrames.Enqueue(currentFrame); - if (PendingFrames.Count >= max_pending_frames) - { - PendingFrames.TryDequeue(out FrameStatistics oldFrame); - FramesHeap.FreeObject(oldFrame); - } - - currentFrame = FramesHeap.ReserveObject(); - currentFrame.Clear(); - - if (HandleGC) - { - for (int i = 0; i < lastAmountGarbageCollects.Length; ++i) - { - int amountCollections = GC.CollectionCount(i); - if (lastAmountGarbageCollects[i] != amountCollections) - { - lastAmountGarbageCollects[i] = amountCollections; - currentFrame.GarbageCollections.Add(i); - } - } - } - - //check for dropped (stutter) frames - traceCollector.NewFrame(Clock.ElapsedFrameTime, Math.Max(10, Math.Max(1000 / Clock.MaximumUpdateHz, AverageFrameTime) * 4)); - - //reset frame totals - currentCollectionTypeStack.Clear(); - consumeStopwatchElapsedTime(); - } - - private double consumeStopwatchElapsedTime() - { - double last = consumptionTime; - - consumptionTime = traceCollector.LastConsumptionTime = ourClock.CurrentTime; - - return consumptionTime - last; - } - - internal double FramesPerSecond => Clock.FramesPerSecond; - internal double AverageFrameTime => Clock.AverageFrameTime; - - #region IDisposable Support - - private bool isDisposed; - - protected virtual void Dispose(bool disposing) - { - if (!isDisposed) - { - isDisposed = true; - traceCollector.Dispose(); - } - } - - ~PerformanceMonitor() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Timing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace osu.Framework.Statistics +{ + internal class PerformanceMonitor : IDisposable + { + private readonly StopwatchClock ourClock = new StopwatchClock(true); + + private readonly Stack currentCollectionTypeStack = new Stack(); + + private readonly InvokeOnDisposal[] endCollectionDelegates = new InvokeOnDisposal[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES]; + + private readonly BackgroundStackTraceCollector traceCollector; + + private FrameStatistics currentFrame; + + private const int max_pending_frames = 100; + + internal readonly ConcurrentQueue PendingFrames = new ConcurrentQueue(); + internal readonly ObjectStack FramesHeap = new ObjectStack(max_pending_frames); + private readonly bool[] activeCounters = new bool[FrameStatistics.NUM_STATISTICS_COUNTER_TYPES]; + + internal bool[] ActiveCounters => (bool[])activeCounters.Clone(); + + public bool EnablePerformanceProfiling + { + get => traceCollector.Enabled; + set => traceCollector.Enabled = value; + } + + private double consumptionTime; + + internal ThrottledFrameClock Clock; + + public double FrameAimTime => 1000.0 / Clock?.MaximumUpdateHz ?? double.MaxValue; + + internal PerformanceMonitor(ThrottledFrameClock clock, Thread thread, IEnumerable counters) + { + Clock = clock; + currentFrame = FramesHeap.ReserveObject(); + + foreach (var c in counters) + activeCounters[(int)c] = true; + + for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++) + { + var t = (PerformanceCollectionType)i; + endCollectionDelegates[i] = new InvokeOnDisposal(() => endCollecting(t)); + } + + traceCollector = new BackgroundStackTraceCollector(thread, ourClock); + } + + /// + /// Start collecting a type of passing time. + /// + public InvokeOnDisposal BeginCollecting(PerformanceCollectionType type) + { + if (currentCollectionTypeStack.Count > 0) + { + PerformanceCollectionType t = currentCollectionTypeStack.Peek(); + + if (!currentFrame.CollectedTimes.ContainsKey(t)) currentFrame.CollectedTimes[t] = 0; + currentFrame.CollectedTimes[t] += consumeStopwatchElapsedTime(); + } + + currentCollectionTypeStack.Push(type); + + return endCollectionDelegates[(int)type]; + } + + /// + /// End collecting a type of passing time (that was previously started). + /// + /// + private void endCollecting(PerformanceCollectionType type) + { + currentCollectionTypeStack.Pop(); + + if (!currentFrame.CollectedTimes.ContainsKey(type)) currentFrame.CollectedTimes[type] = 0; + currentFrame.CollectedTimes[type] += consumeStopwatchElapsedTime(); + } + + private readonly int[] lastAmountGarbageCollects = new int[3]; + + public bool HandleGC = true; + + /// + /// Resets all frame statistics. Run exactly once per frame. + /// + public void NewFrame() + { + // Reset the counters we keep track of + for (int i = 0; i < activeCounters.Length; ++i) + if (activeCounters[i]) + { + currentFrame.Counts[(StatisticsCounterType)i] = FrameStatistics.COUNTERS[i]; + FrameStatistics.COUNTERS[i] = 0; + } + + PendingFrames.Enqueue(currentFrame); + if (PendingFrames.Count >= max_pending_frames) + { + PendingFrames.TryDequeue(out FrameStatistics oldFrame); + FramesHeap.FreeObject(oldFrame); + } + + currentFrame = FramesHeap.ReserveObject(); + currentFrame.Clear(); + + if (HandleGC) + { + for (int i = 0; i < lastAmountGarbageCollects.Length; ++i) + { + int amountCollections = GC.CollectionCount(i); + if (lastAmountGarbageCollects[i] != amountCollections) + { + lastAmountGarbageCollects[i] = amountCollections; + currentFrame.GarbageCollections.Add(i); + } + } + } + + //check for dropped (stutter) frames + traceCollector.NewFrame(Clock.ElapsedFrameTime, Math.Max(10, Math.Max(1000 / Clock.MaximumUpdateHz, AverageFrameTime) * 4)); + + //reset frame totals + currentCollectionTypeStack.Clear(); + consumeStopwatchElapsedTime(); + } + + private double consumeStopwatchElapsedTime() + { + double last = consumptionTime; + + consumptionTime = traceCollector.LastConsumptionTime = ourClock.CurrentTime; + + return consumptionTime - last; + } + + internal double FramesPerSecond => Clock.FramesPerSecond; + internal double AverageFrameTime => Clock.AverageFrameTime; + + #region IDisposable Support + + private bool isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + isDisposed = true; + traceCollector.Dispose(); + } + } + + ~PerformanceMonitor() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/AssertButton.cs b/osu.Framework/Testing/Drawables/Steps/AssertButton.cs index 9b77f69ac..b84a314a4 100644 --- a/osu.Framework/Testing/Drawables/Steps/AssertButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/AssertButton.cs @@ -1,45 +1,45 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using OpenTK.Graphics; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class AssertButton : StepButton - { - public Func Assertion; - public string ExtendedDescription; - public StackTrace CallStack; - - public AssertButton() - { - Action += checkAssert; - LightColour = Color4.OrangeRed; - } - - private void checkAssert() - { - if (Assertion()) - Success(); - else - throw new TracedException($"{Text} {ExtendedDescription}", CallStack); - } - - public override string ToString() => "Assert: " + base.ToString(); - - private class TracedException : Exception - { - private readonly StackTrace trace; - - public TracedException(string description, StackTrace trace) - : base(description) - { - this.trace = trace; - } - - public override string StackTrace => trace.ToString(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using OpenTK.Graphics; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class AssertButton : StepButton + { + public Func Assertion; + public string ExtendedDescription; + public StackTrace CallStack; + + public AssertButton() + { + Action += checkAssert; + LightColour = Color4.OrangeRed; + } + + private void checkAssert() + { + if (Assertion()) + Success(); + else + throw new TracedException($"{Text} {ExtendedDescription}", CallStack); + } + + public override string ToString() => "Assert: " + base.ToString(); + + private class TracedException : Exception + { + private readonly StackTrace trace; + + public TracedException(string description, StackTrace trace) + : base(description) + { + this.trace = trace; + } + + public override string StackTrace => trace.ToString(); + } + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/RepeatStepButton.cs b/osu.Framework/Testing/Drawables/Steps/RepeatStepButton.cs index 5ea649912..ccdb925a9 100644 --- a/osu.Framework/Testing/Drawables/Steps/RepeatStepButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/RepeatStepButton.cs @@ -1,57 +1,57 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class RepeatStepButton : StepButton - { - private readonly int count; - private int invocations; - - public override int RequiredRepetitions => count; - - private string text; - - public new string Text - { - get { return text; } - set { base.Text = text = value; } - } - - public RepeatStepButton(Action action, int count = 1) - { - this.count = count; - Action = action; - - updateText(); - } - - public override void PerformStep(bool userTriggered = false) - { - if (invocations == count && !userTriggered) throw new InvalidOperationException("Repeat step was invoked too many times"); - - invocations++; - - base.PerformStep(userTriggered); - - if (invocations >= count) // Allows for manual execution beyond the invocation limit. - Success(); - - updateText(); - } - - public override void Reset() - { - base.Reset(); - - invocations = 0; - updateText(); - } - - private void updateText() => base.Text = $@"{Text} {invocations}/{count}"; - - public override string ToString() => "Repeat: " + base.ToString(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class RepeatStepButton : StepButton + { + private readonly int count; + private int invocations; + + public override int RequiredRepetitions => count; + + private string text; + + public new string Text + { + get { return text; } + set { base.Text = text = value; } + } + + public RepeatStepButton(Action action, int count = 1) + { + this.count = count; + Action = action; + + updateText(); + } + + public override void PerformStep(bool userTriggered = false) + { + if (invocations == count && !userTriggered) throw new InvalidOperationException("Repeat step was invoked too many times"); + + invocations++; + + base.PerformStep(userTriggered); + + if (invocations >= count) // Allows for manual execution beyond the invocation limit. + Success(); + + updateText(); + } + + public override void Reset() + { + base.Reset(); + + invocations = 0; + updateText(); + } + + private void updateText() => base.Text = $@"{Text} {invocations}/{count}"; + + public override string ToString() => "Repeat: " + base.ToString(); + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/SingleStepButton.cs b/osu.Framework/Testing/Drawables/Steps/SingleStepButton.cs index 81a38192c..0eed6f254 100644 --- a/osu.Framework/Testing/Drawables/Steps/SingleStepButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/SingleStepButton.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class SingleStepButton : StepButton - { - public new Action Action; - - public SingleStepButton() - { - base.Action = () => - { - Action?.Invoke(); - Success(); - }; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class SingleStepButton : StepButton + { + public new Action Action; + + public SingleStepButton() + { + base.Action = () => + { + Action?.Invoke(); + Success(); + }; + } + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/StepButton.cs b/osu.Framework/Testing/Drawables/Steps/StepButton.cs index 3d7092163..acb8cceea 100644 --- a/osu.Framework/Testing/Drawables/Steps/StepButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/StepButton.cs @@ -1,143 +1,143 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using OpenTK.Graphics; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public abstract class StepButton : CompositeDrawable - { - public virtual int RequiredRepetitions => 1; - - protected Box Light; - protected Box Background; - protected SpriteText SpriteText; - - public Action Action { get; protected set; } - - public string Text - { - get { return SpriteText.Text; } - set { SpriteText.Text = value; } - } - - private Color4 lightColour = Color4.BlueViolet; - - public Color4 LightColour - { - get { return lightColour; } - set - { - lightColour = value; - if (IsLoaded) Reset(); - } - } - - private readonly Color4 idleColour = new Color4(0.15f, 0.15f, 0.15f, 1); - private readonly Color4 runningColour = new Color4(0.5f, 0.5f, 0.5f, 1); - - protected StepButton() - { - InternalChildren = new Drawable[] - { - Background = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = idleColour, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - Light = new Box - { - RelativeSizeAxes = Axes.Y, - Width = 5, - }, - SpriteText = new SpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - TextSize = 14, - X = 5, - Padding = new MarginPadding(5), - } - }; - - Height = 20; - RelativeSizeAxes = Axes.X; - - BorderThickness = 1.5f; - BorderColour = new Color4(0.15f, 0.15f, 0.15f, 1); - - CornerRadius = 2; - Masking = true; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Reset(); - } - - protected override bool OnClick(InputState state) - { - try - { - PerformStep(true); - } - catch (Exception e) - { - Logging.Logger.Error(e, $"Step {this} triggered an error"); - } - - return true; - } - - /// - /// Reset this step to a default state. - /// - public virtual void Reset() - { - Background.DelayUntilTransformsFinished().FadeColour(idleColour, 1000, Easing.OutQuint); - Light.FadeColour(lightColour); - } - - public virtual void PerformStep(bool userTriggered = false) - { - Background.ClearTransforms(); - Background.FadeColour(runningColour, 400, Easing.OutQuint); - - try - { - Action?.Invoke(); - } - catch (Exception) - { - Failure(); - throw; - } - } - - protected virtual void Failure() - { - Background.DelayUntilTransformsFinished().FadeColour(new Color4(0.3f, 0.15f, 0.15f, 1), 1000, Easing.OutQuint); - Light.FadeColour(Color4.Red); - } - - protected virtual void Success() - { - Background.FinishTransforms(); - Background.FadeColour(idleColour, 1000, Easing.OutQuint); - - Light.FadeColour(Color4.YellowGreen); - SpriteText.Alpha = 0.8f; - } - - public override string ToString() => Text; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using OpenTK.Graphics; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public abstract class StepButton : CompositeDrawable + { + public virtual int RequiredRepetitions => 1; + + protected Box Light; + protected Box Background; + protected SpriteText SpriteText; + + public Action Action { get; protected set; } + + public string Text + { + get { return SpriteText.Text; } + set { SpriteText.Text = value; } + } + + private Color4 lightColour = Color4.BlueViolet; + + public Color4 LightColour + { + get { return lightColour; } + set + { + lightColour = value; + if (IsLoaded) Reset(); + } + } + + private readonly Color4 idleColour = new Color4(0.15f, 0.15f, 0.15f, 1); + private readonly Color4 runningColour = new Color4(0.5f, 0.5f, 0.5f, 1); + + protected StepButton() + { + InternalChildren = new Drawable[] + { + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = idleColour, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + Light = new Box + { + RelativeSizeAxes = Axes.Y, + Width = 5, + }, + SpriteText = new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = 14, + X = 5, + Padding = new MarginPadding(5), + } + }; + + Height = 20; + RelativeSizeAxes = Axes.X; + + BorderThickness = 1.5f; + BorderColour = new Color4(0.15f, 0.15f, 0.15f, 1); + + CornerRadius = 2; + Masking = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Reset(); + } + + protected override bool OnClick(InputState state) + { + try + { + PerformStep(true); + } + catch (Exception e) + { + Logging.Logger.Error(e, $"Step {this} triggered an error"); + } + + return true; + } + + /// + /// Reset this step to a default state. + /// + public virtual void Reset() + { + Background.DelayUntilTransformsFinished().FadeColour(idleColour, 1000, Easing.OutQuint); + Light.FadeColour(lightColour); + } + + public virtual void PerformStep(bool userTriggered = false) + { + Background.ClearTransforms(); + Background.FadeColour(runningColour, 400, Easing.OutQuint); + + try + { + Action?.Invoke(); + } + catch (Exception) + { + Failure(); + throw; + } + } + + protected virtual void Failure() + { + Background.DelayUntilTransformsFinished().FadeColour(new Color4(0.3f, 0.15f, 0.15f, 1), 1000, Easing.OutQuint); + Light.FadeColour(Color4.Red); + } + + protected virtual void Success() + { + Background.FinishTransforms(); + Background.FadeColour(idleColour, 1000, Easing.OutQuint); + + Light.FadeColour(Color4.YellowGreen); + SpriteText.Alpha = 0.8f; + } + + public override string ToString() => Text; + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/StepSlider.cs b/osu.Framework/Testing/Drawables/Steps/StepSlider.cs index 572ee50ae..e6ded3298 100644 --- a/osu.Framework/Testing/Drawables/Steps/StepSlider.cs +++ b/osu.Framework/Testing/Drawables/Steps/StepSlider.cs @@ -1,105 +1,105 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using OpenTK.Graphics; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using System; -using osu.Framework.Input; -using osu.Framework.Extensions.Color4Extensions; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class StepSlider : SliderBar - where T : struct, IComparable, IConvertible - { - private readonly Box selection; - private readonly Box background; - private readonly SpriteText spriteText; - - private readonly string text; - - public Action ValueChanged; - - public StepSlider(string description, T min, T max, T start) - { - text = description; - - // Styling - Height = 25; - RelativeSizeAxes = Axes.X; - - AddRangeInternal(new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.RoyalBlue.Darken(0.75f), - }, - selection = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.RoyalBlue, - }, - spriteText = new SpriteText - { - Depth = -1, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - }, - }); - - CornerRadius = 2; - Masking = true; - - spriteText.Anchor = Anchor.CentreLeft; - spriteText.Origin = Anchor.CentreLeft; - spriteText.Padding = new MarginPadding(5); - - // Bind to the underlying sliderbar - var currentNumber = (BindableNumber)Current; - currentNumber.MinValue = min; - currentNumber.MaxValue = max; - currentNumber.Default = start; - currentNumber.SetDefault(); - } - - protected override bool OnDragEnd(InputState state) - { - var flash = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.RoyalBlue, - Blending = BlendingMode.Additive, - Alpha = 0.6f, - }; - - Add(flash); - flash.FadeOut(200).Expire(); - - Success(); - return base.OnDragEnd(state); - } - - protected override void UpdateValue(float normalizedValue) - { - var value = Current.Value; - - ValueChanged?.Invoke(value); - spriteText.Text = $"{text}: {Convert.ToDouble(value):G3}"; - selection.ResizeWidthTo(normalizedValue); - } - - protected void Success() - { - background.Alpha = 0.4f; - selection.Alpha = 0.4f; - spriteText.Alpha = 0.8f; - } - - public override string ToString() => spriteText.Text; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using System; +using osu.Framework.Input; +using osu.Framework.Extensions.Color4Extensions; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class StepSlider : SliderBar + where T : struct, IComparable, IConvertible + { + private readonly Box selection; + private readonly Box background; + private readonly SpriteText spriteText; + + private readonly string text; + + public Action ValueChanged; + + public StepSlider(string description, T min, T max, T start) + { + text = description; + + // Styling + Height = 25; + RelativeSizeAxes = Axes.X; + + AddRangeInternal(new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.RoyalBlue.Darken(0.75f), + }, + selection = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.RoyalBlue, + }, + spriteText = new SpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + }); + + CornerRadius = 2; + Masking = true; + + spriteText.Anchor = Anchor.CentreLeft; + spriteText.Origin = Anchor.CentreLeft; + spriteText.Padding = new MarginPadding(5); + + // Bind to the underlying sliderbar + var currentNumber = (BindableNumber)Current; + currentNumber.MinValue = min; + currentNumber.MaxValue = max; + currentNumber.Default = start; + currentNumber.SetDefault(); + } + + protected override bool OnDragEnd(InputState state) + { + var flash = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.RoyalBlue, + Blending = BlendingMode.Additive, + Alpha = 0.6f, + }; + + Add(flash); + flash.FadeOut(200).Expire(); + + Success(); + return base.OnDragEnd(state); + } + + protected override void UpdateValue(float normalizedValue) + { + var value = Current.Value; + + ValueChanged?.Invoke(value); + spriteText.Text = $"{text}: {Convert.ToDouble(value):G3}"; + selection.ResizeWidthTo(normalizedValue); + } + + protected void Success() + { + background.Alpha = 0.4f; + selection.Alpha = 0.4f; + spriteText.Alpha = 0.8f; + } + + public override string ToString() => spriteText.Text; + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/ToggleStepButton.cs b/osu.Framework/Testing/Drawables/Steps/ToggleStepButton.cs index 9eecb5611..b13a5344b 100644 --- a/osu.Framework/Testing/Drawables/Steps/ToggleStepButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/ToggleStepButton.cs @@ -1,39 +1,39 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Graphics; -using OpenTK.Graphics; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class ToggleStepButton : StepButton - { - private readonly Action reloadCallback; - private static readonly Color4 off_colour = Color4.Red; - private static readonly Color4 on_colour = Color4.YellowGreen; - - public bool State; - - public override int RequiredRepetitions => 2; - - public ToggleStepButton(Action reloadCallback) - { - this.reloadCallback = reloadCallback; - Action = clickAction; - LightColour = off_colour; - } - - private void clickAction() - { - State = !State; - Light.FadeColour(State ? on_colour : off_colour); - reloadCallback?.Invoke(State); - - if (!State) - Success(); - } - - public override string ToString() => $"Toggle: {base.ToString()} ({(State ? "on" : "off")})"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Graphics; +using OpenTK.Graphics; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class ToggleStepButton : StepButton + { + private readonly Action reloadCallback; + private static readonly Color4 off_colour = Color4.Red; + private static readonly Color4 on_colour = Color4.YellowGreen; + + public bool State; + + public override int RequiredRepetitions => 2; + + public ToggleStepButton(Action reloadCallback) + { + this.reloadCallback = reloadCallback; + Action = clickAction; + LightColour = off_colour; + } + + private void clickAction() + { + State = !State; + Light.FadeColour(State ? on_colour : off_colour); + reloadCallback?.Invoke(State); + + if (!State) + Success(); + } + + public override string ToString() => $"Toggle: {base.ToString()} ({(State ? "on" : "off")})"; + } +} diff --git a/osu.Framework/Testing/Drawables/Steps/UntilStepButton.cs b/osu.Framework/Testing/Drawables/Steps/UntilStepButton.cs index b4d58db27..a02de8dec 100644 --- a/osu.Framework/Testing/Drawables/Steps/UntilStepButton.cs +++ b/osu.Framework/Testing/Drawables/Steps/UntilStepButton.cs @@ -1,85 +1,85 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using osu.Framework.Graphics; -using OpenTK.Graphics; - -namespace osu.Framework.Testing.Drawables.Steps -{ - public class UntilStepButton : StepButton - { - private bool success; - - private int invocations; - - private const int max_attempt_milliseconds = 10000; - - public override int RequiredRepetitions => success ? 0 : int.MaxValue; - - public new Action Action; - - private string text; - - public new string Text - { - get { return text; } - set { base.Text = text = value; } - } - - private Stopwatch elapsedTime; - - public UntilStepButton(Func waitUntilTrueDelegate) - { - updateText(); - LightColour = Color4.Sienna; - - base.Action = () => - { - invocations++; - - if (elapsedTime == null) - elapsedTime = Stopwatch.StartNew(); - - updateText(); - - if (waitUntilTrueDelegate()) - { - elapsedTime = null; - success = true; - Success(); - } - else if (elapsedTime.ElapsedMilliseconds >= max_attempt_milliseconds) - throw new TimeoutException(); - - Action?.Invoke(); - }; - } - - public override void Reset() - { - base.Reset(); - - invocations = 0; - elapsedTime = null; - success = false; - } - - protected override void Success() - { - base.Success(); - Light.FadeColour(Color4.YellowGreen); - } - - protected override void Failure() - { - base.Failure(); - Light.FadeColour(Color4.Red); - } - - private void updateText() => base.Text = $@"{Text} ({invocations} tries)"; - - public override string ToString() => "Repeat: " + base.ToString(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using osu.Framework.Graphics; +using OpenTK.Graphics; + +namespace osu.Framework.Testing.Drawables.Steps +{ + public class UntilStepButton : StepButton + { + private bool success; + + private int invocations; + + private const int max_attempt_milliseconds = 10000; + + public override int RequiredRepetitions => success ? 0 : int.MaxValue; + + public new Action Action; + + private string text; + + public new string Text + { + get { return text; } + set { base.Text = text = value; } + } + + private Stopwatch elapsedTime; + + public UntilStepButton(Func waitUntilTrueDelegate) + { + updateText(); + LightColour = Color4.Sienna; + + base.Action = () => + { + invocations++; + + if (elapsedTime == null) + elapsedTime = Stopwatch.StartNew(); + + updateText(); + + if (waitUntilTrueDelegate()) + { + elapsedTime = null; + success = true; + Success(); + } + else if (elapsedTime.ElapsedMilliseconds >= max_attempt_milliseconds) + throw new TimeoutException(); + + Action?.Invoke(); + }; + } + + public override void Reset() + { + base.Reset(); + + invocations = 0; + elapsedTime = null; + success = false; + } + + protected override void Success() + { + base.Success(); + Light.FadeColour(Color4.YellowGreen); + } + + protected override void Failure() + { + base.Failure(); + Light.FadeColour(Color4.Red); + } + + private void updateText() => base.Text = $@"{Text} ({invocations} tries)"; + + public override string ToString() => "Repeat: " + base.ToString(); + } +} diff --git a/osu.Framework/Testing/Drawables/TestCaseButton.cs b/osu.Framework/Testing/Drawables/TestCaseButton.cs index b97d71765..9153f03c1 100644 --- a/osu.Framework/Testing/Drawables/TestCaseButton.cs +++ b/osu.Framework/Testing/Drawables/TestCaseButton.cs @@ -1,109 +1,109 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.ComponentModel; -using System.Reflection; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using OpenTK.Graphics; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Framework.Testing.Drawables -{ - internal class TestCaseButton : ClickableContainer, IFilterable - { - public IEnumerable FilterTerms => text.Children.OfType().SelectMany(c => c.FilterTerms); - - public bool MatchingFilter - { - set - { - if (value) - Show(); - else - Hide(); - } - } - - private readonly Box box; - private readonly TextFlowContainer text; - - public readonly Type TestType; - - public bool Current - { - set - { - const float transition_duration = 100; - - if (value) - { - box.FadeColour(new Color4(220, 220, 220, 255), transition_duration); - text.FadeColour(Color4.Black, transition_duration); - } - else - { - box.FadeColour(new Color4(140, 140, 140, 255), transition_duration); - text.FadeColour(Color4.White, transition_duration); - } - } - } - - public TestCaseButton(Type test) - { - Masking = true; - - TestType = test; - - CornerRadius = 5; - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - AddRange(new Drawable[] - { - box = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(140, 140, 140, 255), - Alpha = 0.7f - }, - text = new TextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Left = 4, - Right = 4, - Bottom = 2, - }, - } - }); - - text.AddText(test.Name.Replace("TestCase", "")); - - var description = test.GetCustomAttribute()?.Description; - if (description != null) - { - text.NewLine(); - text.AddText(description, t => t.TextSize = 15); - } - } - - protected override bool OnHover(InputState state) - { - box.FadeTo(1, 150); - return true; - } - - protected override void OnHoverLost(InputState state) - { - box.FadeTo(0.7f, 150); - base.OnHoverLost(state); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.ComponentModel; +using System.Reflection; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using OpenTK.Graphics; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Framework.Testing.Drawables +{ + internal class TestCaseButton : ClickableContainer, IFilterable + { + public IEnumerable FilterTerms => text.Children.OfType().SelectMany(c => c.FilterTerms); + + public bool MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + private readonly Box box; + private readonly TextFlowContainer text; + + public readonly Type TestType; + + public bool Current + { + set + { + const float transition_duration = 100; + + if (value) + { + box.FadeColour(new Color4(220, 220, 220, 255), transition_duration); + text.FadeColour(Color4.Black, transition_duration); + } + else + { + box.FadeColour(new Color4(140, 140, 140, 255), transition_duration); + text.FadeColour(Color4.White, transition_duration); + } + } + } + + public TestCaseButton(Type test) + { + Masking = true; + + TestType = test; + + CornerRadius = 5; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddRange(new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(140, 140, 140, 255), + Alpha = 0.7f + }, + text = new TextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Left = 4, + Right = 4, + Bottom = 2, + }, + } + }); + + text.AddText(test.Name.Replace("TestCase", "")); + + var description = test.GetCustomAttribute()?.Description; + if (description != null) + { + text.NewLine(); + text.AddText(description, t => t.TextSize = 15); + } + } + + protected override bool OnHover(InputState state) + { + box.FadeTo(1, 150); + return true; + } + + protected override void OnHoverLost(InputState state) + { + box.FadeTo(0.7f, 150); + base.OnHoverLost(state); + } + } +} diff --git a/osu.Framework/Testing/DynamicClassCompiler.cs b/osu.Framework/Testing/DynamicClassCompiler.cs index 73f226227..ec564506b 100644 --- a/osu.Framework/Testing/DynamicClassCompiler.cs +++ b/osu.Framework/Testing/DynamicClassCompiler.cs @@ -1,196 +1,196 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using osu.Framework.Logging; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace osu.Framework.Testing -{ - public class DynamicClassCompiler - where T : IDynamicallyCompile - { - public Action CompilationStarted; - - public Action CompilationFinished; - - public Action CompilationFailed; - - private readonly List watchers = new List(); - - private string lastTouchedFile; - - private T checkpointObject; - - public void Checkpoint(T obj) - { - checkpointObject = obj; - } - - private readonly List requiredFiles = new List(); - private List requiredTypeNames = new List(); - - private HashSet assemblies; - - private readonly List validDirectories = new List(); - - public void Start() - { - var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); - - Task.Run(() => - { - var basePath = di.Parent?.Parent?.Parent?.Parent?.FullName; - - if (!Directory.Exists(basePath)) - return; - - foreach (var dir in Directory.GetDirectories(basePath)) - { - // only watch directories which house a csproj. this avoids submodules and directories like .git which can contain many files. - if (!Directory.GetFiles(dir, "*.csproj").Any()) - continue; - - validDirectories.Add(dir); - - var fsw = new FileSystemWatcher(dir, @"*.cs") - { - EnableRaisingEvents = true, - IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, - }; - - fsw.Changed += onChange; - - watchers.Add(fsw); - } - }); - } - - private void onChange(object sender, FileSystemEventArgs e) - { - lock (compileLock) - { - if (checkpointObject == null || isCompiling) - return; - - var checkpointName = checkpointObject.GetType().Name; - - var reqTypes = checkpointObject.RequiredTypes.Select(t => t.Name).ToList(); - - // add ourselves as a required type. - reqTypes.Add(checkpointName); - // if we are a TestCase, add the class we are testing automatically. - reqTypes.Add(checkpointName.Replace("TestCase", "")); - - if (!reqTypes.Contains(Path.GetFileNameWithoutExtension(e.Name))) - return; - - if (!reqTypes.SequenceEqual(requiredTypeNames)) - { - requiredTypeNames = reqTypes; - - requiredFiles.Clear(); - foreach (var d in validDirectories) - requiredFiles.AddRange(Directory - .EnumerateFiles(d, "*.cs", SearchOption.AllDirectories) - .Where(fw => requiredTypeNames.Contains(Path.GetFileNameWithoutExtension(fw)))); - } - - lastTouchedFile = e.FullPath; - - isCompiling = true; - Task.Run((Action)recompile) - .ContinueWith(_ => isCompiling = false); - } - } - - private int currentVersion; - - private bool isCompiling; - - private readonly object compileLock = new object(); - - private void recompile() - { - if (assemblies == null) - { - assemblies = new HashSet(); - foreach (var ass in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic)) - assemblies.Add(ass.Location); - } - - var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); - var references = assemblies.Select(a => MetadataReference.CreateFromFile(a)); - - while (!checkFileReady(lastTouchedFile)) - Thread.Sleep(10); - - Logger.Log($@"Recompiling {Path.GetFileName(checkpointObject.GetType().Name)}...", LoggingTarget.Runtime, LogLevel.Important); - - CompilationStarted?.Invoke(); - - // ensure we don't duplicate the dynamic suffix. - string assemblyNamespace = checkpointObject.GetType().Assembly.GetName().Name.Replace(".Dynamic", ""); - - string assemblyVersion = $"{++currentVersion}.0.*"; - string dynamicNamespace = $"{assemblyNamespace}.Dynamic"; - - var compilation = CSharpCompilation.Create( - dynamicNamespace, - requiredFiles.Select(file => CSharpSyntaxTree.ParseText(File.ReadAllText(file), null, file)) - // Compile the assembly with a new version so that it replaces the existing one - .Append(CSharpSyntaxTree.ParseText($"using System.Reflection; [assembly: AssemblyVersion(\"{assemblyVersion}\")]")) - , - references, - options - ); - - using (var ms = new MemoryStream()) - { - var compilationResult = compilation.Emit(ms); - - if (compilationResult.Success) - { - ms.Seek(0, SeekOrigin.Begin); - CompilationFinished?.Invoke( - Assembly.Load(ms.ToArray()).GetModules()[0]?.GetTypes().LastOrDefault(t => t.FullName == checkpointObject.GetType().FullName) - ); - } - else - { - foreach (var diagnostic in compilationResult.Diagnostics) - { - if (diagnostic.Severity < DiagnosticSeverity.Error) - continue; - - CompilationFailed?.Invoke(new Exception(diagnostic.ToString())); - } - } - } - } - - /// - /// Check whether a file has finished being written to. - /// - private static bool checkFileReady(string filename) - { - try - { - using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None)) - return inputStream.Length > 0; - } - catch (Exception) - { - return false; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using osu.Framework.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace osu.Framework.Testing +{ + public class DynamicClassCompiler + where T : IDynamicallyCompile + { + public Action CompilationStarted; + + public Action CompilationFinished; + + public Action CompilationFailed; + + private readonly List watchers = new List(); + + private string lastTouchedFile; + + private T checkpointObject; + + public void Checkpoint(T obj) + { + checkpointObject = obj; + } + + private readonly List requiredFiles = new List(); + private List requiredTypeNames = new List(); + + private HashSet assemblies; + + private readonly List validDirectories = new List(); + + public void Start() + { + var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); + + Task.Run(() => + { + var basePath = di.Parent?.Parent?.Parent?.Parent?.FullName; + + if (!Directory.Exists(basePath)) + return; + + foreach (var dir in Directory.GetDirectories(basePath)) + { + // only watch directories which house a csproj. this avoids submodules and directories like .git which can contain many files. + if (!Directory.GetFiles(dir, "*.csproj").Any()) + continue; + + validDirectories.Add(dir); + + var fsw = new FileSystemWatcher(dir, @"*.cs") + { + EnableRaisingEvents = true, + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, + }; + + fsw.Changed += onChange; + + watchers.Add(fsw); + } + }); + } + + private void onChange(object sender, FileSystemEventArgs e) + { + lock (compileLock) + { + if (checkpointObject == null || isCompiling) + return; + + var checkpointName = checkpointObject.GetType().Name; + + var reqTypes = checkpointObject.RequiredTypes.Select(t => t.Name).ToList(); + + // add ourselves as a required type. + reqTypes.Add(checkpointName); + // if we are a TestCase, add the class we are testing automatically. + reqTypes.Add(checkpointName.Replace("TestCase", "")); + + if (!reqTypes.Contains(Path.GetFileNameWithoutExtension(e.Name))) + return; + + if (!reqTypes.SequenceEqual(requiredTypeNames)) + { + requiredTypeNames = reqTypes; + + requiredFiles.Clear(); + foreach (var d in validDirectories) + requiredFiles.AddRange(Directory + .EnumerateFiles(d, "*.cs", SearchOption.AllDirectories) + .Where(fw => requiredTypeNames.Contains(Path.GetFileNameWithoutExtension(fw)))); + } + + lastTouchedFile = e.FullPath; + + isCompiling = true; + Task.Run((Action)recompile) + .ContinueWith(_ => isCompiling = false); + } + } + + private int currentVersion; + + private bool isCompiling; + + private readonly object compileLock = new object(); + + private void recompile() + { + if (assemblies == null) + { + assemblies = new HashSet(); + foreach (var ass in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic)) + assemblies.Add(ass.Location); + } + + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + var references = assemblies.Select(a => MetadataReference.CreateFromFile(a)); + + while (!checkFileReady(lastTouchedFile)) + Thread.Sleep(10); + + Logger.Log($@"Recompiling {Path.GetFileName(checkpointObject.GetType().Name)}...", LoggingTarget.Runtime, LogLevel.Important); + + CompilationStarted?.Invoke(); + + // ensure we don't duplicate the dynamic suffix. + string assemblyNamespace = checkpointObject.GetType().Assembly.GetName().Name.Replace(".Dynamic", ""); + + string assemblyVersion = $"{++currentVersion}.0.*"; + string dynamicNamespace = $"{assemblyNamespace}.Dynamic"; + + var compilation = CSharpCompilation.Create( + dynamicNamespace, + requiredFiles.Select(file => CSharpSyntaxTree.ParseText(File.ReadAllText(file), null, file)) + // Compile the assembly with a new version so that it replaces the existing one + .Append(CSharpSyntaxTree.ParseText($"using System.Reflection; [assembly: AssemblyVersion(\"{assemblyVersion}\")]")) + , + references, + options + ); + + using (var ms = new MemoryStream()) + { + var compilationResult = compilation.Emit(ms); + + if (compilationResult.Success) + { + ms.Seek(0, SeekOrigin.Begin); + CompilationFinished?.Invoke( + Assembly.Load(ms.ToArray()).GetModules()[0]?.GetTypes().LastOrDefault(t => t.FullName == checkpointObject.GetType().FullName) + ); + } + else + { + foreach (var diagnostic in compilationResult.Diagnostics) + { + if (diagnostic.Severity < DiagnosticSeverity.Error) + continue; + + CompilationFailed?.Invoke(new Exception(diagnostic.ToString())); + } + } + } + } + + /// + /// Check whether a file has finished being written to. + /// + private static bool checkFileReady(string filename) + { + try + { + using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None)) + return inputStream.Length > 0; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/osu.Framework/Testing/GridTestCase.cs b/osu.Framework/Testing/GridTestCase.cs index f623a4124..136fe1ce6 100644 --- a/osu.Framework/Testing/GridTestCase.cs +++ b/osu.Framework/Testing/GridTestCase.cs @@ -1,50 +1,50 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Framework.Testing -{ - /// - /// An abstract test case which exposes small cells arranged in a grid. - /// Useful for displaying multiple configurations of a tested component at a glance. - /// - public abstract class GridTestCase : TestCase - { - private readonly Drawable[,] cells; - - /// - /// The amount of rows in the grid. - /// - protected readonly int Rows; - - /// - /// The amount of columns in the grid. - /// - protected readonly int Cols; - - /// - /// Constructs a grid test case with the given dimensions. - /// - protected GridTestCase(int rows, int cols) - { - Rows = rows; - Cols = cols; - - GridContainer testContainer; - Add(testContainer = new GridContainer { RelativeSizeAxes = Axes.Both }); - - cells = new Drawable[rows, cols]; - for (int r = 0; r < rows; r++) - for (int c = 0; c < cols; c++) - cells[r, c] = new Container { RelativeSizeAxes = Axes.Both }; - - testContainer.Content = cells.ToJagged(); - } - - protected Container Cell(int index) => (Container)cells[index / Cols, index % Cols]; - protected Container Cell(int row, int col) => (Container)cells[row, col]; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Framework.Testing +{ + /// + /// An abstract test case which exposes small cells arranged in a grid. + /// Useful for displaying multiple configurations of a tested component at a glance. + /// + public abstract class GridTestCase : TestCase + { + private readonly Drawable[,] cells; + + /// + /// The amount of rows in the grid. + /// + protected readonly int Rows; + + /// + /// The amount of columns in the grid. + /// + protected readonly int Cols; + + /// + /// Constructs a grid test case with the given dimensions. + /// + protected GridTestCase(int rows, int cols) + { + Rows = rows; + Cols = cols; + + GridContainer testContainer; + Add(testContainer = new GridContainer { RelativeSizeAxes = Axes.Both }); + + cells = new Drawable[rows, cols]; + for (int r = 0; r < rows; r++) + for (int c = 0; c < cols; c++) + cells[r, c] = new Container { RelativeSizeAxes = Axes.Both }; + + testContainer.Content = cells.ToJagged(); + } + + protected Container Cell(int index) => (Container)cells[index / Cols, index % Cols]; + protected Container Cell(int row, int col) => (Container)cells[row, col]; + } +} diff --git a/osu.Framework/Testing/IDynamicallyCompile.cs b/osu.Framework/Testing/IDynamicallyCompile.cs index d41c7ee7e..023411c4f 100644 --- a/osu.Framework/Testing/IDynamicallyCompile.cs +++ b/osu.Framework/Testing/IDynamicallyCompile.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Framework.Testing -{ - /// - /// A class which can be recompiled at runtime to allow for rapid testing. - /// - public interface IDynamicallyCompile - { - /// - /// A list of types which may be edited and should be included during recompilation. - /// - IReadOnlyList RequiredTypes { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Testing +{ + /// + /// A class which can be recompiled at runtime to allow for rapid testing. + /// + public interface IDynamicallyCompile + { + /// + /// A list of types which may be edited and should be included during recompilation. + /// + IReadOnlyList RequiredTypes { get; } + } +} diff --git a/osu.Framework/Testing/Input/ManualInputManager.cs b/osu.Framework/Testing/Input/ManualInputManager.cs index 95dd251da..f92d6caa8 100644 --- a/osu.Framework/Testing/Input/ManualInputManager.cs +++ b/osu.Framework/Testing/Input/ManualInputManager.cs @@ -1,70 +1,70 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Graphics; -using osu.Framework.Input; -using osu.Framework.Input.Handlers; -using osu.Framework.Platform; -using OpenTK; -using OpenTK.Input; -using MouseState = osu.Framework.Input.MouseState; - -namespace osu.Framework.Testing.Input -{ - public class ManualInputManager : PassThroughInputManager - { - private readonly ManualInputHandler handler; - - public ManualInputManager() - { - UseParentState = true; - AddHandler(handler = new ManualInputHandler()); - } - - public void MoveMouseTo(Drawable drawable) - { - UseParentState = false; - MoveMouseTo(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre)); - } - - public void MoveMouseTo(Vector2 position) - { - UseParentState = false; - handler.MoveMouseTo(position); - } - - public void Click(MouseButton button) - { - UseParentState = false; - handler.Click(button); - } - - private class ManualInputHandler : InputHandler - { - private Vector2 lastMousePosition; - - public void MoveMouseTo(Vector2 position) - { - PendingStates.Enqueue(new InputState { Mouse = new MouseState { Position = position } }); - lastMousePosition = position; - } - - public void Click(MouseButton button) - { - var mouseState = new MouseState { Position = lastMousePosition }; - mouseState.SetPressed(button, true); - - PendingStates.Enqueue(new InputState { Mouse = mouseState }); - - mouseState = (MouseState)mouseState.Clone(); - mouseState.SetPressed(button, false); - - PendingStates.Enqueue(new InputState { Mouse = mouseState }); - } - - public override bool Initialize(GameHost host) => true; - public override bool IsActive => true; - public override int Priority => 0; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.Platform; +using OpenTK; +using OpenTK.Input; +using MouseState = osu.Framework.Input.MouseState; + +namespace osu.Framework.Testing.Input +{ + public class ManualInputManager : PassThroughInputManager + { + private readonly ManualInputHandler handler; + + public ManualInputManager() + { + UseParentState = true; + AddHandler(handler = new ManualInputHandler()); + } + + public void MoveMouseTo(Drawable drawable) + { + UseParentState = false; + MoveMouseTo(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre)); + } + + public void MoveMouseTo(Vector2 position) + { + UseParentState = false; + handler.MoveMouseTo(position); + } + + public void Click(MouseButton button) + { + UseParentState = false; + handler.Click(button); + } + + private class ManualInputHandler : InputHandler + { + private Vector2 lastMousePosition; + + public void MoveMouseTo(Vector2 position) + { + PendingStates.Enqueue(new InputState { Mouse = new MouseState { Position = position } }); + lastMousePosition = position; + } + + public void Click(MouseButton button) + { + var mouseState = new MouseState { Position = lastMousePosition }; + mouseState.SetPressed(button, true); + + PendingStates.Enqueue(new InputState { Mouse = mouseState }); + + mouseState = (MouseState)mouseState.Clone(); + mouseState.SetPressed(button, false); + + PendingStates.Enqueue(new InputState { Mouse = mouseState }); + } + + public override bool Initialize(GameHost host) => true; + public override bool IsActive => true; + public override int Priority => 0; + } + } +} diff --git a/osu.Framework/Testing/TestBrowser.cs b/osu.Framework/Testing/TestBrowser.cs index c077712fe..be663b78b 100644 --- a/osu.Framework/Testing/TestBrowser.cs +++ b/osu.Framework/Testing/TestBrowser.cs @@ -1,567 +1,567 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Testing.Drawables; -using osu.Framework.Timing; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Input; - -namespace osu.Framework.Testing -{ - public class TestBrowser : KeyBindingContainer, IKeyBindingHandler - { - public TestCase CurrentTest { get; private set; } - - private TextBox searchTextBox; - private SearchContainer leftFlowContainer; - private Container testContentContainer; - private Container compilingNotice; - - public readonly List TestTypes = new List(); - - private ConfigManager config; - - private DynamicClassCompiler backgroundCompiler; - - private bool interactive; - - private readonly List assemblies; - - /// - /// Creates a new TestBrowser that displays the TestCases of every assembly that start with either "osu" or the specified namespace (if it isn't null) - /// - /// Assembly prefix which is used to match assemblies whose tests should be displayed - public TestBrowser(string assemblyNamespace = null) - { - assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(n => n.FullName.StartsWith("osu") || assemblyNamespace != null && n.FullName.StartsWith(assemblyNamespace)).ToList(); - - //we want to build the lists here because we're interested in the assembly we were *created* on. - foreach (Assembly asm in assemblies.ToList()) - { - var tests = asm.GetLoadableTypes().Where(t => t.IsSubclassOf(typeof(TestCase)) && !t.IsAbstract && t.IsPublic).ToList(); - - if (!tests.Any()) - { - assemblies.Remove(asm); - continue; - } - - foreach (Type type in tests) - TestTypes.Add(type); - } - - TestTypes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - } - - private void updateList(Assembly asm) - { - leftFlowContainer.Clear(); - //Add buttons for each TestCase. - leftFlowContainer.AddRange(TestTypes.Where(t => t.Assembly == asm).Select(t => new TestCaseButton(t) { Action = () => LoadTest(t) })); - } - - private BindableDouble rateBindable; - - private Toolbar toolbar; - private Container leftContainer; - private Container mainContainer; - - private const float test_list_width = 200; - - private Action exit; - - private Bindable showLogOverlay; - - [BackgroundDependencyLoader] - private void load(Storage storage, GameHost host, FrameworkConfigManager frameworkConfig) - { - interactive = host.Window != null; - config = new TestBrowserConfig(storage); - - exit = host.Exit; - - showLogOverlay = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay); - - rateBindable = new BindableDouble(1) - { - MinValue = 0, - MaxValue = 2, - }; - - var rateAdjustClock = new StopwatchClock(true); - var framedClock = new FramedClock(rateAdjustClock); - - Children = new Drawable[] - { - leftContainer = new Container - { - RelativeSizeAxes = Axes.Y, - Size = new Vector2(test_list_width, 1), - Children = new Drawable[] - { - new Box - { - Colour = Color4.DimGray, - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - searchTextBox = new TextBox - { - OnCommit = delegate - { - var firstVisible = leftFlowContainer.FirstOrDefault(b => b.IsPresent); - if (firstVisible != null) - LoadTest(firstVisible.TestType); - }, - Height = 20, - RelativeSizeAxes = Axes.X, - PlaceholderText = "type to search" - }, - new ScrollContainer - { - Padding = new MarginPadding { Top = 3, Bottom = 20 }, - RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = leftFlowContainer = new SearchContainer - { - Padding = new MarginPadding(3), - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - } - } - } - } - } - }, - mainContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = test_list_width }, - Children = new Drawable[] - { - toolbar = new Toolbar - { - RelativeSizeAxes = Axes.X, - Height = 50, - Depth = -1, - }, - testContentContainer = new Container - { - Clock = framedClock, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 50 }, - Child = compilingNotice = new Container - { - Alpha = 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Depth = float.MinValue, - CornerRadius = 5, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - new SpriteText - { - TextSize = 30, - Text = @"Compiling new version..." - } - }, - } - } - } - } - }; - - searchTextBox.Current.ValueChanged += newValue => leftFlowContainer.SearchTerm = newValue; - - backgroundCompiler = new DynamicClassCompiler - { - CompilationStarted = compileStarted, - CompilationFinished = compileFinished, - CompilationFailed = compileFailed - }; - try - { - backgroundCompiler.Start(); - } - catch - { - //it's okay for this to fail for now. - } - - foreach (Assembly asm in assemblies) - toolbar.AssemblyDropdown.AddDropdownItem(asm.GetName().Name, asm); - - toolbar.AssemblyDropdown.Current.ValueChanged += updateList; - toolbar.RunAllSteps.Current.ValueChanged += v => runTests(null); - toolbar.RateAdjustSlider.Current.BindTo(rateBindable); - - rateBindable.ValueChanged += v => rateAdjustClock.Rate = v; - rateBindable.TriggerChange(); - } - - private void compileStarted() => Schedule(() => - { - compilingNotice.Show(); - compilingNotice.FadeColour(Color4.White); - }); - - private void compileFailed(Exception ex) => Schedule(() => { - showLogOverlay.Value = true; - Logger.Error(ex, "Error with dynamic compilation!"); - - compilingNotice.FadeIn(100, Easing.OutQuint).Then().FadeOut(800, Easing.InQuint); - compilingNotice.FadeColour(Color4.Red, 100); - }); - - private void compileFinished(Type newType) => Schedule(() => - { - compilingNotice.FadeOut(800, Easing.InQuint); - compilingNotice.FadeColour(Color4.YellowGreen, 100); - - int i = TestTypes.FindIndex(t => t.Name == newType.Name && t.Assembly.GetName().Name == newType.Assembly.GetName().Name); - - if (i < 0) - TestTypes.Add(newType); - else - TestTypes[i] = newType; - - try - { - LoadTest(newType, isDynamicLoad: true); - } - catch (Exception e) - { - compileFailed(e); - } - }); - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (CurrentTest == null) - LoadTest(TestTypes.Find(t => t.Name == config.Get(TestBrowserSetting.LastTest))); - } - - private void toggleTestList() - { - if (leftContainer.Width > 0) - { - leftContainer.Width = 0; - mainContainer.Padding = new MarginPadding(); - } - else - { - leftContainer.Width = test_list_width; - mainContainer.Padding = new MarginPadding { Left = test_list_width }; - } - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (!args.Repeat) - { - switch (args.Key) - { - case Key.Escape: - exit(); - return true; - } - } - - return base.OnKeyDown(state, args); - } - - public override IEnumerable DefaultKeyBindings => new[] - { - new KeyBinding(new [] {InputKey.Control, InputKey.F},TestBrowserAction.Search), - new KeyBinding(new [] {InputKey.Control, InputKey.R},TestBrowserAction.Reload), // for macOS - - new KeyBinding(new [] {InputKey.Super, InputKey.F},TestBrowserAction.Search), // for macOS - new KeyBinding(new [] {InputKey.Super, InputKey.R},TestBrowserAction.Reload), // for macOS - - new KeyBinding(new [] {InputKey.Control, InputKey.H},TestBrowserAction.ToggleTestList), - }; - - public bool OnPressed(TestBrowserAction action) - { - switch (action) - { - case TestBrowserAction.Search: - if (leftContainer.Width == 0) toggleTestList(); - GetContainingInputManager().ChangeFocus(searchTextBox); - return true; - case TestBrowserAction.Reload: - LoadTest(CurrentTest.GetType()); - return true; - case TestBrowserAction.ToggleTestList: - toggleTestList(); - return true; - } - - return false; - } - - public bool OnReleased(TestBrowserAction action) => false; - - public void LoadTest(int testIndex) => LoadTest(TestTypes[testIndex]); - - public void LoadTest(Type testType = null, Action onCompletion = null, bool isDynamicLoad = false) - { - if (testType == null && TestTypes.Count > 0) - testType = TestTypes[0]; - - config.Set(TestBrowserSetting.LastTest, testType?.Name ?? string.Empty); - - var lastTest = CurrentTest; - - if (testType == null) - return; - - var newTest = (TestCase)Activator.CreateInstance(testType); - - var dropdown = toolbar.AssemblyDropdown; - - const string dynamic = "dynamic"; - - dropdown.RemoveDropdownItem(dropdown.Items.LastOrDefault(i => i.Value.FullName.Contains(dynamic)).Value); - - // if we are a dynamically compiled type (via DynamicClassCompiler) we should update the dropdown accordingly. - if (isDynamicLoad) - dropdown.AddDropdownItem($"{dynamic} ({testType.Name})", testType.Assembly); - else - TestTypes.RemoveAll(t => t.Assembly.FullName.Contains(dynamic)); - - dropdown.Current.Value = testType.Assembly; - - CurrentTest = newTest; - - updateButtons(); - - testContentContainer.Add(new ErrorCatchingDelayedLoadWrapper(CurrentTest, isDynamicLoad) - { - OnCaughtError = compileFailed - }); - - newTest.OnLoadComplete = d => Schedule(() => - { - if (lastTest?.Parent != null) - { - testContentContainer.Remove(lastTest.Parent); - lastTest.Clear(); - } - - if (CurrentTest != newTest) - { - // There could have been multiple loads fired after us. In such a case we want to silently remove ourselves. - testContentContainer.Remove(newTest.Parent); - return; - } - - updateButtons(); - - var methods = testType.GetMethods(); - - var setUpMethod = methods.FirstOrDefault(m => m.GetCustomAttributes(typeof(SetUpAttribute), false).Length > 0); - - foreach (var m in methods.Where(m => m.Name != "TestConstructor" && m.GetCustomAttributes(typeof(TestAttribute), false).Length > 0)) - { - var step = CurrentTest.AddStep(m.Name, () => { setUpMethod?.Invoke(CurrentTest, null); }); - step.LightColour = Color4.Teal; - m.Invoke(CurrentTest, null); - } - - backgroundCompiler.Checkpoint(CurrentTest); - runTests(onCompletion); - updateButtons(); - }); - } - - private void runTests(Action onCompletion) - { - if (!interactive || toolbar.RunAllSteps.Current) - CurrentTest.RunAllSteps(onCompletion, e => Logger.Log($@"Error on step: {e}")); - else - CurrentTest.RunFirstStep(); - } - - private void updateButtons() - { - foreach (var b in leftFlowContainer.Children) - b.Current = b.TestType.Name == CurrentTest?.GetType().Name; - } - - private class ErrorCatchingDelayedLoadWrapper : DelayedLoadWrapper - { - private readonly bool catchErrors; - - public Action OnCaughtError; - - public ErrorCatchingDelayedLoadWrapper(Drawable content, bool catchErrors) - : base(content, 0) - { - this.catchErrors = catchErrors; - } - - public override bool UpdateSubTree() - { - try - { - return base.UpdateSubTree(); - } - catch (Exception e) - { - if (!catchErrors) - throw; - - OnCaughtError?.Invoke(e); - RemoveInternal(Content); - } - - return false; - } - } - - private class Toolbar : CompositeDrawable - { - public BasicSliderBar RateAdjustSlider; - - public BasicDropdown AssemblyDropdown; - - public BasicCheckbox RunAllSteps; - - [BackgroundDependencyLoader] - private void load() - { - SpriteText playbackSpeedDisplay; - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - new Container - { - Padding = new MarginPadding(10), - RelativeSizeAxes = Axes.Both, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Distributed), - }, - Content = new[] - { - new Drawable[] - { - new FillFlowContainer - { - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Children = new Drawable[] - { - new SpriteText - { - Padding = new MarginPadding(5), - Text = "Current Assembly:" - }, - AssemblyDropdown = new BasicDropdown - { - Width = 300, - }, - RunAllSteps = new BasicCheckbox - { - LabelText = "Run all steps", - AutoSizeAxes = Axes.Y, - Width = 140, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new SpriteText - { - Padding = new MarginPadding(5), - Text = "Rate:" - }, - RateAdjustSlider = new BasicSliderBar - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.MediumPurple, - SelectionColor = Color4.White, - }, - playbackSpeedDisplay = new SpriteText - { - Padding = new MarginPadding(5), - }, - } - } - } - } - }, - }, - }, - }; - - RateAdjustSlider.Current.ValueChanged += v => playbackSpeedDisplay.Text = v.ToString("0%"); - } - } - } - - public enum TestBrowserAction - { - ToggleTestList, - Reload, - Search - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing.Drawables; +using osu.Framework.Timing; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; + +namespace osu.Framework.Testing +{ + public class TestBrowser : KeyBindingContainer, IKeyBindingHandler + { + public TestCase CurrentTest { get; private set; } + + private TextBox searchTextBox; + private SearchContainer leftFlowContainer; + private Container testContentContainer; + private Container compilingNotice; + + public readonly List TestTypes = new List(); + + private ConfigManager config; + + private DynamicClassCompiler backgroundCompiler; + + private bool interactive; + + private readonly List assemblies; + + /// + /// Creates a new TestBrowser that displays the TestCases of every assembly that start with either "osu" or the specified namespace (if it isn't null) + /// + /// Assembly prefix which is used to match assemblies whose tests should be displayed + public TestBrowser(string assemblyNamespace = null) + { + assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(n => n.FullName.StartsWith("osu") || assemblyNamespace != null && n.FullName.StartsWith(assemblyNamespace)).ToList(); + + //we want to build the lists here because we're interested in the assembly we were *created* on. + foreach (Assembly asm in assemblies.ToList()) + { + var tests = asm.GetLoadableTypes().Where(t => t.IsSubclassOf(typeof(TestCase)) && !t.IsAbstract && t.IsPublic).ToList(); + + if (!tests.Any()) + { + assemblies.Remove(asm); + continue; + } + + foreach (Type type in tests) + TestTypes.Add(type); + } + + TestTypes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + } + + private void updateList(Assembly asm) + { + leftFlowContainer.Clear(); + //Add buttons for each TestCase. + leftFlowContainer.AddRange(TestTypes.Where(t => t.Assembly == asm).Select(t => new TestCaseButton(t) { Action = () => LoadTest(t) })); + } + + private BindableDouble rateBindable; + + private Toolbar toolbar; + private Container leftContainer; + private Container mainContainer; + + private const float test_list_width = 200; + + private Action exit; + + private Bindable showLogOverlay; + + [BackgroundDependencyLoader] + private void load(Storage storage, GameHost host, FrameworkConfigManager frameworkConfig) + { + interactive = host.Window != null; + config = new TestBrowserConfig(storage); + + exit = host.Exit; + + showLogOverlay = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay); + + rateBindable = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 2, + }; + + var rateAdjustClock = new StopwatchClock(true); + var framedClock = new FramedClock(rateAdjustClock); + + Children = new Drawable[] + { + leftContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(test_list_width, 1), + Children = new Drawable[] + { + new Box + { + Colour = Color4.DimGray, + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + searchTextBox = new TextBox + { + OnCommit = delegate + { + var firstVisible = leftFlowContainer.FirstOrDefault(b => b.IsPresent); + if (firstVisible != null) + LoadTest(firstVisible.TestType); + }, + Height = 20, + RelativeSizeAxes = Axes.X, + PlaceholderText = "type to search" + }, + new ScrollContainer + { + Padding = new MarginPadding { Top = 3, Bottom = 20 }, + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = leftFlowContainer = new SearchContainer + { + Padding = new MarginPadding(3), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + } + } + } + } + } + }, + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = test_list_width }, + Children = new Drawable[] + { + toolbar = new Toolbar + { + RelativeSizeAxes = Axes.X, + Height = 50, + Depth = -1, + }, + testContentContainer = new Container + { + Clock = framedClock, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 50 }, + Child = compilingNotice = new Container + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + Depth = float.MinValue, + CornerRadius = 5, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + new SpriteText + { + TextSize = 30, + Text = @"Compiling new version..." + } + }, + } + } + } + } + }; + + searchTextBox.Current.ValueChanged += newValue => leftFlowContainer.SearchTerm = newValue; + + backgroundCompiler = new DynamicClassCompiler + { + CompilationStarted = compileStarted, + CompilationFinished = compileFinished, + CompilationFailed = compileFailed + }; + try + { + backgroundCompiler.Start(); + } + catch + { + //it's okay for this to fail for now. + } + + foreach (Assembly asm in assemblies) + toolbar.AssemblyDropdown.AddDropdownItem(asm.GetName().Name, asm); + + toolbar.AssemblyDropdown.Current.ValueChanged += updateList; + toolbar.RunAllSteps.Current.ValueChanged += v => runTests(null); + toolbar.RateAdjustSlider.Current.BindTo(rateBindable); + + rateBindable.ValueChanged += v => rateAdjustClock.Rate = v; + rateBindable.TriggerChange(); + } + + private void compileStarted() => Schedule(() => + { + compilingNotice.Show(); + compilingNotice.FadeColour(Color4.White); + }); + + private void compileFailed(Exception ex) => Schedule(() => { + showLogOverlay.Value = true; + Logger.Error(ex, "Error with dynamic compilation!"); + + compilingNotice.FadeIn(100, Easing.OutQuint).Then().FadeOut(800, Easing.InQuint); + compilingNotice.FadeColour(Color4.Red, 100); + }); + + private void compileFinished(Type newType) => Schedule(() => + { + compilingNotice.FadeOut(800, Easing.InQuint); + compilingNotice.FadeColour(Color4.YellowGreen, 100); + + int i = TestTypes.FindIndex(t => t.Name == newType.Name && t.Assembly.GetName().Name == newType.Assembly.GetName().Name); + + if (i < 0) + TestTypes.Add(newType); + else + TestTypes[i] = newType; + + try + { + LoadTest(newType, isDynamicLoad: true); + } + catch (Exception e) + { + compileFailed(e); + } + }); + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (CurrentTest == null) + LoadTest(TestTypes.Find(t => t.Name == config.Get(TestBrowserSetting.LastTest))); + } + + private void toggleTestList() + { + if (leftContainer.Width > 0) + { + leftContainer.Width = 0; + mainContainer.Padding = new MarginPadding(); + } + else + { + leftContainer.Width = test_list_width; + mainContainer.Padding = new MarginPadding { Left = test_list_width }; + } + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!args.Repeat) + { + switch (args.Key) + { + case Key.Escape: + exit(); + return true; + } + } + + return base.OnKeyDown(state, args); + } + + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(new [] {InputKey.Control, InputKey.F},TestBrowserAction.Search), + new KeyBinding(new [] {InputKey.Control, InputKey.R},TestBrowserAction.Reload), // for macOS + + new KeyBinding(new [] {InputKey.Super, InputKey.F},TestBrowserAction.Search), // for macOS + new KeyBinding(new [] {InputKey.Super, InputKey.R},TestBrowserAction.Reload), // for macOS + + new KeyBinding(new [] {InputKey.Control, InputKey.H},TestBrowserAction.ToggleTestList), + }; + + public bool OnPressed(TestBrowserAction action) + { + switch (action) + { + case TestBrowserAction.Search: + if (leftContainer.Width == 0) toggleTestList(); + GetContainingInputManager().ChangeFocus(searchTextBox); + return true; + case TestBrowserAction.Reload: + LoadTest(CurrentTest.GetType()); + return true; + case TestBrowserAction.ToggleTestList: + toggleTestList(); + return true; + } + + return false; + } + + public bool OnReleased(TestBrowserAction action) => false; + + public void LoadTest(int testIndex) => LoadTest(TestTypes[testIndex]); + + public void LoadTest(Type testType = null, Action onCompletion = null, bool isDynamicLoad = false) + { + if (testType == null && TestTypes.Count > 0) + testType = TestTypes[0]; + + config.Set(TestBrowserSetting.LastTest, testType?.Name ?? string.Empty); + + var lastTest = CurrentTest; + + if (testType == null) + return; + + var newTest = (TestCase)Activator.CreateInstance(testType); + + var dropdown = toolbar.AssemblyDropdown; + + const string dynamic = "dynamic"; + + dropdown.RemoveDropdownItem(dropdown.Items.LastOrDefault(i => i.Value.FullName.Contains(dynamic)).Value); + + // if we are a dynamically compiled type (via DynamicClassCompiler) we should update the dropdown accordingly. + if (isDynamicLoad) + dropdown.AddDropdownItem($"{dynamic} ({testType.Name})", testType.Assembly); + else + TestTypes.RemoveAll(t => t.Assembly.FullName.Contains(dynamic)); + + dropdown.Current.Value = testType.Assembly; + + CurrentTest = newTest; + + updateButtons(); + + testContentContainer.Add(new ErrorCatchingDelayedLoadWrapper(CurrentTest, isDynamicLoad) + { + OnCaughtError = compileFailed + }); + + newTest.OnLoadComplete = d => Schedule(() => + { + if (lastTest?.Parent != null) + { + testContentContainer.Remove(lastTest.Parent); + lastTest.Clear(); + } + + if (CurrentTest != newTest) + { + // There could have been multiple loads fired after us. In such a case we want to silently remove ourselves. + testContentContainer.Remove(newTest.Parent); + return; + } + + updateButtons(); + + var methods = testType.GetMethods(); + + var setUpMethod = methods.FirstOrDefault(m => m.GetCustomAttributes(typeof(SetUpAttribute), false).Length > 0); + + foreach (var m in methods.Where(m => m.Name != "TestConstructor" && m.GetCustomAttributes(typeof(TestAttribute), false).Length > 0)) + { + var step = CurrentTest.AddStep(m.Name, () => { setUpMethod?.Invoke(CurrentTest, null); }); + step.LightColour = Color4.Teal; + m.Invoke(CurrentTest, null); + } + + backgroundCompiler.Checkpoint(CurrentTest); + runTests(onCompletion); + updateButtons(); + }); + } + + private void runTests(Action onCompletion) + { + if (!interactive || toolbar.RunAllSteps.Current) + CurrentTest.RunAllSteps(onCompletion, e => Logger.Log($@"Error on step: {e}")); + else + CurrentTest.RunFirstStep(); + } + + private void updateButtons() + { + foreach (var b in leftFlowContainer.Children) + b.Current = b.TestType.Name == CurrentTest?.GetType().Name; + } + + private class ErrorCatchingDelayedLoadWrapper : DelayedLoadWrapper + { + private readonly bool catchErrors; + + public Action OnCaughtError; + + public ErrorCatchingDelayedLoadWrapper(Drawable content, bool catchErrors) + : base(content, 0) + { + this.catchErrors = catchErrors; + } + + public override bool UpdateSubTree() + { + try + { + return base.UpdateSubTree(); + } + catch (Exception e) + { + if (!catchErrors) + throw; + + OnCaughtError?.Invoke(e); + RemoveInternal(Content); + } + + return false; + } + } + + private class Toolbar : CompositeDrawable + { + public BasicSliderBar RateAdjustSlider; + + public BasicDropdown AssemblyDropdown; + + public BasicCheckbox RunAllSteps; + + [BackgroundDependencyLoader] + private void load() + { + SpriteText playbackSpeedDisplay; + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + new Container + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed), + }, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new SpriteText + { + Padding = new MarginPadding(5), + Text = "Current Assembly:" + }, + AssemblyDropdown = new BasicDropdown + { + Width = 300, + }, + RunAllSteps = new BasicCheckbox + { + LabelText = "Run all steps", + AutoSizeAxes = Axes.Y, + Width = 140, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new SpriteText + { + Padding = new MarginPadding(5), + Text = "Rate:" + }, + RateAdjustSlider = new BasicSliderBar + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.MediumPurple, + SelectionColor = Color4.White, + }, + playbackSpeedDisplay = new SpriteText + { + Padding = new MarginPadding(5), + }, + } + } + } + } + }, + }, + }, + }; + + RateAdjustSlider.Current.ValueChanged += v => playbackSpeedDisplay.Text = v.ToString("0%"); + } + } + } + + public enum TestBrowserAction + { + ToggleTestList, + Reload, + Search + } +} diff --git a/osu.Framework/Testing/TestBrowserConfig.cs b/osu.Framework/Testing/TestBrowserConfig.cs index a6cb71cd3..72068514a 100644 --- a/osu.Framework/Testing/TestBrowserConfig.cs +++ b/osu.Framework/Testing/TestBrowserConfig.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Platform; - -namespace osu.Framework.Testing -{ - internal class TestBrowserConfig : IniConfigManager - { - protected override string Filename => @"visualtests.cfg"; - - public TestBrowserConfig(Storage storage) : base(storage) - { - } - - protected override void InitialiseDefaults() - { - base.InitialiseDefaults(); - Set(TestBrowserSetting.LastTest, string.Empty); - } - } - - internal enum TestBrowserSetting - { - LastTest, - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Platform; + +namespace osu.Framework.Testing +{ + internal class TestBrowserConfig : IniConfigManager + { + protected override string Filename => @"visualtests.cfg"; + + public TestBrowserConfig(Storage storage) : base(storage) + { + } + + protected override void InitialiseDefaults() + { + base.InitialiseDefaults(); + Set(TestBrowserSetting.LastTest, string.Empty); + } + } + + internal enum TestBrowserSetting + { + LastTest, + } +} diff --git a/osu.Framework/Testing/TestBrowserTestRunner.cs b/osu.Framework/Testing/TestBrowserTestRunner.cs index 0fd562b4e..0b7882adc 100644 --- a/osu.Framework/Testing/TestBrowserTestRunner.cs +++ b/osu.Framework/Testing/TestBrowserTestRunner.cs @@ -1,80 +1,80 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Framework.Platform; - -namespace osu.Framework.Testing -{ - public class TestBrowserTestRunner : CompositeDrawable - { - private const double time_between_tests = 200; - - private Bindable volume; - private double volumeAtStartup; - - public TestBrowserTestRunner(TestBrowser browser) - { - this.browser = browser; - } - - [BackgroundDependencyLoader] - private void load(GameHost host, FrameworkConfigManager config) - { - this.host = host; - - volume = config.GetBindable(FrameworkSetting.VolumeUniversal); - volumeAtStartup = volume.Value; - volume.Value = 0; - } - - protected override void Dispose(bool isDisposing) - { - volume.Value = volumeAtStartup; - base.Dispose(isDisposing); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - host.MaximumDrawHz = int.MaxValue; - host.MaximumUpdateHz = int.MaxValue; - host.MaximumInactiveHz = int.MaxValue; - - AddInternal(browser); - - Console.WriteLine($@"{(int)Time.Current}: Running {browser.TestTypes.Count} visual test cases..."); - - runNext(); - } - - private int testIndex; - - private Type loadableTestType => testIndex >= 0 ? browser.TestTypes.ElementAtOrDefault(testIndex) : null; - - private readonly TestBrowser browser; - private GameHost host; - - private void runNext() - { - if (loadableTestType == null) - { - //we're done - Scheduler.AddDelayed(host.Exit, time_between_tests); - return; - } - - if (browser.CurrentTest?.GetType() != loadableTestType) - browser.LoadTest(loadableTestType, () => - { - testIndex++; - Scheduler.AddDelayed(runNext, time_between_tests); - }); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; + +namespace osu.Framework.Testing +{ + public class TestBrowserTestRunner : CompositeDrawable + { + private const double time_between_tests = 200; + + private Bindable volume; + private double volumeAtStartup; + + public TestBrowserTestRunner(TestBrowser browser) + { + this.browser = browser; + } + + [BackgroundDependencyLoader] + private void load(GameHost host, FrameworkConfigManager config) + { + this.host = host; + + volume = config.GetBindable(FrameworkSetting.VolumeUniversal); + volumeAtStartup = volume.Value; + volume.Value = 0; + } + + protected override void Dispose(bool isDisposing) + { + volume.Value = volumeAtStartup; + base.Dispose(isDisposing); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + host.MaximumDrawHz = int.MaxValue; + host.MaximumUpdateHz = int.MaxValue; + host.MaximumInactiveHz = int.MaxValue; + + AddInternal(browser); + + Console.WriteLine($@"{(int)Time.Current}: Running {browser.TestTypes.Count} visual test cases..."); + + runNext(); + } + + private int testIndex; + + private Type loadableTestType => testIndex >= 0 ? browser.TestTypes.ElementAtOrDefault(testIndex) : null; + + private readonly TestBrowser browser; + private GameHost host; + + private void runNext() + { + if (loadableTestType == null) + { + //we're done + Scheduler.AddDelayed(host.Exit, time_between_tests); + return; + } + + if (browser.CurrentTest?.GetType() != loadableTestType) + browser.LoadTest(loadableTestType, () => + { + testIndex++; + Scheduler.AddDelayed(runNext, time_between_tests); + }); + } + } +} diff --git a/osu.Framework/Testing/TestCase.cs b/osu.Framework/Testing/TestCase.cs index 52a9fc43b..c0f6b2920 100644 --- a/osu.Framework/Testing/TestCase.cs +++ b/osu.Framework/Testing/TestCase.cs @@ -1,288 +1,288 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Platform; -using osu.Framework.Testing.Drawables.Steps; -using osu.Framework.Threading; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Framework.Testing -{ - [TestFixture] - public abstract class TestCase : Container, IDynamicallyCompile - { - public readonly FillFlowContainer StepsContainer; - private readonly Container content; - - protected override Container Content => content; - - private bool mainTest = true; - - /// - /// Runs prior to all tests except to ensure that the - /// is reverted to a clean state for all tests. - /// - [SetUp] - public void SetupTest() - { - if (!mainTest) - StepsContainer.Clear(); - } - - /// - /// Ensures that the NUnit test runs correctly by running a . - /// This runs during NUnit's TearDown to ensure that steps (e.g. from ) - /// are properly added and executed. - /// - [TearDown] - public virtual void RunTest() - { - Storage storage; - using (var host = new HeadlessGameHost($"test-{Guid.NewGuid()}", realtime: false)) - { - storage = host.Storage; - host.Run(new TestCaseTestRunner(this)); - } - - try - { - // clean up after each run - storage.DeleteDirectory(string.Empty); - } - catch - { - } - } - - /// - /// Most derived usages of this start with TestCase. This will be removed for display purposes. - /// - private const string prefix = "TestCase"; - - /// - /// Tests any steps and assertions in the constructor of this . - /// This test must run before any other tests, as it relies on not being cleared and not having any elements. - /// - [Test, Order(int.MinValue)] - public void TestConstructor() { mainTest = false; } - - protected TestCase() - { - Name = GetType().ReadableName(); - - // Skip the "TestCase" prefix - if (Name.StartsWith(prefix)) Name = Name.Replace(prefix, string.Empty); - - RelativeSizeAxes = Axes.Both; - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = new Color4(25, 25, 25, 255), - RelativeSizeAxes = Axes.Y, - Width = steps_width, - }, - scroll = new ScrollContainer - { - Width = steps_width, - Depth = float.MinValue, - RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding(5), - Child = StepsContainer = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - }, - new Container - { - Masking = true, - Padding = new MarginPadding - { - Left = steps_width + padding, - Right = padding, - Top = padding, - Bottom = padding, - }, - RelativeSizeAxes = Axes.Both, - Child = content = new Container - { - Masking = true, - RelativeSizeAxes = Axes.Both - } - }, - }; - } - - private const float steps_width = 180; - private const float padding = 0; - - private int actionIndex; - private int actionRepetition; - private ScheduledDelegate stepRunner; - private readonly ScrollContainer scroll; - - public void RunAllSteps(Action onCompletion = null, Action onError = null) - { - stepRunner?.Cancel(); - foreach (var step in StepsContainer.OfType()) - step.Reset(); - - actionIndex = -1; - actionRepetition = 0; - runNextStep(onCompletion, onError); - } - - public void RunFirstStep() - { - stepRunner?.Cancel(); // Fixes RunAllSteps not working when toggled off - foreach (var step in StepsContainer.OfType()) - step.Reset(); - - actionIndex = 0; - try - { - loadableStep?.PerformStep(); - } - catch (Exception e) - { - Logging.Logger.Error(e, "Error on running first step"); - } - } - - private StepButton loadableStep => actionIndex >= 0 ? StepsContainer.Children.ElementAtOrDefault(actionIndex) as StepButton : null; - - protected virtual double TimePerAction => 200; - - private void runNextStep(Action onCompletion, Action onError) - { - try - { - if (loadableStep != null) - { - if (loadableStep.IsMaskedAway) - scroll.ScrollTo(loadableStep); - loadableStep.PerformStep(); - } - } - catch (Exception e) - { - onError?.Invoke(e); - return; - } - - string text = "."; - - if (actionRepetition == 0) - { - text = $"{(int)Time.Current}: ".PadLeft(7); - - if (actionIndex < 0) - text += $"{GetType().ReadableName()}"; - else - text += $"step {actionIndex + 1} {loadableStep?.ToString() ?? string.Empty}"; - } - - Console.Write(text); - - actionRepetition++; - - if (actionRepetition > (loadableStep?.RequiredRepetitions ?? 1) - 1) - { - Console.WriteLine(); - actionIndex++; - actionRepetition = 0; - } - - if (actionIndex > StepsContainer.Children.Count - 1) - { - onCompletion?.Invoke(); - return; - } - - if (Parent != null) - stepRunner = Scheduler.AddDelayed(() => runNextStep(onCompletion, onError), TimePerAction); - } - - public StepButton AddStep(string description, Action action) - { - var step = new SingleStepButton - { - Text = description, - Action = action - }; - - StepsContainer.Add(step); - - return step; - } - - protected void AddRepeatStep(string description, Action action, int invocationCount) - { - StepsContainer.Add(new RepeatStepButton(action, invocationCount) - { - Text = description, - }); - } - - protected void AddToggleStep(string description, Action action) - { - StepsContainer.Add(new ToggleStepButton(action) - { - Text = description - }); - } - - protected void AddUntilStep(Func waitUntilTrueDelegate, string description = null) - { - StepsContainer.Add(new UntilStepButton(waitUntilTrueDelegate) - { - Text = description ?? @"Until", - }); - } - - protected void AddWaitStep(int waitCount, string description = null) - { - StepsContainer.Add(new RepeatStepButton(() => { }, waitCount) - { - Text = description ?? @"Wait", - }); - } - - protected void AddSliderStep(string description, T min, T max, T start, Action valueChanged) - where T : struct, IComparable, IConvertible - { - StepsContainer.Add(new StepSlider(description, min, max, start) - { - ValueChanged = valueChanged, - }); - } - - protected void AddAssert(string description, Func assert, string extendedDescription = null) - { - StepsContainer.Add(new AssertButton - { - Text = description, - ExtendedDescription = extendedDescription, - CallStack = new StackTrace(1), - Assertion = assert, - }); - } - - public virtual IReadOnlyList RequiredTypes => new Type[] { }; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; +using osu.Framework.Testing.Drawables.Steps; +using osu.Framework.Threading; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Framework.Testing +{ + [TestFixture] + public abstract class TestCase : Container, IDynamicallyCompile + { + public readonly FillFlowContainer StepsContainer; + private readonly Container content; + + protected override Container Content => content; + + private bool mainTest = true; + + /// + /// Runs prior to all tests except to ensure that the + /// is reverted to a clean state for all tests. + /// + [SetUp] + public void SetupTest() + { + if (!mainTest) + StepsContainer.Clear(); + } + + /// + /// Ensures that the NUnit test runs correctly by running a . + /// This runs during NUnit's TearDown to ensure that steps (e.g. from ) + /// are properly added and executed. + /// + [TearDown] + public virtual void RunTest() + { + Storage storage; + using (var host = new HeadlessGameHost($"test-{Guid.NewGuid()}", realtime: false)) + { + storage = host.Storage; + host.Run(new TestCaseTestRunner(this)); + } + + try + { + // clean up after each run + storage.DeleteDirectory(string.Empty); + } + catch + { + } + } + + /// + /// Most derived usages of this start with TestCase. This will be removed for display purposes. + /// + private const string prefix = "TestCase"; + + /// + /// Tests any steps and assertions in the constructor of this . + /// This test must run before any other tests, as it relies on not being cleared and not having any elements. + /// + [Test, Order(int.MinValue)] + public void TestConstructor() { mainTest = false; } + + protected TestCase() + { + Name = GetType().ReadableName(); + + // Skip the "TestCase" prefix + if (Name.StartsWith(prefix)) Name = Name.Replace(prefix, string.Empty); + + RelativeSizeAxes = Axes.Both; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = new Color4(25, 25, 25, 255), + RelativeSizeAxes = Axes.Y, + Width = steps_width, + }, + scroll = new ScrollContainer + { + Width = steps_width, + Depth = float.MinValue, + RelativeSizeAxes = Axes.Y, + Padding = new MarginPadding(5), + Child = StepsContainer = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }, + new Container + { + Masking = true, + Padding = new MarginPadding + { + Left = steps_width + padding, + Right = padding, + Top = padding, + Bottom = padding, + }, + RelativeSizeAxes = Axes.Both, + Child = content = new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both + } + }, + }; + } + + private const float steps_width = 180; + private const float padding = 0; + + private int actionIndex; + private int actionRepetition; + private ScheduledDelegate stepRunner; + private readonly ScrollContainer scroll; + + public void RunAllSteps(Action onCompletion = null, Action onError = null) + { + stepRunner?.Cancel(); + foreach (var step in StepsContainer.OfType()) + step.Reset(); + + actionIndex = -1; + actionRepetition = 0; + runNextStep(onCompletion, onError); + } + + public void RunFirstStep() + { + stepRunner?.Cancel(); // Fixes RunAllSteps not working when toggled off + foreach (var step in StepsContainer.OfType()) + step.Reset(); + + actionIndex = 0; + try + { + loadableStep?.PerformStep(); + } + catch (Exception e) + { + Logging.Logger.Error(e, "Error on running first step"); + } + } + + private StepButton loadableStep => actionIndex >= 0 ? StepsContainer.Children.ElementAtOrDefault(actionIndex) as StepButton : null; + + protected virtual double TimePerAction => 200; + + private void runNextStep(Action onCompletion, Action onError) + { + try + { + if (loadableStep != null) + { + if (loadableStep.IsMaskedAway) + scroll.ScrollTo(loadableStep); + loadableStep.PerformStep(); + } + } + catch (Exception e) + { + onError?.Invoke(e); + return; + } + + string text = "."; + + if (actionRepetition == 0) + { + text = $"{(int)Time.Current}: ".PadLeft(7); + + if (actionIndex < 0) + text += $"{GetType().ReadableName()}"; + else + text += $"step {actionIndex + 1} {loadableStep?.ToString() ?? string.Empty}"; + } + + Console.Write(text); + + actionRepetition++; + + if (actionRepetition > (loadableStep?.RequiredRepetitions ?? 1) - 1) + { + Console.WriteLine(); + actionIndex++; + actionRepetition = 0; + } + + if (actionIndex > StepsContainer.Children.Count - 1) + { + onCompletion?.Invoke(); + return; + } + + if (Parent != null) + stepRunner = Scheduler.AddDelayed(() => runNextStep(onCompletion, onError), TimePerAction); + } + + public StepButton AddStep(string description, Action action) + { + var step = new SingleStepButton + { + Text = description, + Action = action + }; + + StepsContainer.Add(step); + + return step; + } + + protected void AddRepeatStep(string description, Action action, int invocationCount) + { + StepsContainer.Add(new RepeatStepButton(action, invocationCount) + { + Text = description, + }); + } + + protected void AddToggleStep(string description, Action action) + { + StepsContainer.Add(new ToggleStepButton(action) + { + Text = description + }); + } + + protected void AddUntilStep(Func waitUntilTrueDelegate, string description = null) + { + StepsContainer.Add(new UntilStepButton(waitUntilTrueDelegate) + { + Text = description ?? @"Until", + }); + } + + protected void AddWaitStep(int waitCount, string description = null) + { + StepsContainer.Add(new RepeatStepButton(() => { }, waitCount) + { + Text = description ?? @"Wait", + }); + } + + protected void AddSliderStep(string description, T min, T max, T start, Action valueChanged) + where T : struct, IComparable, IConvertible + { + StepsContainer.Add(new StepSlider(description, min, max, start) + { + ValueChanged = valueChanged, + }); + } + + protected void AddAssert(string description, Func assert, string extendedDescription = null) + { + StepsContainer.Add(new AssertButton + { + Text = description, + ExtendedDescription = extendedDescription, + CallStack = new StackTrace(1), + Assertion = assert, + }); + } + + public virtual IReadOnlyList RequiredTypes => new Type[] { }; + } +} diff --git a/osu.Framework/Testing/TestCaseTestRunner.cs b/osu.Framework/Testing/TestCaseTestRunner.cs index 47fa49030..c182fe161 100644 --- a/osu.Framework/Testing/TestCaseTestRunner.cs +++ b/osu.Framework/Testing/TestCaseTestRunner.cs @@ -1,81 +1,81 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Platform; -using osu.Framework.Screens; - -namespace osu.Framework.Testing -{ - public class TestCaseTestRunner : Game - { - public TestCaseTestRunner(TestCase testCase) - { - Add(new TestRunner(testCase)); - } - - public class TestRunner : Screen - { - private const double time_between_tests = 200; - - private Bindable volume; - private double volumeAtStartup; - - private readonly TestCase test; - private GameHost host; - - public TestRunner(TestCase test) - { - this.test = test; - } - - [BackgroundDependencyLoader] - private void load(GameHost host, FrameworkConfigManager config) - { - this.host = host; - - volume = config.GetBindable(FrameworkSetting.VolumeUniversal); - volumeAtStartup = volume.Value; - volume.Value = 0; - } - - protected override void Dispose(bool isDisposing) - { - volume.Value = volumeAtStartup; - base.Dispose(isDisposing); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - host.MaximumDrawHz = int.MaxValue; - host.MaximumUpdateHz = int.MaxValue; - host.MaximumInactiveHz = int.MaxValue; - - Add(test); - - Console.WriteLine($@"{(int)Time.Current}: Running {test} visual test cases..."); - - // Nunit will run the tests in the TestCase with the same TestCase instance so the TestCase - // needs to be removed before the host is exited, otherwise it will end up disposed - - test.RunAllSteps(() => - { - Scheduler.AddDelayed(() => - { - Remove(test); - host.Exit(); - }, time_between_tests); - }, e => - { - // Other tests may run even if this one failed, so the TestCase still needs to be removed - Remove(test); - throw new Exception("The test case threw an exception while running", e); - }); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Platform; +using osu.Framework.Screens; + +namespace osu.Framework.Testing +{ + public class TestCaseTestRunner : Game + { + public TestCaseTestRunner(TestCase testCase) + { + Add(new TestRunner(testCase)); + } + + public class TestRunner : Screen + { + private const double time_between_tests = 200; + + private Bindable volume; + private double volumeAtStartup; + + private readonly TestCase test; + private GameHost host; + + public TestRunner(TestCase test) + { + this.test = test; + } + + [BackgroundDependencyLoader] + private void load(GameHost host, FrameworkConfigManager config) + { + this.host = host; + + volume = config.GetBindable(FrameworkSetting.VolumeUniversal); + volumeAtStartup = volume.Value; + volume.Value = 0; + } + + protected override void Dispose(bool isDisposing) + { + volume.Value = volumeAtStartup; + base.Dispose(isDisposing); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + host.MaximumDrawHz = int.MaxValue; + host.MaximumUpdateHz = int.MaxValue; + host.MaximumInactiveHz = int.MaxValue; + + Add(test); + + Console.WriteLine($@"{(int)Time.Current}: Running {test} visual test cases..."); + + // Nunit will run the tests in the TestCase with the same TestCase instance so the TestCase + // needs to be removed before the host is exited, otherwise it will end up disposed + + test.RunAllSteps(() => + { + Scheduler.AddDelayed(() => + { + Remove(test); + host.Exit(); + }, time_between_tests); + }, e => + { + // Other tests may run even if this one failed, so the TestCase still needs to be removed + Remove(test); + throw new Exception("The test case threw an exception while running", e); + }); + } + } + } +} diff --git a/osu.Framework/Threading/AtomicCounter.cs b/osu.Framework/Threading/AtomicCounter.cs index cbacfcefd..7009793b9 100644 --- a/osu.Framework/Threading/AtomicCounter.cs +++ b/osu.Framework/Threading/AtomicCounter.cs @@ -1,34 +1,34 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System.Threading; - -namespace osu.Framework.Threading -{ - public class AtomicCounter - { - private long count; - - public long Increment() - { - return Interlocked.Increment(ref count); - } - - public long Add(long value) - { - return Interlocked.Add(ref count, value); - } - - public long Reset() - { - return Interlocked.Exchange(ref count, 0); - } - - public long Value - { - set { Interlocked.Exchange(ref count, value); } - - get { return Interlocked.Read(ref count); } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System.Threading; + +namespace osu.Framework.Threading +{ + public class AtomicCounter + { + private long count; + + public long Increment() + { + return Interlocked.Increment(ref count); + } + + public long Add(long value) + { + return Interlocked.Add(ref count, value); + } + + public long Reset() + { + return Interlocked.Exchange(ref count, 0); + } + + public long Value + { + set { Interlocked.Exchange(ref count, value); } + + get { return Interlocked.Read(ref count); } + } + } +} diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index c80a945dc..05c696c30 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -1,33 +1,33 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Threading -{ - public class AudioThread : GameThread - { - public AudioThread(Action onNewFrame) - : base(onNewFrame, "Audio") - { - } - - internal override IEnumerable StatisticsCounters => new[] - { - StatisticsCounterType.TasksRun, - StatisticsCounterType.Tracks, - StatisticsCounterType.Samples, - StatisticsCounterType.SChannels, - StatisticsCounterType.Components, - }; - - protected override void PerformExit() - { - base.PerformExit(); - - ManagedBass.Bass.Free(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Threading +{ + public class AudioThread : GameThread + { + public AudioThread(Action onNewFrame) + : base(onNewFrame, "Audio") + { + } + + internal override IEnumerable StatisticsCounters => new[] + { + StatisticsCounterType.TasksRun, + StatisticsCounterType.Tracks, + StatisticsCounterType.Samples, + StatisticsCounterType.SChannels, + StatisticsCounterType.Components, + }; + + protected override void PerformExit() + { + base.PerformExit(); + + ManagedBass.Bass.Free(); + } + } +} diff --git a/osu.Framework/Threading/DrawThread.cs b/osu.Framework/Threading/DrawThread.cs index f3e578935..c1976d02c 100644 --- a/osu.Framework/Threading/DrawThread.cs +++ b/osu.Framework/Threading/DrawThread.cs @@ -1,28 +1,28 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Threading -{ - public class DrawThread : GameThread - { - public DrawThread(Action onNewFrame) - : base(onNewFrame, "Draw") - { - } - - internal override IEnumerable StatisticsCounters => new[] - { - StatisticsCounterType.VBufBinds, - StatisticsCounterType.VBufOverflow, - StatisticsCounterType.TextureBinds, - StatisticsCounterType.DrawCalls, - StatisticsCounterType.VerticesDraw, - StatisticsCounterType.VerticesUpl, - StatisticsCounterType.Pixels, - }; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Threading +{ + public class DrawThread : GameThread + { + public DrawThread(Action onNewFrame) + : base(onNewFrame, "Draw") + { + } + + internal override IEnumerable StatisticsCounters => new[] + { + StatisticsCounterType.VBufBinds, + StatisticsCounterType.VBufOverflow, + StatisticsCounterType.TextureBinds, + StatisticsCounterType.DrawCalls, + StatisticsCounterType.VerticesDraw, + StatisticsCounterType.VerticesUpl, + StatisticsCounterType.Pixels, + }; + } +} diff --git a/osu.Framework/Threading/GameThread.cs b/osu.Framework/Threading/GameThread.cs index d39789e43..42ffb822a 100644 --- a/osu.Framework/Threading/GameThread.cs +++ b/osu.Framework/Threading/GameThread.cs @@ -1,147 +1,147 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Threading; -using osu.Framework.Statistics; -using osu.Framework.Timing; -using System.Collections.Generic; - -namespace osu.Framework.Threading -{ - public class GameThread - { - internal const double DEFAULT_ACTIVE_HZ = 1000; - internal const double DEFAULT_INACTIVE_HZ = 60; - - internal PerformanceMonitor Monitor { get; } - public ThrottledFrameClock Clock { get; } - public Thread Thread { get; } - public Scheduler Scheduler { get; } - - private readonly Action onNewFrame; - - private bool isActive = true; - - public bool IsActive - { - get { return isActive; } - set - { - isActive = value; - Clock.MaximumUpdateHz = isActive ? activeHz : inactiveHz; - } - } - - private double activeHz = DEFAULT_ACTIVE_HZ; - - public double ActiveHz - { - get { return activeHz; } - - set - { - activeHz = value; - if (IsActive) - Clock.MaximumUpdateHz = activeHz; - } - } - - private double inactiveHz = DEFAULT_INACTIVE_HZ; - - public double InactiveHz - { - get { return inactiveHz; } - - set - { - inactiveHz = value; - if (!IsActive) - Clock.MaximumUpdateHz = inactiveHz; - } - } - - public static string PrefixedThreadNameFor(string name) => $"{nameof(GameThread)}.{name}"; - - public bool Running => Thread.IsAlive; - - private readonly ManualResetEvent initializedEvent = new ManualResetEvent(false); - - public Action OnThreadStart; - - internal virtual IEnumerable StatisticsCounters => Array.Empty(); - - public readonly string Name; - - internal GameThread(Action onNewFrame, string name) - { - this.onNewFrame = onNewFrame; - - Thread = new Thread(runWork) - { - Name = PrefixedThreadNameFor(name), - IsBackground = true, - }; - - Name = name; - Clock = new ThrottledFrameClock(); - Monitor = new PerformanceMonitor(Clock, Thread, StatisticsCounters); - Scheduler = new Scheduler(null, Clock); - } - - public void WaitUntilInitialized() - { - initializedEvent.WaitOne(); - } - - private void runWork() - { - Scheduler.SetCurrentThread(); - - OnThreadStart?.Invoke(); - - initializedEvent.Set(); - - while (!exitCompleted) - ProcessFrame(); - } - - protected void ProcessFrame() - { - if (exitCompleted) - return; - - if (exitRequested) - { - PerformExit(); - exitCompleted = true; - return; - } - - Monitor.NewFrame(); - - using (Monitor.BeginCollecting(PerformanceCollectionType.Scheduler)) - Scheduler.Update(); - - using (Monitor.BeginCollecting(PerformanceCollectionType.Work)) - onNewFrame?.Invoke(); - - using (Monitor.BeginCollecting(PerformanceCollectionType.Sleep)) - Clock.ProcessFrame(); - } - - private volatile bool exitRequested; - private volatile bool exitCompleted; - - public bool Exited => exitCompleted; - - public void Exit() => exitRequested = true; - public void Start() => Thread?.Start(); - - protected virtual void PerformExit() - { - Monitor?.Dispose(); - initializedEvent?.Dispose(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Threading; +using osu.Framework.Statistics; +using osu.Framework.Timing; +using System.Collections.Generic; + +namespace osu.Framework.Threading +{ + public class GameThread + { + internal const double DEFAULT_ACTIVE_HZ = 1000; + internal const double DEFAULT_INACTIVE_HZ = 60; + + internal PerformanceMonitor Monitor { get; } + public ThrottledFrameClock Clock { get; } + public Thread Thread { get; } + public Scheduler Scheduler { get; } + + private readonly Action onNewFrame; + + private bool isActive = true; + + public bool IsActive + { + get { return isActive; } + set + { + isActive = value; + Clock.MaximumUpdateHz = isActive ? activeHz : inactiveHz; + } + } + + private double activeHz = DEFAULT_ACTIVE_HZ; + + public double ActiveHz + { + get { return activeHz; } + + set + { + activeHz = value; + if (IsActive) + Clock.MaximumUpdateHz = activeHz; + } + } + + private double inactiveHz = DEFAULT_INACTIVE_HZ; + + public double InactiveHz + { + get { return inactiveHz; } + + set + { + inactiveHz = value; + if (!IsActive) + Clock.MaximumUpdateHz = inactiveHz; + } + } + + public static string PrefixedThreadNameFor(string name) => $"{nameof(GameThread)}.{name}"; + + public bool Running => Thread.IsAlive; + + private readonly ManualResetEvent initializedEvent = new ManualResetEvent(false); + + public Action OnThreadStart; + + internal virtual IEnumerable StatisticsCounters => Array.Empty(); + + public readonly string Name; + + internal GameThread(Action onNewFrame, string name) + { + this.onNewFrame = onNewFrame; + + Thread = new Thread(runWork) + { + Name = PrefixedThreadNameFor(name), + IsBackground = true, + }; + + Name = name; + Clock = new ThrottledFrameClock(); + Monitor = new PerformanceMonitor(Clock, Thread, StatisticsCounters); + Scheduler = new Scheduler(null, Clock); + } + + public void WaitUntilInitialized() + { + initializedEvent.WaitOne(); + } + + private void runWork() + { + Scheduler.SetCurrentThread(); + + OnThreadStart?.Invoke(); + + initializedEvent.Set(); + + while (!exitCompleted) + ProcessFrame(); + } + + protected void ProcessFrame() + { + if (exitCompleted) + return; + + if (exitRequested) + { + PerformExit(); + exitCompleted = true; + return; + } + + Monitor.NewFrame(); + + using (Monitor.BeginCollecting(PerformanceCollectionType.Scheduler)) + Scheduler.Update(); + + using (Monitor.BeginCollecting(PerformanceCollectionType.Work)) + onNewFrame?.Invoke(); + + using (Monitor.BeginCollecting(PerformanceCollectionType.Sleep)) + Clock.ProcessFrame(); + } + + private volatile bool exitRequested; + private volatile bool exitCompleted; + + public bool Exited => exitCompleted; + + public void Exit() => exitRequested = true; + public void Start() => Thread?.Start(); + + protected virtual void PerformExit() + { + Monitor?.Dispose(); + initializedEvent?.Dispose(); + } + } +} diff --git a/osu.Framework/Threading/InputThread.cs b/osu.Framework/Threading/InputThread.cs index 262aa3b7f..79a20c9f5 100644 --- a/osu.Framework/Threading/InputThread.cs +++ b/osu.Framework/Threading/InputThread.cs @@ -1,25 +1,25 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Threading -{ - public class InputThread : GameThread - { - public InputThread(Action onNewFrame) - : base(onNewFrame, "Input") - { - } - - internal override IEnumerable StatisticsCounters => new[] - { - StatisticsCounterType.MouseEvents, - StatisticsCounterType.KeyEvents, - }; - - public void RunUpdate() => ProcessFrame(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Threading +{ + public class InputThread : GameThread + { + public InputThread(Action onNewFrame) + : base(onNewFrame, "Input") + { + } + + internal override IEnumerable StatisticsCounters => new[] + { + StatisticsCounterType.MouseEvents, + StatisticsCounterType.KeyEvents, + }; + + public void RunUpdate() => ProcessFrame(); + } +} diff --git a/osu.Framework/Threading/Scheduler.cs b/osu.Framework/Threading/Scheduler.cs index 8060a0144..eb468f7cb 100644 --- a/osu.Framework/Threading/Scheduler.cs +++ b/osu.Framework/Threading/Scheduler.cs @@ -1,301 +1,301 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using osu.Framework.Extensions; -using osu.Framework.Timing; - -namespace osu.Framework.Threading -{ - /// - /// Marshals delegates to run from the Scheduler's base thread in a threadsafe manner - /// - public class Scheduler - { - private readonly ConcurrentQueue schedulerQueue = new ConcurrentQueue(); - private readonly List timedTasks = new List(); - private readonly List perUpdateTasks = new List(); - private int mainThreadId; - - private IClock clock; - private double currentTime => clock?.CurrentTime ?? 0; - - /// - /// The base thread is assumed to be the the thread on which the constructor is run. - /// - public Scheduler() - { - SetCurrentThread(); - clock = new StopwatchClock(true); - } - - /// - /// The base thread is assumed to be the the thread on which the constructor is run. - /// - public Scheduler(Thread mainThread) - { - SetCurrentThread(mainThread); - clock = new StopwatchClock(true); - } - - /// - /// The base thread is assumed to be the the thread on which the constructor is run. - /// - public Scheduler(Thread mainThread, IClock clock) - { - SetCurrentThread(mainThread); - this.clock = clock; - } - - public void UpdateClock(IClock newClock) - { - if (newClock == null) - throw new NullReferenceException($"{nameof(newClock)} may not be null."); - - if (newClock == clock) - return; - - lock (timedTasks) - { - if (clock == null) - { - // This is the first time we will get a valid time, so assume this is the - // reference point everything scheduled so far starts from. - foreach (var s in timedTasks) - s.ExecutionTime += newClock.CurrentTime; - } - - clock = newClock; - } - } - - /// - /// Returns whether we are on the main thread or not. - /// - protected virtual bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId; - - - private readonly List tasksToSchedule = new List(); - private readonly List tasksToRemove = new List(); - - /// - /// Run any pending work tasks. - /// - /// true if any tasks were run. - public virtual int Update() - { - lock (timedTasks) - { - double currentTimeLocal = currentTime; - - if (timedTasks.Count > 0) - { - foreach (var sd in timedTasks) - { - if (sd.ExecutionTime <= currentTimeLocal) - { - tasksToRemove.Add(sd); - - if (sd.Cancelled) continue; - - schedulerQueue.Enqueue(sd.RunTask); - - if (sd.RepeatInterval >= 0) - { - if (timedTasks.Count > 1000) - throw new ArgumentException("Too many timed tasks are in the queue!"); - - sd.ExecutionTime += sd.RepeatInterval; - tasksToSchedule.Add(sd); - } - } - } - - foreach (var t in tasksToRemove) - timedTasks.Remove(t); - - tasksToRemove.Clear(); - - foreach (var t in tasksToSchedule) - timedTasks.AddInPlace(t); - - tasksToSchedule.Clear(); - } - } - - for (int i = 0; i < perUpdateTasks.Count; i++) - { - ScheduledDelegate task = perUpdateTasks[i]; - if (task.Cancelled) - { - perUpdateTasks.RemoveAt(i--); - continue; - } - - schedulerQueue.Enqueue(task.RunTask); - } - - int countRun = 0; - - while (schedulerQueue.TryDequeue(out Action action)) - { - //todo: error handling - action.Invoke(); - countRun++; - } - - return countRun; - } - - /// - /// Cancel any pending work tasks. - /// - public void CancelDelayedTasks() - { - lock (timedTasks) - { - foreach (var t in timedTasks) - t.Cancel(); - timedTasks.Clear(); - } - } - - internal void SetCurrentThread(Thread thread) - { - mainThreadId = thread?.ManagedThreadId ?? -1; - } - - internal void SetCurrentThread() - { - mainThreadId = Thread.CurrentThread.ManagedThreadId; - } - - /// - /// Add a task to be scheduled. - /// - /// The work to be done. - /// If set to false, the task will be executed immediately if we are on the main thread. - /// Whether we could run without scheduling - public bool Add(Action task, bool forceScheduled = true) - { - if (!forceScheduled && IsMainThread) - { - //We are on the main thread already - don't need to schedule. - task.Invoke(); - return true; - } - - schedulerQueue.Enqueue(task); - - return false; - } - - public void Add(ScheduledDelegate task) - { - lock (timedTasks) - { - if (task.RepeatInterval == 0) - perUpdateTasks.Add(task); - else - timedTasks.AddInPlace(task); - } - } - - /// - /// Add a task which will be run after a specified delay. - /// - /// The work to be done. - /// Milliseconds until run. - /// Whether this task should repeat. - public ScheduledDelegate AddDelayed(Action task, double timeUntilRun, bool repeat = false) - { - // We are locking here already to make sure we have no concurrent access to currentTime - lock (timedTasks) - { - ScheduledDelegate del = new ScheduledDelegate(task, currentTime + timeUntilRun, repeat ? timeUntilRun : -1); - Add(del); - return del; - } - } - - /// - /// Adds a task which will only be run once per frame, no matter how many times it was scheduled in the previous frame. - /// - /// The work to be done. - /// Whether this is the first queue attempt of this work. - public bool AddOnce(Action task) - { - if (schedulerQueue.Contains(task)) - return false; - - schedulerQueue.Enqueue(task); - - return true; - } - } - - public class ScheduledDelegate : IComparable - { - public ScheduledDelegate(Action task, double executionTime, double repeatInterval = -1) - { - ExecutionTime = executionTime; - RepeatInterval = repeatInterval; - this.task = task; - } - - /// - /// The work task. - /// - private readonly Action task; - - /// - /// Set to true to skip scheduled executions until we are ready. - /// - internal bool Waiting; - - public void Wait() - { - Waiting = true; - } - - public void Continue() - { - Waiting = false; - } - - public void RunTask() - { - if (!Waiting) - task(); - Completed = true; - } - - public bool Completed; - - public bool Cancelled { get; private set; } - - public void Cancel() - { - Cancelled = true; - } - - /// - /// The earliest ElapsedTime value at which we can be executed. - /// - public double ExecutionTime; - - /// - /// Time in milliseconds between repeats of this task. -1 means no repeats. - /// - public double RepeatInterval; - - public int CompareTo(ScheduledDelegate other) - { - return ExecutionTime == other.ExecutionTime ? -1 : ExecutionTime.CompareTo(other.ExecutionTime); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Framework.Extensions; +using osu.Framework.Timing; + +namespace osu.Framework.Threading +{ + /// + /// Marshals delegates to run from the Scheduler's base thread in a threadsafe manner + /// + public class Scheduler + { + private readonly ConcurrentQueue schedulerQueue = new ConcurrentQueue(); + private readonly List timedTasks = new List(); + private readonly List perUpdateTasks = new List(); + private int mainThreadId; + + private IClock clock; + private double currentTime => clock?.CurrentTime ?? 0; + + /// + /// The base thread is assumed to be the the thread on which the constructor is run. + /// + public Scheduler() + { + SetCurrentThread(); + clock = new StopwatchClock(true); + } + + /// + /// The base thread is assumed to be the the thread on which the constructor is run. + /// + public Scheduler(Thread mainThread) + { + SetCurrentThread(mainThread); + clock = new StopwatchClock(true); + } + + /// + /// The base thread is assumed to be the the thread on which the constructor is run. + /// + public Scheduler(Thread mainThread, IClock clock) + { + SetCurrentThread(mainThread); + this.clock = clock; + } + + public void UpdateClock(IClock newClock) + { + if (newClock == null) + throw new NullReferenceException($"{nameof(newClock)} may not be null."); + + if (newClock == clock) + return; + + lock (timedTasks) + { + if (clock == null) + { + // This is the first time we will get a valid time, so assume this is the + // reference point everything scheduled so far starts from. + foreach (var s in timedTasks) + s.ExecutionTime += newClock.CurrentTime; + } + + clock = newClock; + } + } + + /// + /// Returns whether we are on the main thread or not. + /// + protected virtual bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId; + + + private readonly List tasksToSchedule = new List(); + private readonly List tasksToRemove = new List(); + + /// + /// Run any pending work tasks. + /// + /// true if any tasks were run. + public virtual int Update() + { + lock (timedTasks) + { + double currentTimeLocal = currentTime; + + if (timedTasks.Count > 0) + { + foreach (var sd in timedTasks) + { + if (sd.ExecutionTime <= currentTimeLocal) + { + tasksToRemove.Add(sd); + + if (sd.Cancelled) continue; + + schedulerQueue.Enqueue(sd.RunTask); + + if (sd.RepeatInterval >= 0) + { + if (timedTasks.Count > 1000) + throw new ArgumentException("Too many timed tasks are in the queue!"); + + sd.ExecutionTime += sd.RepeatInterval; + tasksToSchedule.Add(sd); + } + } + } + + foreach (var t in tasksToRemove) + timedTasks.Remove(t); + + tasksToRemove.Clear(); + + foreach (var t in tasksToSchedule) + timedTasks.AddInPlace(t); + + tasksToSchedule.Clear(); + } + } + + for (int i = 0; i < perUpdateTasks.Count; i++) + { + ScheduledDelegate task = perUpdateTasks[i]; + if (task.Cancelled) + { + perUpdateTasks.RemoveAt(i--); + continue; + } + + schedulerQueue.Enqueue(task.RunTask); + } + + int countRun = 0; + + while (schedulerQueue.TryDequeue(out Action action)) + { + //todo: error handling + action.Invoke(); + countRun++; + } + + return countRun; + } + + /// + /// Cancel any pending work tasks. + /// + public void CancelDelayedTasks() + { + lock (timedTasks) + { + foreach (var t in timedTasks) + t.Cancel(); + timedTasks.Clear(); + } + } + + internal void SetCurrentThread(Thread thread) + { + mainThreadId = thread?.ManagedThreadId ?? -1; + } + + internal void SetCurrentThread() + { + mainThreadId = Thread.CurrentThread.ManagedThreadId; + } + + /// + /// Add a task to be scheduled. + /// + /// The work to be done. + /// If set to false, the task will be executed immediately if we are on the main thread. + /// Whether we could run without scheduling + public bool Add(Action task, bool forceScheduled = true) + { + if (!forceScheduled && IsMainThread) + { + //We are on the main thread already - don't need to schedule. + task.Invoke(); + return true; + } + + schedulerQueue.Enqueue(task); + + return false; + } + + public void Add(ScheduledDelegate task) + { + lock (timedTasks) + { + if (task.RepeatInterval == 0) + perUpdateTasks.Add(task); + else + timedTasks.AddInPlace(task); + } + } + + /// + /// Add a task which will be run after a specified delay. + /// + /// The work to be done. + /// Milliseconds until run. + /// Whether this task should repeat. + public ScheduledDelegate AddDelayed(Action task, double timeUntilRun, bool repeat = false) + { + // We are locking here already to make sure we have no concurrent access to currentTime + lock (timedTasks) + { + ScheduledDelegate del = new ScheduledDelegate(task, currentTime + timeUntilRun, repeat ? timeUntilRun : -1); + Add(del); + return del; + } + } + + /// + /// Adds a task which will only be run once per frame, no matter how many times it was scheduled in the previous frame. + /// + /// The work to be done. + /// Whether this is the first queue attempt of this work. + public bool AddOnce(Action task) + { + if (schedulerQueue.Contains(task)) + return false; + + schedulerQueue.Enqueue(task); + + return true; + } + } + + public class ScheduledDelegate : IComparable + { + public ScheduledDelegate(Action task, double executionTime, double repeatInterval = -1) + { + ExecutionTime = executionTime; + RepeatInterval = repeatInterval; + this.task = task; + } + + /// + /// The work task. + /// + private readonly Action task; + + /// + /// Set to true to skip scheduled executions until we are ready. + /// + internal bool Waiting; + + public void Wait() + { + Waiting = true; + } + + public void Continue() + { + Waiting = false; + } + + public void RunTask() + { + if (!Waiting) + task(); + Completed = true; + } + + public bool Completed; + + public bool Cancelled { get; private set; } + + public void Cancel() + { + Cancelled = true; + } + + /// + /// The earliest ElapsedTime value at which we can be executed. + /// + public double ExecutionTime; + + /// + /// Time in milliseconds between repeats of this task. -1 means no repeats. + /// + public double RepeatInterval; + + public int CompareTo(ScheduledDelegate other) + { + return ExecutionTime == other.ExecutionTime ? -1 : ExecutionTime.CompareTo(other.ExecutionTime); + } + } +} diff --git a/osu.Framework/Threading/UpdateThread.cs b/osu.Framework/Threading/UpdateThread.cs index 0c9bd0b6f..bb421da87 100644 --- a/osu.Framework/Threading/UpdateThread.cs +++ b/osu.Framework/Threading/UpdateThread.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Statistics; -using System; -using System.Collections.Generic; - -namespace osu.Framework.Threading -{ - public class UpdateThread : GameThread - { - public UpdateThread(Action onNewFrame) - : base(onNewFrame, "Update") - { - } - - internal override IEnumerable StatisticsCounters => new[] - { - StatisticsCounterType.Invalidations, - StatisticsCounterType.Refreshes, - StatisticsCounterType.DrawNodeCtor, - StatisticsCounterType.DrawNodeAppl, - StatisticsCounterType.ScheduleInvk, - }; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Statistics; +using System; +using System.Collections.Generic; + +namespace osu.Framework.Threading +{ + public class UpdateThread : GameThread + { + public UpdateThread(Action onNewFrame) + : base(onNewFrame, "Update") + { + } + + internal override IEnumerable StatisticsCounters => new[] + { + StatisticsCounterType.Invalidations, + StatisticsCounterType.Refreshes, + StatisticsCounterType.DrawNodeCtor, + StatisticsCounterType.DrawNodeAppl, + StatisticsCounterType.ScheduleInvk, + }; + } +} diff --git a/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs b/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs index 84ca68bed..41d5c2737 100644 --- a/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs +++ b/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs @@ -1,142 +1,142 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - /// - /// Adds the ability to keep the clock running even when the underlying source has stopped or cannot handle the current time range. - /// This is handled by performing seeks on the underlying source and checking whether they were successful or not. - /// On failure to seek, we take over with an internal clock until control can be returned to the actual source. - /// - /// This clock type removes the requirement of having a source set. - /// - /// If a is set, it is presumed that we have exclusive control over operations on it. - /// This is used to our advantage to allow correct state tracking in the event of cross-thread communication delays (with an audio thread, for instance). - /// - public class DecoupleableInterpolatingFramedClock : InterpolatingFramedClock, IAdjustableClock - { - /// - /// Specify whether we are coupled 1:1 to SourceClock. If not, we can independently continue operation. - /// - public bool IsCoupled = true; - - /// - /// In some cases we should always use the interpolated source. - /// - private bool useInterpolatedSourceTime => IsRunning && FramedSourceClock?.IsRunning == true; - - private readonly FramedClock decoupledClock; - private readonly StopwatchClock decoupledStopwatch; - - /// - /// We need to be able to pass on adjustments to the source if it supports them. - /// - private IAdjustableClock adjustableSource => SourceClock as IAdjustableClock; - - public override double CurrentTime => useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime; - - public override bool IsRunning => decoupledClock.IsRunning; // we always want to use our local IsRunning state, as it is more correct. - - public override double ElapsedFrameTime => useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime; - - public override double Rate - { - get { return SourceClock?.Rate ?? 1; } - set { adjustableSource.Rate = value; } - } - - public void ResetSpeedAdjustments() => Rate = 1; - - public DecoupleableInterpolatingFramedClock() - { - decoupledClock = new FramedClock(decoupledStopwatch = new StopwatchClock()); - } - - public override void ProcessFrame() - { - base.ProcessFrame(); - - decoupledStopwatch.Rate = adjustableSource?.Rate ?? 1; - decoupledClock.ProcessFrame(); - - bool sourceRunning = SourceClock?.IsRunning ?? false; - - if (IsRunning) - { - if (IsCoupled) - { - // when coupled, we want to stop when our source clock stops. - if (sourceRunning) - decoupledStopwatch.Seek(CurrentTime); - else - Stop(); - } - else - { - // when decoupled, if we're running but our source isn't, we should try a seek to see if it's capable to handle the current time. - if (!sourceRunning) - Start(); - } - } - } - - public override void ChangeSource(IClock source) - { - if (source == null) return; - - // transfer our value to the source clock. - (source as IAdjustableClock)?.Seek(CurrentTime); - - SourceClock = source; - FramedSourceClock = SourceClock as IFrameBasedClock ?? new FramedClock(SourceClock); - } - - public void Reset() - { - IsCoupled = true; - - adjustableSource?.Reset(); - decoupledStopwatch.Reset(); - } - - public void Start() - { - if (adjustableSource?.IsRunning == false) - { - if (adjustableSource.Seek(CurrentTime)) - //only start the source clock if our time values match. - //this handles the case where we seeked to an unsupported value and the source clock is out of sync. - adjustableSource.Start(); - } - - decoupledStopwatch.Start(); - } - - public void Stop() - { - decoupledStopwatch.Stop(); - adjustableSource?.Stop(); - } - - public bool Seek(double position) - { - try - { - bool success = adjustableSource?.Seek(position) != false; - - if (IsCoupled) - return success; - - if (!success) - //if we failed to seek then stop the source and use decoupled mode. - adjustableSource?.Stop(); - - return decoupledStopwatch.Seek(position); - } - finally - { - ProcessFrame(); - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + /// + /// Adds the ability to keep the clock running even when the underlying source has stopped or cannot handle the current time range. + /// This is handled by performing seeks on the underlying source and checking whether they were successful or not. + /// On failure to seek, we take over with an internal clock until control can be returned to the actual source. + /// + /// This clock type removes the requirement of having a source set. + /// + /// If a is set, it is presumed that we have exclusive control over operations on it. + /// This is used to our advantage to allow correct state tracking in the event of cross-thread communication delays (with an audio thread, for instance). + /// + public class DecoupleableInterpolatingFramedClock : InterpolatingFramedClock, IAdjustableClock + { + /// + /// Specify whether we are coupled 1:1 to SourceClock. If not, we can independently continue operation. + /// + public bool IsCoupled = true; + + /// + /// In some cases we should always use the interpolated source. + /// + private bool useInterpolatedSourceTime => IsRunning && FramedSourceClock?.IsRunning == true; + + private readonly FramedClock decoupledClock; + private readonly StopwatchClock decoupledStopwatch; + + /// + /// We need to be able to pass on adjustments to the source if it supports them. + /// + private IAdjustableClock adjustableSource => SourceClock as IAdjustableClock; + + public override double CurrentTime => useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime; + + public override bool IsRunning => decoupledClock.IsRunning; // we always want to use our local IsRunning state, as it is more correct. + + public override double ElapsedFrameTime => useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime; + + public override double Rate + { + get { return SourceClock?.Rate ?? 1; } + set { adjustableSource.Rate = value; } + } + + public void ResetSpeedAdjustments() => Rate = 1; + + public DecoupleableInterpolatingFramedClock() + { + decoupledClock = new FramedClock(decoupledStopwatch = new StopwatchClock()); + } + + public override void ProcessFrame() + { + base.ProcessFrame(); + + decoupledStopwatch.Rate = adjustableSource?.Rate ?? 1; + decoupledClock.ProcessFrame(); + + bool sourceRunning = SourceClock?.IsRunning ?? false; + + if (IsRunning) + { + if (IsCoupled) + { + // when coupled, we want to stop when our source clock stops. + if (sourceRunning) + decoupledStopwatch.Seek(CurrentTime); + else + Stop(); + } + else + { + // when decoupled, if we're running but our source isn't, we should try a seek to see if it's capable to handle the current time. + if (!sourceRunning) + Start(); + } + } + } + + public override void ChangeSource(IClock source) + { + if (source == null) return; + + // transfer our value to the source clock. + (source as IAdjustableClock)?.Seek(CurrentTime); + + SourceClock = source; + FramedSourceClock = SourceClock as IFrameBasedClock ?? new FramedClock(SourceClock); + } + + public void Reset() + { + IsCoupled = true; + + adjustableSource?.Reset(); + decoupledStopwatch.Reset(); + } + + public void Start() + { + if (adjustableSource?.IsRunning == false) + { + if (adjustableSource.Seek(CurrentTime)) + //only start the source clock if our time values match. + //this handles the case where we seeked to an unsupported value and the source clock is out of sync. + adjustableSource.Start(); + } + + decoupledStopwatch.Start(); + } + + public void Stop() + { + decoupledStopwatch.Stop(); + adjustableSource?.Stop(); + } + + public bool Seek(double position) + { + try + { + bool success = adjustableSource?.Seek(position) != false; + + if (IsCoupled) + return success; + + if (!success) + //if we failed to seek then stop the source and use decoupled mode. + adjustableSource?.Stop(); + + return decoupledStopwatch.Seek(position); + } + finally + { + ProcessFrame(); + } + } + } +} diff --git a/osu.Framework/Timing/FrameTimeInfo.cs b/osu.Framework/Timing/FrameTimeInfo.cs index 391ea19a5..b85c1d07c 100644 --- a/osu.Framework/Timing/FrameTimeInfo.cs +++ b/osu.Framework/Timing/FrameTimeInfo.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Globalization; - -namespace osu.Framework.Timing -{ - public struct FrameTimeInfo - { - /// - /// Elapsed time during last frame in milliseconds. - /// - public double Elapsed; - - /// - /// Begin time of this frame. - /// - public double Current; - - public override string ToString() => Math.Truncate(Current).ToString(CultureInfo.InvariantCulture); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Globalization; + +namespace osu.Framework.Timing +{ + public struct FrameTimeInfo + { + /// + /// Elapsed time during last frame in milliseconds. + /// + public double Elapsed; + + /// + /// Begin time of this frame. + /// + public double Current; + + public override string ToString() => Math.Truncate(Current).ToString(CultureInfo.InvariantCulture); + } +} diff --git a/osu.Framework/Timing/FramedClock.cs b/osu.Framework/Timing/FramedClock.cs index 22b6420a4..ad7b79965 100644 --- a/osu.Framework/Timing/FramedClock.cs +++ b/osu.Framework/Timing/FramedClock.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.MathUtils; -using System; - -namespace osu.Framework.Timing -{ - /// - /// Takes a clock source and separates time reading on a per-frame level. - /// The CurrentTime value will only change on initial construction and whenever ProcessFrame is run. - /// - public class FramedClock : IFrameBasedClock - { - public IClock Source { get; } - - /// - /// Construct a new FramedClock with an optional source clock. - /// - /// A source clock which will be used as the backing time source. If null, a StopwatchClock will be created. When provided, the CurrentTime of will be transferred instantly. - public FramedClock(IClock source = null) - { - if (source != null) - { - CurrentTime = LastFrameTime = source.CurrentTime; - Source = source; - } - else - Source = new StopwatchClock(true); - } - - public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - - public double AverageFrameTime { get; private set; } - - public double FramesPerSecond { get; private set; } - - public virtual double CurrentTime { get; protected set; } - - protected virtual double LastFrameTime { get; set; } - - public double Rate => Source.Rate; - - protected double SourceTime => Source.CurrentTime; - - public double ElapsedFrameTime => CurrentTime - LastFrameTime; - - public bool IsRunning => Source?.IsRunning ?? false; - - private double timeUntilNextCalculation; - private double timeSinceLastCalculation; - private int framesSinceLastCalculation; - - private const int fps_calculation_interval = 250; - - public virtual void ProcessFrame() - { - (Source as IFrameBasedClock)?.ProcessFrame(); - - if (timeUntilNextCalculation <= 0) - { - timeUntilNextCalculation += fps_calculation_interval; - - if (framesSinceLastCalculation == 0) - FramesPerSecond = 0; - else - FramesPerSecond = (int)Math.Ceiling(framesSinceLastCalculation * 1000f / timeSinceLastCalculation); - timeSinceLastCalculation = framesSinceLastCalculation = 0; - } - - framesSinceLastCalculation++; - timeUntilNextCalculation -= ElapsedFrameTime; - timeSinceLastCalculation += ElapsedFrameTime; - - AverageFrameTime = Interpolation.Damp(AverageFrameTime, ElapsedFrameTime, 0.01, Math.Max(ElapsedFrameTime, 0) / 1000); - - LastFrameTime = CurrentTime; - CurrentTime = SourceTime; - } - - public override string ToString() => $@"{GetType().ReadableName()} ({Math.Truncate(CurrentTime)}ms, {FramesPerSecond} FPS)"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.MathUtils; +using System; + +namespace osu.Framework.Timing +{ + /// + /// Takes a clock source and separates time reading on a per-frame level. + /// The CurrentTime value will only change on initial construction and whenever ProcessFrame is run. + /// + public class FramedClock : IFrameBasedClock + { + public IClock Source { get; } + + /// + /// Construct a new FramedClock with an optional source clock. + /// + /// A source clock which will be used as the backing time source. If null, a StopwatchClock will be created. When provided, the CurrentTime of will be transferred instantly. + public FramedClock(IClock source = null) + { + if (source != null) + { + CurrentTime = LastFrameTime = source.CurrentTime; + Source = source; + } + else + Source = new StopwatchClock(true); + } + + public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; + + public double AverageFrameTime { get; private set; } + + public double FramesPerSecond { get; private set; } + + public virtual double CurrentTime { get; protected set; } + + protected virtual double LastFrameTime { get; set; } + + public double Rate => Source.Rate; + + protected double SourceTime => Source.CurrentTime; + + public double ElapsedFrameTime => CurrentTime - LastFrameTime; + + public bool IsRunning => Source?.IsRunning ?? false; + + private double timeUntilNextCalculation; + private double timeSinceLastCalculation; + private int framesSinceLastCalculation; + + private const int fps_calculation_interval = 250; + + public virtual void ProcessFrame() + { + (Source as IFrameBasedClock)?.ProcessFrame(); + + if (timeUntilNextCalculation <= 0) + { + timeUntilNextCalculation += fps_calculation_interval; + + if (framesSinceLastCalculation == 0) + FramesPerSecond = 0; + else + FramesPerSecond = (int)Math.Ceiling(framesSinceLastCalculation * 1000f / timeSinceLastCalculation); + timeSinceLastCalculation = framesSinceLastCalculation = 0; + } + + framesSinceLastCalculation++; + timeUntilNextCalculation -= ElapsedFrameTime; + timeSinceLastCalculation += ElapsedFrameTime; + + AverageFrameTime = Interpolation.Damp(AverageFrameTime, ElapsedFrameTime, 0.01, Math.Max(ElapsedFrameTime, 0) / 1000); + + LastFrameTime = CurrentTime; + CurrentTime = SourceTime; + } + + public override string ToString() => $@"{GetType().ReadableName()} ({Math.Truncate(CurrentTime)}ms, {FramesPerSecond} FPS)"; + } +} diff --git a/osu.Framework/Timing/FramedOffsetClock.cs b/osu.Framework/Timing/FramedOffsetClock.cs index d5f10d409..a8de88550 100644 --- a/osu.Framework/Timing/FramedOffsetClock.cs +++ b/osu.Framework/Timing/FramedOffsetClock.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - public class FramedOffsetClock : FramedClock - { - private double offset; - - public override double CurrentTime => base.CurrentTime + offset; - - public double Offset - { - get { return offset; } - set - { - LastFrameTime += value - offset; - offset = value; - } - } - - public FramedOffsetClock(IClock source) - : base(source) - { - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + public class FramedOffsetClock : FramedClock + { + private double offset; + + public override double CurrentTime => base.CurrentTime + offset; + + public double Offset + { + get { return offset; } + set + { + LastFrameTime += value - offset; + offset = value; + } + } + + public FramedOffsetClock(IClock source) + : base(source) + { + } + } +} diff --git a/osu.Framework/Timing/IAdjustableClock.cs b/osu.Framework/Timing/IAdjustableClock.cs index 6214a33e3..f85859634 100644 --- a/osu.Framework/Timing/IAdjustableClock.cs +++ b/osu.Framework/Timing/IAdjustableClock.cs @@ -1,42 +1,42 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - /// - /// A clock that can be started, stopped, reset etc. - /// - public interface IAdjustableClock : IClock - { - /// - /// Stop and reset position. - /// - void Reset(); - - /// - /// Start (resume) running. - /// - void Start(); - - /// - /// Stop (pause) running. - /// - void Stop(); - - /// - /// Seek to a specific time position. - /// - /// Whether a seek was possible. - bool Seek(double position); - - /// - /// The rate this clock is running at, relative to real-time. - /// - new double Rate { get; set; } - - /// - /// Reset the rate to a stable value. - /// - void ResetSpeedAdjustments(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + /// + /// A clock that can be started, stopped, reset etc. + /// + public interface IAdjustableClock : IClock + { + /// + /// Stop and reset position. + /// + void Reset(); + + /// + /// Start (resume) running. + /// + void Start(); + + /// + /// Stop (pause) running. + /// + void Stop(); + + /// + /// Seek to a specific time position. + /// + /// Whether a seek was possible. + bool Seek(double position); + + /// + /// The rate this clock is running at, relative to real-time. + /// + new double Rate { get; set; } + + /// + /// Reset the rate to a stable value. + /// + void ResetSpeedAdjustments(); + } +} diff --git a/osu.Framework/Timing/IClock.cs b/osu.Framework/Timing/IClock.cs index 9d85285f1..1ad998973 100644 --- a/osu.Framework/Timing/IClock.cs +++ b/osu.Framework/Timing/IClock.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - /// - /// A basic clock for keeping time. - /// - public interface IClock - { - /// - /// The current time of this clock, in milliseconds. - /// - double CurrentTime { get; } - - /// - /// The rate this clock is running at, relative to real-time. - /// - double Rate { get; } - - /// - /// Whether this clock is currently running or not. - /// - bool IsRunning { get; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + /// + /// A basic clock for keeping time. + /// + public interface IClock + { + /// + /// The current time of this clock, in milliseconds. + /// + double CurrentTime { get; } + + /// + /// The rate this clock is running at, relative to real-time. + /// + double Rate { get; } + + /// + /// Whether this clock is currently running or not. + /// + bool IsRunning { get; } + } +} diff --git a/osu.Framework/Timing/IFrameBasedClock.cs b/osu.Framework/Timing/IFrameBasedClock.cs index c83a7cf5a..a6d603527 100644 --- a/osu.Framework/Timing/IFrameBasedClock.cs +++ b/osu.Framework/Timing/IFrameBasedClock.cs @@ -1,27 +1,27 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - /// - /// A clock which will only update its current time when a frame proces is triggered. - /// Useful for keeping a consistent time state across an individual update. - /// - public interface IFrameBasedClock : IClock - { - /// - /// Elapsed time since last frame in milliseconds. - /// - double ElapsedFrameTime { get; } - - double AverageFrameTime { get; } - double FramesPerSecond { get; } - - FrameTimeInfo TimeInfo { get; } - - /// - /// Processes one frame. Generally should be run once per update loop. - /// - void ProcessFrame(); - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + /// + /// A clock which will only update its current time when a frame proces is triggered. + /// Useful for keeping a consistent time state across an individual update. + /// + public interface IFrameBasedClock : IClock + { + /// + /// Elapsed time since last frame in milliseconds. + /// + double ElapsedFrameTime { get; } + + double AverageFrameTime { get; } + double FramesPerSecond { get; } + + FrameTimeInfo TimeInfo { get; } + + /// + /// Processes one frame. Generally should be run once per update loop. + /// + void ProcessFrame(); + } +} diff --git a/osu.Framework/Timing/InterpolatingFramedClock.cs b/osu.Framework/Timing/InterpolatingFramedClock.cs index f05330b4d..2e475ade9 100644 --- a/osu.Framework/Timing/InterpolatingFramedClock.cs +++ b/osu.Framework/Timing/InterpolatingFramedClock.cs @@ -1,91 +1,91 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; - -namespace osu.Framework.Timing -{ - /// - /// A clock which uses an internal stopwatch to interpolate (smooth out) a source. - /// Note that this will NOT function unless a source has been set. - /// - public class InterpolatingFramedClock : IFrameBasedClock - { - private readonly FramedClock clock = new FramedClock(new StopwatchClock(true)); - - protected IClock SourceClock; - - protected IFrameBasedClock FramedSourceClock; - protected double LastInterpolatedTime; - protected double CurrentInterpolatedTime; - - public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - - public double AverageFrameTime { get; } = 0; - - public double FramesPerSecond { get; } = 0; - - public virtual void ChangeSource(IClock source) - { - if (source != null) - { - SourceClock = source; - FramedSourceClock = SourceClock as IFrameBasedClock ?? new FramedClock(SourceClock); - } - - LastInterpolatedTime = 0; - CurrentInterpolatedTime = 0; - } - - public InterpolatingFramedClock(IClock source = null) - { - ChangeSource(source); - } - - public virtual double CurrentTime => sourceIsRunning ? CurrentInterpolatedTime : FramedSourceClock.CurrentTime; - - public double AllowableErrorMilliseconds = 1000.0 / 60 * 2; - - private bool sourceIsRunning; - - public virtual double Rate - { - get { return FramedSourceClock.Rate; } - set { throw new NotSupportedException(); } - } - - public virtual bool IsRunning => sourceIsRunning; - - public virtual double Drift => CurrentTime - FramedSourceClock.CurrentTime; - - public virtual double ElapsedFrameTime => CurrentInterpolatedTime - LastInterpolatedTime; - - public virtual void ProcessFrame() - { - if (FramedSourceClock == null) return; - - clock.ProcessFrame(); - FramedSourceClock.ProcessFrame(); - - sourceIsRunning = FramedSourceClock.IsRunning; - - LastInterpolatedTime = CurrentTime; - - if (!FramedSourceClock.IsRunning) - return; - - CurrentInterpolatedTime += clock.ElapsedFrameTime * Rate; - - if (Math.Abs(FramedSourceClock.CurrentTime - CurrentInterpolatedTime) > AllowableErrorMilliseconds) - { - //if we've exceeded the allowable error, we should use the source clock's time value. - CurrentInterpolatedTime = FramedSourceClock.CurrentTime; - } - else - { - //if we differ from the elapsed time of the source, let's adjust for the difference. - CurrentInterpolatedTime += (FramedSourceClock.CurrentTime - CurrentInterpolatedTime) / 8; - } - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; + +namespace osu.Framework.Timing +{ + /// + /// A clock which uses an internal stopwatch to interpolate (smooth out) a source. + /// Note that this will NOT function unless a source has been set. + /// + public class InterpolatingFramedClock : IFrameBasedClock + { + private readonly FramedClock clock = new FramedClock(new StopwatchClock(true)); + + protected IClock SourceClock; + + protected IFrameBasedClock FramedSourceClock; + protected double LastInterpolatedTime; + protected double CurrentInterpolatedTime; + + public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; + + public double AverageFrameTime { get; } = 0; + + public double FramesPerSecond { get; } = 0; + + public virtual void ChangeSource(IClock source) + { + if (source != null) + { + SourceClock = source; + FramedSourceClock = SourceClock as IFrameBasedClock ?? new FramedClock(SourceClock); + } + + LastInterpolatedTime = 0; + CurrentInterpolatedTime = 0; + } + + public InterpolatingFramedClock(IClock source = null) + { + ChangeSource(source); + } + + public virtual double CurrentTime => sourceIsRunning ? CurrentInterpolatedTime : FramedSourceClock.CurrentTime; + + public double AllowableErrorMilliseconds = 1000.0 / 60 * 2; + + private bool sourceIsRunning; + + public virtual double Rate + { + get { return FramedSourceClock.Rate; } + set { throw new NotSupportedException(); } + } + + public virtual bool IsRunning => sourceIsRunning; + + public virtual double Drift => CurrentTime - FramedSourceClock.CurrentTime; + + public virtual double ElapsedFrameTime => CurrentInterpolatedTime - LastInterpolatedTime; + + public virtual void ProcessFrame() + { + if (FramedSourceClock == null) return; + + clock.ProcessFrame(); + FramedSourceClock.ProcessFrame(); + + sourceIsRunning = FramedSourceClock.IsRunning; + + LastInterpolatedTime = CurrentTime; + + if (!FramedSourceClock.IsRunning) + return; + + CurrentInterpolatedTime += clock.ElapsedFrameTime * Rate; + + if (Math.Abs(FramedSourceClock.CurrentTime - CurrentInterpolatedTime) > AllowableErrorMilliseconds) + { + //if we've exceeded the allowable error, we should use the source clock's time value. + CurrentInterpolatedTime = FramedSourceClock.CurrentTime; + } + else + { + //if we differ from the elapsed time of the source, let's adjust for the difference. + CurrentInterpolatedTime += (FramedSourceClock.CurrentTime - CurrentInterpolatedTime) / 8; + } + } + } +} diff --git a/osu.Framework/Timing/ManualClock.cs b/osu.Framework/Timing/ManualClock.cs index 4b0ec4d86..300d74c94 100644 --- a/osu.Framework/Timing/ManualClock.cs +++ b/osu.Framework/Timing/ManualClock.cs @@ -1,15 +1,15 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - /// - /// A completely manual clock implementation. Everything is settable. - /// - public class ManualClock : IClock - { - public double CurrentTime { get; set; } - public double Rate { get; set; } - public bool IsRunning { get; set; } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + /// + /// A completely manual clock implementation. Everything is settable. + /// + public class ManualClock : IClock + { + public double CurrentTime { get; set; } + public double Rate { get; set; } + public bool IsRunning { get; set; } + } +} diff --git a/osu.Framework/Timing/OffsetClock.cs b/osu.Framework/Timing/OffsetClock.cs index a947c61c3..2bb097b29 100644 --- a/osu.Framework/Timing/OffsetClock.cs +++ b/osu.Framework/Timing/OffsetClock.cs @@ -1,23 +1,23 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -namespace osu.Framework.Timing -{ - public class OffsetClock : IClock - { - protected IClock Source; - - public double Offset; - - public double CurrentTime => Source.CurrentTime + Offset; - - public double Rate => Source.Rate; - - public bool IsRunning => Source.IsRunning; - - public OffsetClock(IClock source) - { - Source = source; - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +namespace osu.Framework.Timing +{ + public class OffsetClock : IClock + { + protected IClock Source; + + public double Offset; + + public double CurrentTime => Source.CurrentTime + Offset; + + public double Rate => Source.Rate; + + public bool IsRunning => Source.IsRunning; + + public OffsetClock(IClock source) + { + Source = source; + } + } +} diff --git a/osu.Framework/Timing/StopwatchClock.cs b/osu.Framework/Timing/StopwatchClock.cs index 289835e15..29cfa6b02 100644 --- a/osu.Framework/Timing/StopwatchClock.cs +++ b/osu.Framework/Timing/StopwatchClock.cs @@ -1,67 +1,67 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using osu.Framework.Extensions.TypeExtensions; -using System; -using System.Diagnostics; - -namespace osu.Framework.Timing -{ - public class StopwatchClock : Stopwatch, IAdjustableClock - { - private double seekOffset; - - /// - /// Keep track of how much stopwatch time we have used at previous rates. - /// - private double rateChangeUsed; - - /// - /// Keep track of the resultant time that was accumulated at previous rates. - /// - private double rateChangeAccumulated; - - public StopwatchClock(bool start = false) - { - if (start) - Start(); - } - - public double CurrentTime => stopwatchCurrentTime + seekOffset; - - /// - /// The current time, represented solely by the accumulated time. - /// - private double stopwatchCurrentTime => (stopwatchMilliseconds - rateChangeUsed) * rate + rateChangeAccumulated; - - private double stopwatchMilliseconds => (double)ElapsedTicks / Frequency * 1000; - - private double rate = 1; - - public double Rate - { - get { return rate; } - - set - { - if (rate == value) return; - - rateChangeAccumulated += (stopwatchMilliseconds - rateChangeUsed) * rate; - rateChangeUsed = stopwatchMilliseconds; - - rate = value; - } - } - - public void ResetSpeedAdjustments() => Rate = 1; - - public bool Seek(double position) - { - // Determine the offset that when added to stopwatchCurrentTime; results in the requested time value - seekOffset = position - stopwatchCurrentTime; - return true; - } - - public override string ToString() => $@"{GetType().ReadableName()} ({Math.Truncate(CurrentTime)}ms)"; - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using osu.Framework.Extensions.TypeExtensions; +using System; +using System.Diagnostics; + +namespace osu.Framework.Timing +{ + public class StopwatchClock : Stopwatch, IAdjustableClock + { + private double seekOffset; + + /// + /// Keep track of how much stopwatch time we have used at previous rates. + /// + private double rateChangeUsed; + + /// + /// Keep track of the resultant time that was accumulated at previous rates. + /// + private double rateChangeAccumulated; + + public StopwatchClock(bool start = false) + { + if (start) + Start(); + } + + public double CurrentTime => stopwatchCurrentTime + seekOffset; + + /// + /// The current time, represented solely by the accumulated time. + /// + private double stopwatchCurrentTime => (stopwatchMilliseconds - rateChangeUsed) * rate + rateChangeAccumulated; + + private double stopwatchMilliseconds => (double)ElapsedTicks / Frequency * 1000; + + private double rate = 1; + + public double Rate + { + get { return rate; } + + set + { + if (rate == value) return; + + rateChangeAccumulated += (stopwatchMilliseconds - rateChangeUsed) * rate; + rateChangeUsed = stopwatchMilliseconds; + + rate = value; + } + } + + public void ResetSpeedAdjustments() => Rate = 1; + + public bool Seek(double position) + { + // Determine the offset that when added to stopwatchCurrentTime; results in the requested time value + seekOffset = position - stopwatchCurrentTime; + return true; + } + + public override string ToString() => $@"{GetType().ReadableName()} ({Math.Truncate(CurrentTime)}ms)"; + } +} diff --git a/osu.Framework/Timing/ThrottledFrameClock.cs b/osu.Framework/Timing/ThrottledFrameClock.cs index 5cb060606..158e68241 100644 --- a/osu.Framework/Timing/ThrottledFrameClock.cs +++ b/osu.Framework/Timing/ThrottledFrameClock.cs @@ -1,84 +1,84 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE - -using System; -using System.Diagnostics; -using System.Threading; - -namespace osu.Framework.Timing -{ - /// - /// A FrameClock which will limit the number of frames processed by adding Thread.Sleep calls on each ProcessFrame. - /// - public class ThrottledFrameClock : FramedClock - { - /// - /// The number of updated per second which is permitted. - /// - public double MaximumUpdateHz = 1000.0; - - /// - /// If true, we will perform a Thread.Sleep even if the period is absolute zero. - /// Allows other threads to process. - /// - public bool AlwaysSleep = true; - - private double minimumFrameTime => 1000.0 / MaximumUpdateHz; - - private double accumulatedSleepError; - - private void throttle() - { - int timeToSleepFloored = 0; - - //If we are limiting to a specific rate, and not enough time has passed for the next frame to be accepted we should pause here. - if (MaximumUpdateHz > 0 && minimumFrameTime > 0) - { - double targetMilliseconds = minimumFrameTime; - if (ElapsedFrameTime < targetMilliseconds) - { - // Using ticks for sleeping is pointless due to them being rounded to milliseconds internally anyways (in windows at least). - double timeToSleep = targetMilliseconds - ElapsedFrameTime; - timeToSleepFloored = (int)Math.Floor(timeToSleep); - - Trace.Assert(timeToSleepFloored >= 0); - - accumulatedSleepError += timeToSleep - timeToSleepFloored; - int accumulatedSleepErrorCompensation = (int)Math.Round(accumulatedSleepError); - - // Can't sleep a negative amount of time - accumulatedSleepErrorCompensation = Math.Max(accumulatedSleepErrorCompensation, -timeToSleepFloored); - - accumulatedSleepError -= accumulatedSleepErrorCompensation; - timeToSleepFloored += accumulatedSleepErrorCompensation; - - // We don't want re-schedules with Thread.Sleep(0). We already have that case down below. - if (timeToSleepFloored > 0) - Thread.Sleep(timeToSleepFloored); - - // Sleep is not guaranteed to be an exact time. It only guaranteed to sleep AT LEAST the specified time. We also used some time to compute the above things, so this is also factored in here. - double afterSleepTime = SourceTime; - accumulatedSleepError += timeToSleepFloored - (afterSleepTime - CurrentTime); - CurrentTime = afterSleepTime; - } - else - { - // We use the negative spareTime to compensate for framerate jitter slightly. - double spareTime = ElapsedFrameTime - targetMilliseconds; - accumulatedSleepError = -spareTime; - } - } - - // Call the scheduler to give lower-priority background processes a chance to do stuff. - if (timeToSleepFloored == 0) - Thread.Sleep(0); - } - - public override void ProcessFrame() - { - base.ProcessFrame(); - - throttle(); - } - } -} +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Diagnostics; +using System.Threading; + +namespace osu.Framework.Timing +{ + /// + /// A FrameClock which will limit the number of frames processed by adding Thread.Sleep calls on each ProcessFrame. + /// + public class ThrottledFrameClock : FramedClock + { + /// + /// The number of updated per second which is permitted. + /// + public double MaximumUpdateHz = 1000.0; + + /// + /// If true, we will perform a Thread.Sleep even if the period is absolute zero. + /// Allows other threads to process. + /// + public bool AlwaysSleep = true; + + private double minimumFrameTime => 1000.0 / MaximumUpdateHz; + + private double accumulatedSleepError; + + private void throttle() + { + int timeToSleepFloored = 0; + + //If we are limiting to a specific rate, and not enough time has passed for the next frame to be accepted we should pause here. + if (MaximumUpdateHz > 0 && minimumFrameTime > 0) + { + double targetMilliseconds = minimumFrameTime; + if (ElapsedFrameTime < targetMilliseconds) + { + // Using ticks for sleeping is pointless due to them being rounded to milliseconds internally anyways (in windows at least). + double timeToSleep = targetMilliseconds - ElapsedFrameTime; + timeToSleepFloored = (int)Math.Floor(timeToSleep); + + Trace.Assert(timeToSleepFloored >= 0); + + accumulatedSleepError += timeToSleep - timeToSleepFloored; + int accumulatedSleepErrorCompensation = (int)Math.Round(accumulatedSleepError); + + // Can't sleep a negative amount of time + accumulatedSleepErrorCompensation = Math.Max(accumulatedSleepErrorCompensation, -timeToSleepFloored); + + accumulatedSleepError -= accumulatedSleepErrorCompensation; + timeToSleepFloored += accumulatedSleepErrorCompensation; + + // We don't want re-schedules with Thread.Sleep(0). We already have that case down below. + if (timeToSleepFloored > 0) + Thread.Sleep(timeToSleepFloored); + + // Sleep is not guaranteed to be an exact time. It only guaranteed to sleep AT LEAST the specified time. We also used some time to compute the above things, so this is also factored in here. + double afterSleepTime = SourceTime; + accumulatedSleepError += timeToSleepFloored - (afterSleepTime - CurrentTime); + CurrentTime = afterSleepTime; + } + else + { + // We use the negative spareTime to compensate for framerate jitter slightly. + double spareTime = ElapsedFrameTime - targetMilliseconds; + accumulatedSleepError = -spareTime; + } + } + + // Call the scheduler to give lower-priority background processes a chance to do stuff. + if (timeToSleepFloored == 0) + Thread.Sleep(0); + } + + public override void ProcessFrame() + { + base.ProcessFrame(); + + throttle(); + } + } +}