diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/SampleGame.Android/AndroidManifest.xml b/SampleGame.Android/AndroidManifest.xml index e69de29bb..b99f1e558 100644 --- a/SampleGame.Android/AndroidManifest.xml +++ b/SampleGame.Android/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SampleGame.Android/SampleGame.Android.csproj b/SampleGame.Android/SampleGame.Android.csproj index e69de29bb..ab0f71c35 100644 --- a/SampleGame.Android/SampleGame.Android.csproj +++ b/SampleGame.Android/SampleGame.Android.csproj @@ -0,0 +1,13 @@ + + + + net8.0-android + Exe + SampleGame.Android + SampleGame.Android + + + + + + diff --git a/SampleGame.Android/SampleGameActivity.cs b/SampleGame.Android/SampleGameActivity.cs index e69de29bb..bdb800d5d 100644 --- a/SampleGame.Android/SampleGameActivity.cs +++ b/SampleGame.Android/SampleGameActivity.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.App; +using osu.Framework; +using osu.Framework.Android; + +namespace SampleGame.Android +{ + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] + public class SampleGameActivity : AndroidGameActivity + { + protected override Game CreateGame() => new SampleGameGame(); + } +} diff --git a/SampleGame.Desktop/Program.cs b/SampleGame.Desktop/Program.cs new file mode 100644 index 000000000..4756edbaa --- /dev/null +++ b/SampleGame.Desktop/Program.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Platform; + +namespace SampleGame.Desktop +{ + public static class Program + { + [STAThread] + public static void Main(string[] args) + { + using (GameHost host = Host.GetSuitableDesktopHost(@"sample-game")) + using (Game game = new SampleGameGame()) + host.Run(game); + } + } +} diff --git a/SampleGame.Desktop/SampleGame.Desktop.csproj b/SampleGame.Desktop/SampleGame.Desktop.csproj new file mode 100644 index 000000000..7ddb6db1a --- /dev/null +++ b/SampleGame.Desktop/SampleGame.Desktop.csproj @@ -0,0 +1,10 @@ + + + net8.0 + WinExe + + + + + + diff --git a/SampleGame.iOS/AppDelegate.cs b/SampleGame.iOS/AppDelegate.cs index e69de29bb..9712408a0 100644 --- a/SampleGame.iOS/AppDelegate.cs +++ b/SampleGame.iOS/AppDelegate.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework; +using osu.Framework.iOS; + +namespace SampleGame.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Game CreateGame() => new SampleGameGame(); + } +} diff --git a/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json index e69de29bb..c7dd1941e 100644 --- a/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SampleGame.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,231 @@ +{ + "images": [ + { + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "idiom": "ipad", + "scale": "1x", + "size": "20x20" + }, + { + "idiom": "ipad", + "scale": "2x", + "size": "20x20" + }, + { + "idiom": "ipad", + "scale": "1x", + "size": "29x29" + }, + { + "idiom": "ipad", + "scale": "2x", + "size": "29x29" + }, + { + "idiom": "ipad", + "scale": "1x", + "size": "40x40" + }, + { + "idiom": "ipad", + "scale": "2x", + "size": "40x40" + }, + { + "idiom": "ipad", + "scale": "1x", + "size": "76x76" + }, + { + "idiom": "ipad", + "scale": "2x", + "size": "76x76" + }, + { + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" + }, + { + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + }, + { + "idiom": "car", + "scale": "2x", + "size": "60x60" + }, + { + "idiom": "car", + "scale": "3x", + "size": "60x60" + }, + { + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "24x24", + "subtype": "38mm" + }, + { + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "27.5x27.5", + "subtype": "42mm" + }, + { + "idiom": "watch", + "role": "companionSettings", + "scale": "2x", + "size": "29x29" + }, + { + "idiom": "watch", + "role": "companionSettings", + "scale": "3x", + "size": "29x29" + }, + { + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "40x40", + "subtype": "38mm" + }, + { + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "44x44", + "subtype": "40mm" + }, + { + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "50x50", + "subtype": "44mm" + }, + { + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "86x86", + "subtype": "38mm" + }, + { + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "98x98", + "subtype": "42mm" + }, + { + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "108x108", + "subtype": "44mm" + }, + { + "idiom": "watch-marketing", + "scale": "1x", + "size": "1024x1024" + }, + { + "idiom": "mac", + "scale": "1x", + "size": "16x16" + }, + { + "idiom": "mac", + "scale": "2x", + "size": "16x16" + }, + { + "idiom": "mac", + "scale": "1x", + "size": "32x32" + }, + { + "idiom": "mac", + "scale": "2x", + "size": "32x32" + }, + { + "idiom": "mac", + "scale": "1x", + "size": "128x128" + }, + { + "idiom": "mac", + "scale": "2x", + "size": "128x128" + }, + { + "idiom": "mac", + "scale": "1x", + "size": "256x256" + }, + { + "idiom": "mac", + "scale": "2x", + "size": "256x256" + }, + { + "idiom": "mac", + "scale": "1x", + "size": "512x512" + }, + { + "idiom": "mac", + "scale": "2x", + "size": "512x512" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/SampleGame.iOS/Assets.xcassets/Contents.json b/SampleGame.iOS/Assets.xcassets/Contents.json index e69de29bb..4caf392f9 100644 --- a/SampleGame.iOS/Assets.xcassets/Contents.json +++ b/SampleGame.iOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SampleGame.iOS/Entitlements.plist b/SampleGame.iOS/Entitlements.plist index e69de29bb..9ae599370 100644 --- a/SampleGame.iOS/Entitlements.plist +++ b/SampleGame.iOS/Entitlements.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/SampleGame.iOS/Info.plist b/SampleGame.iOS/Info.plist index e69de29bb..417bfcd8e 100644 --- a/SampleGame.iOS/Info.plist +++ b/SampleGame.iOS/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleName + SampleGame.iOS + CFBundleIdentifier + sh.ppy.sample-game + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 13.4 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset + UIStatusBarHidden + + UIRequiresFullScreen + + UIApplicationSupportsIndirectInputEvents + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/SampleGame.iOS/LaunchScreen.storyboard b/SampleGame.iOS/LaunchScreen.storyboard index e69de29bb..5d2e905aa 100644 --- a/SampleGame.iOS/LaunchScreen.storyboard +++ b/SampleGame.iOS/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleGame.iOS/Program.cs b/SampleGame.iOS/Program.cs index e69de29bb..b5dbf419e 100644 --- a/SampleGame.iOS/Program.cs +++ b/SampleGame.iOS/Program.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using UIKit; + +namespace SampleGame.iOS +{ + public static class Program + { + public static void Main(string[] args) + { + UIApplication.Main(args, null, typeof(AppDelegate)); + } + } +} diff --git a/SampleGame.iOS/SampleGame.iOS.csproj b/SampleGame.iOS/SampleGame.iOS.csproj index e69de29bb..8048e4f2b 100644 --- a/SampleGame.iOS/SampleGame.iOS.csproj +++ b/SampleGame.iOS/SampleGame.iOS.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0-ios + 13.4 + + + + + + + + diff --git a/SampleGame/SampleGame.csproj b/SampleGame/SampleGame.csproj new file mode 100644 index 000000000..d3d384f9c --- /dev/null +++ b/SampleGame/SampleGame.csproj @@ -0,0 +1,8 @@ + + + net8.0 + + + + + diff --git a/SampleGame/SampleGameGame.cs b/SampleGame/SampleGameGame.cs new file mode 100644 index 000000000..ae72ffa24 --- /dev/null +++ b/SampleGame/SampleGameGame.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework; +using osu.Framework.Graphics; +using osuTK; +using osuTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Allocation; + +namespace SampleGame +{ + public partial class SampleGameGame : Game + { + private Box box = null!; + + [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.SourceGeneration.Tests/Dependencies/DependencyInjectionSourceGeneratorTests.cs b/osu.Framework.SourceGeneration.Tests/Dependencies/DependencyInjectionSourceGeneratorTests.cs index 770f39fa2..b65d8a1e9 100644 --- a/osu.Framework.SourceGeneration.Tests/Dependencies/DependencyInjectionSourceGeneratorTests.cs +++ b/osu.Framework.SourceGeneration.Tests/Dependencies/DependencyInjectionSourceGeneratorTests.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Xunit; using VerifyCS = osu.Framework.SourceGeneration.Tests.Verifiers.CSharpSourceGeneratorVerifier; @@ -47,5 +48,19 @@ namespace osu.Framework.SourceGeneration.Tests.Dependencies return VerifyCS.VerifyAsync(commonSourceFiles, sourceFiles, commonGeneratedFiles, generatedFiles); } + + [Theory] + [InlineData("EmptyFile")] + public Task CheckDebugNoOutput(string name) + { + GetTestSources(name, + out (string filename, string content)[] commonSourceFiles, + out (string filename, string content)[] sourceFiles, + out _, + out _ + ); + + return VerifyCS.VerifyAsync(commonSourceFiles, sourceFiles, [], [], OptimizationLevel.Debug); + } } } diff --git a/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs b/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs index b92dfc04f..c50fa830f 100644 --- a/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs +++ b/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using osu.Framework.SourceGeneration.Generators; @@ -16,9 +17,10 @@ namespace osu.Framework.SourceGeneration.Tests.Verifiers (string filename, string content)[] commonSources, (string filename, string content)[] sources, (string filename, string content)[] commonGenerated, - (string filename, string content)[] generated) + (string filename, string content)[] generated, + OptimizationLevel optimizationLevel = OptimizationLevel.Release) { - var test = new Test(); + var test = new Test(optimizationLevel); foreach (var s in commonSources) test.TestState.Sources.Add((s.filename, SourceText.From(s.content, Encoding.UTF8))); diff --git a/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier_Test.cs b/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier_Test.cs index a18203f56..6860c5e8f 100644 --- a/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier_Test.cs +++ b/osu.Framework.SourceGeneration.Tests/Verifiers/CSharpSourceGeneratorVerifier_Test.cs @@ -16,13 +16,20 @@ namespace osu.Framework.SourceGeneration.Tests.Verifiers { public class Test : CSharpSourceGeneratorTest { + private readonly OptimizationLevel optimizationLevel; + + public Test(OptimizationLevel optimizationLevel = OptimizationLevel.Release) + { + this.optimizationLevel = optimizationLevel; + } + public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; protected override IEnumerable GetSourceGenerators() => [typeof(TSourceGenerator)]; protected override CompilationOptions CreateCompilationOptions() { - return base.CreateCompilationOptions().WithOptimizationLevel(OptimizationLevel.Release); + return base.CreateCompilationOptions().WithOptimizationLevel(optimizationLevel); } protected override ParseOptions CreateParseOptions() diff --git a/osu.Framework.SourceGeneration/Generators/AbstractIncrementalGenerator.cs b/osu.Framework.SourceGeneration/Generators/AbstractIncrementalGenerator.cs index 60bd8c9bd..e6c79c404 100644 --- a/osu.Framework.SourceGeneration/Generators/AbstractIncrementalGenerator.cs +++ b/osu.Framework.SourceGeneration/Generators/AbstractIncrementalGenerator.cs @@ -20,11 +20,15 @@ namespace osu.Framework.SourceGeneration.Generators IncrementalValuesProvider syntaxTargets = context.SyntaxProvider.CreateSyntaxProvider( (n, _) => isSyntaxTarget(n), - (ctx, _) => returnWithEvent(new IncrementalSyntaxTarget((ClassDeclarationSyntax)ctx.Node, ctx.SemanticModel), EventDriver.OnSyntaxTargetCreated)) - .Select((t, _) => t.WithName()) - .Combine(context.CompilationProvider) - .Where(c => c.Right.Options.OptimizationLevel == OptimizationLevel.Release) - .Select((t, _) => t.Item1) + (ctx, _) => + { + if (ctx.SemanticModel.Compilation.Options.OptimizationLevel == OptimizationLevel.Debug) + return null; + + return returnWithEvent(new IncrementalSyntaxTarget((ClassDeclarationSyntax)ctx.Node, ctx.SemanticModel), EventDriver.OnSyntaxTargetCreated); + }) + .Where(t => t != null) + .Select((t, _) => t!.WithName()) .Select((t, _) => returnWithEvent(t.WithSemanticTarget(CreateSemanticTarget), EventDriver.OnSemanticTargetCreated)); // Stage 2: Separate out the old and new syntax targets for the same class object. diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs deleted file mode 100644 index 19ce63607..000000000 --- a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainer.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -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.Testing; -using osuTK; - -namespace osu.Framework.Tests.Visual.Containers -{ - public partial class TestSceneAcrylicContainer : TestScene - { - private bool isWhiteTint = true; - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Children = new Drawable[] - { - // Background texture (full screen) - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get("sample-texture") - }, - - // Left side: Semi-transparent overlay to show original texture - new Container - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White.Opacity(0.1f) // Very subtle overlay - } - }, - - // Right side: AcrylicContainer with blur effect (buffered container) - new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 0.5f, - Child = new AcrylicContainer() - }, - - // Labels - new SpriteText - { - Text = "Original Texture (No Blur)", - Font = FontUsage.Default.With(size: 24), - Colour = Colour4.White, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Position = new Vector2(-200, 20), - }, - new SpriteText - { - Text = "AcrylicContainer (Buffered Blur)", - Font = FontUsage.Default.With(size: 24), - Colour = Colour4.White, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Position = new Vector2(200, 20), - } - }; - - // Blur strength control - AddSliderStep("blur strength", 0f, 20f, 10f, strength => - { - if (Children[2] is AcrylicContainer acrylic) - acrylic.BlurStrength = strength; - }); - - // Tint colour controls - AddSliderStep("tint alpha", 0f, 1f, 0.8f, alpha => - { - if (Children[2] is AcrylicContainer acrylic) - acrylic.TintColour = Colour4.White.Opacity(alpha); - }); - - AddSliderStep("tint red", 0f, 1f, 1f, red => - { - if (Children[2] is AcrylicContainer acrylic) - { - var currentColour = acrylic.TintColour; - acrylic.TintColour = new Colour4(red, currentColour.G, currentColour.B, currentColour.A); - } - }); - - AddSliderStep("tint green", 0f, 1f, 1f, green => - { - if (Children[2] is AcrylicContainer acrylic) - { - var currentColour = acrylic.TintColour; - acrylic.TintColour = new Colour4(currentColour.R, green, currentColour.B, currentColour.A); - } - }); - - AddSliderStep("tint blue", 0f, 1f, 1f, blue => - { - if (Children[2] is AcrylicContainer acrylic) - { - var currentColour = acrylic.TintColour; - acrylic.TintColour = new Colour4(currentColour.R, currentColour.G, blue, currentColour.A); - } - }); - - AddStep("toggle tint colour", () => - { - if (Children[2] is AcrylicContainer acrylic) - { - isWhiteTint = !isWhiteTint; - acrylic.TintColour = isWhiteTint - ? Colour4.White.Opacity(0.8f) - : Colour4.Blue.Opacity(0.8f); - } - }); - - // Test different blur scenarios - AddStep("no blur", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 0; - }); - AddStep("light blur", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 5; - }); - AddStep("medium blur", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 10; - }); - AddStep("heavy blur", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.BlurStrength = 20; - }); - - // Test tint scenarios - AddStep("no tint", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0); - }); - AddStep("subtle tint", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.3f); - }); - AddStep("medium tint", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.6f); - }); - AddStep("strong tint", () => - { - if (Children[2] is AcrylicContainer acrylic) acrylic.TintColour = Colour4.White.Opacity(0.9f); - }); - - // Debug presets - AddStep("debug: high contrast", () => - { - if (Children[2] is AcrylicContainer acrylic) - { - acrylic.BlurStrength = 15f; - acrylic.TintColour = Colour4.Red.Opacity(0.7f); - } - }); - - AddStep("debug: subtle effect", () => - { - if (Children[2] is AcrylicContainer acrylic) - { - acrylic.BlurStrength = 3f; - acrylic.TintColour = Colour4.Black.Opacity(0.2f); - } - }); - - AddStep("debug: reset to default", () => - { - if (Children[2] is AcrylicContainer acrylic) - { - acrylic.BlurStrength = 10f; - acrylic.TintColour = Colour4.White.Opacity(0.8f); - } - }); - } - } -} diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs b/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs deleted file mode 100644 index edc30e8cb..000000000 --- a/osu.Framework.Tests/Visual/Containers/TestSceneAcrylicContainerNew.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -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.Testing; -using osu.Framework.Utils; -using osuTK; -using osuTK.Graphics; - -namespace osu.Framework.Tests.Visual.Containers -{ - /// - /// 测试真正的毛玻璃效果 - 使用新重构的AcrylicContainer - /// 这个容器可以对下层的任何内容进行实时模糊,包括视频、动画等 - /// - public partial class TestSceneAcrylicContainerNew : TestScene - { - private AcrylicContainer? acrylicEffect; - private Box? animatedBox; - private readonly List movingBoxes = new List(); - - [BackgroundDependencyLoader] - private void load() - { - // 创建一个彩色渐变背景层 - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Blue, Color4.Purple) - }); - - // 添加一些移动的彩色方块作为背景内容 - for (int i = 0; i < 5; i++) - { - var box = new Box - { - Size = new Vector2(100), - Colour = new Color4( - (float)RNG.NextDouble(), - (float)RNG.NextDouble(), - (float)RNG.NextDouble(), - 1f - ), - Position = new Vector2( - (float)(RNG.NextDouble() * 800), - (float)(RNG.NextDouble() * 600) - ) - }; - - Add(box); - movingBoxes.Add(box); - } - - // 添加一个持续旋转的大方块 - animatedBox = new Box - { - Size = new Vector2(200), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Yellow - }; - - Add(animatedBox); - - // 在上面添加毛玻璃效果容器 - Add(new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(100), - Child = acrylicEffect = new AcrylicContainer - { - RelativeSizeAxes = Axes.Both, - BlurStrength = 50f, - TintColour = new Color4(1, 1, 1, 0.5f), - Children = new Drawable[] - { - // 在毛玻璃效果上面显示一些文本 - new SpriteText - { - Text = "毛玻璃效果 (Acrylic Effect)\n\n此容器实时模糊背后的所有内容,\n包括背景、动画和兄弟元素。", - Font = FontUsage.Default.With(size: 30), - Colour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = true - } - } - } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // 在LoadComplete后启动动画 - // 让方块循环移动 - foreach (var box in movingBoxes) - { - box.MoveTo(new Vector2( - (float)(RNG.NextDouble() * 800), - (float)(RNG.NextDouble() * 600) - ), 3000).Then().MoveTo(new Vector2( - (float)(RNG.NextDouble() * 800), - (float)(RNG.NextDouble() * 600) - ), 3000).Loop(); - } - - // 让黄色方块持续旋转 - animatedBox?.RotateTo(360, 4000).Then().RotateTo(0, 0).Loop(); - - // 添加控制UI - AddLabel("模糊强度 (Blur Strength)"); - AddSliderStep("blur", 0f, 50f, 15f, value => - { - if (acrylicEffect != null) - acrylicEffect.BlurStrength = value; - }); - - AddLabel("着色透明度 (Tint Alpha)"); - AddSliderStep("alpha", 0f, 1f, 0.3f, value => - { - if (acrylicEffect != null) - { - var current = acrylicEffect.TintColour; - acrylicEffect.TintColour = new Color4(current.R, current.G, current.B, value); - } - }); - - AddLabel("着色颜色 (Tint Color)"); - AddStep("黑色 (Black)", () => - { - if (acrylicEffect != null) - { - var alpha = acrylicEffect.TintColour.A; - acrylicEffect.TintColour = new Color4(0, 0, 0, alpha); - } - }); - - AddStep("白色 (White)", () => - { - if (acrylicEffect != null) - { - var alpha = acrylicEffect.TintColour.A; - acrylicEffect.TintColour = new Color4(1, 1, 1, alpha); - } - }); - - AddStep("红色 (Red)", () => - { - if (acrylicEffect != null) - { - var alpha = acrylicEffect.TintColour.A; - acrylicEffect.TintColour = new Color4(1, 0, 0, alpha); - } - }); - - AddStep("蓝色 (Blue)", () => - { - if (acrylicEffect != null) - { - var alpha = acrylicEffect.TintColour.A; - acrylicEffect.TintColour = new Color4(0, 0, 1, alpha); - } - }); - - AddLabel("效果演示 (Demos)"); - AddStep("显示/隐藏毛玻璃", () => - { - if (acrylicEffect != null) - acrylicEffect.Alpha = acrylicEffect.Alpha > 0 ? 0 : 1; - }); - - AddStep("脉冲动画", () => - { - acrylicEffect?.ScaleTo(1.1f, 500, Easing.OutQuint) - .Then() - .ScaleTo(1f, 500, Easing.InQuint); - }); - } - } -} diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneCubicBezierEasing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneCubicBezierEasing.cs new file mode 100644 index 000000000..3f3bae265 --- /dev/null +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneCubicBezierEasing.cs @@ -0,0 +1,329 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +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.Graphics.Transforms; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osuTK; + +namespace osu.Framework.Tests.Visual.Drawables +{ + public partial class TestSceneCubicBezierEasing : TestScene + { + private SpriteText easingText = null!; + private EasingEditor easingEditor = null!; + + [Resolved] + private Clipboard clipboard { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Horizontal = 100 }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new Drawable[] + { + easingEditor = new EasingEditor + { + Size = new Vector2(200) + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(4), + Children = new Drawable[] + { + easingText = new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 420, + }, + new BasicButton + { + AutoSizeAxes = Axes.Y, + Width = 50, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "Copy", + FlashColour = FrameworkColour.BlueGreen.Lighten(0.5f), + FlashDuration = 1000, + Action = () => clipboard.SetText(easingText.Text.ToString()), + } + } + }, + new EasingPreview(easingEditor.EasingFunction.GetBoundCopy()) + } + } + } + }; + + easingEditor.EasingFunction.BindValueChanged(e => + { + var easing = e.NewValue; + + easingText.Text = FormattableString.Invariant($"new {nameof(CubicBezierEasingFunction)}({easing.X1:0.##}, {easing.Y1:0.##}, {easing.X2:0.##}, {easing.Y2:0.##})"); + }); + } + + private partial class EasingEditor : CompositeDrawable + { + private readonly Bindable p1 = new Bindable(new Vector2(0.5f, 0f)); + private readonly Bindable p2 = new Bindable(new Vector2(0.5f, 1f)); + private readonly SmoothPath path; + private readonly Box line1, line2; + + public readonly Bindable EasingFunction = new Bindable(); + + public EasingEditor() + { + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + }, + path = new SmoothPath + { + Position = new Vector2(1, -1), + PathRadius = 1f, + Anchor = Anchor.BottomLeft, + }, + line1 = new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Anchor = Anchor.BottomLeft, + Origin = Anchor.CentreLeft, + EdgeSmoothness = new Vector2(1), + Alpha = 0.1f, + }, + line2 = new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Anchor = Anchor.TopRight, + Origin = Anchor.CentreRight, + EdgeSmoothness = new Vector2(1), + Alpha = 0.1f, + }, + new ControlPointHandle + { + ControlPoint = { BindTarget = p1 }, + }, + new ControlPointHandle + { + ControlPoint = { BindTarget = p2 }, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + p1.BindValueChanged(_ => Scheduler.AddOnce(easingChanged)); + p2.BindValueChanged(_ => Scheduler.AddOnce(easingChanged)); + easingChanged(); + } + + private void easingChanged() + { + path.ClearVertices(); + + var easing = EasingFunction.Value = new CubicBezierEasingFunction(p1.Value.X, p1.Value.Y, p2.Value.X, p2.Value.Y); + + for (double d = 0; d < 1; d += 0.01) + { + double value = easing.ApplyEasing(d); + + path.AddVertex(new Vector2((float)d * DrawWidth, 1 - (float)value * DrawHeight)); + } + + path.AddVertex(new Vector2(DrawWidth, 1 - (float)easing.ApplyEasing(1) * DrawHeight)); + + path.OriginPosition = path.PositionInBoundingBox(new Vector2()); + + line1.Width = p1.Value.Length; + line1.Rotation = -MathHelper.RadiansToDegrees(MathF.Atan2(p1.Value.Y, p1.Value.X)); + + line2.Width = Vector2.Distance(p2.Value, Vector2.One); + line2.Rotation = -MathHelper.RadiansToDegrees(MathF.Atan2(1 - p2.Value.Y, 1 - p2.Value.X)); + } + } + + private partial class ControlPointHandle : CompositeDrawable + { + public readonly Bindable ControlPoint = new Bindable(); + + public ControlPointHandle() + { + RelativePositionAxes = Axes.Both; + Size = new Vector2(20); + Origin = Anchor.Centre; + + InternalChild = new Circle + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ControlPoint.BindValueChanged(p => Position = new Vector2(p.NewValue.X, 1 - p.NewValue.Y), true); + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + + var position = Vector2.Divide(Parent!.ToLocalSpace(e.ScreenSpaceMousePosition), Parent.ChildSize); + + ControlPoint.Value = new Vector2( + float.Round(float.Clamp(position.X, 0, 1), 2), + float.Round(1f - position.Y, 2) + ); + } + + protected override bool OnHover(HoverEvent e) + { + this.ScaleTo(1.35f, 50); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.ScaleTo(1f, 50); + } + } + + private partial class EasingPreview : CompositeDrawable + { + private readonly Box box; + private readonly SpriteText durationText; + + private bool flipped; + private readonly IBindable easingFunction; + + private readonly BindableDouble duration = new BindableDouble + { + Value = 1000, + MinValue = 100, + MaxValue = 5000, + }; + + public EasingPreview(IBindable easingFunction) + { + this.easingFunction = easingFunction; + + Width = 400; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + Height = 25, + RowDimensions = [new Dimension(GridSizeMode.AutoSize)], + ColumnDimensions = [new Dimension(GridSizeMode.Absolute, 150), new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize)], + + Content = new Drawable?[][] + { + [ + durationText = new SpriteText + { + Text = "Duration: 1000" + }, + new BasicSliderBar + { + Current = duration, + RelativeSizeAxes = Axes.X, + Height = 20, + }, + null, + new BasicButton + { + RelativeSizeAxes = Axes.Y, + Width = 50, + Text = "Play", + FlashColour = FrameworkColour.BlueGreen.Lighten(0.5f), + Action = play, + } + ] + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 50 }, + Child = box = new Box + { + RelativePositionAxes = Axes.X, + Size = new Vector2(50), + }, + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + duration.BindValueChanged(e => durationText.Text = FormattableString.Invariant($"Duration: {e.NewValue:N0}"), true); + } + + private void play() + { + box.MoveToX(flipped ? 0 : 1, duration.Value, easingFunction.Value); + + flipped = !flipped; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneFrostedGlass.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneFrostedGlass.cs new file mode 100644 index 000000000..ecf803ded --- /dev/null +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneFrostedGlass.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Graphics +{ + public partial class TestSceneFrostedGlass : FrameworkTestScene + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Container backgroundCircles; + + Children = new Drawable[] + { + // Background with animated circles + backgroundCircles = new Container + { + RelativeSizeAxes = Axes.Both, + }, + // Frosted glass container overlaying part of the screen + // new FrostedGlassContainer + // { + // RelativeSizeAxes = Axes.Both, + // Width = 0.5f, + // BlurSigma = new Vector2(10), + // Children = new Drawable[] + // { + // new Box + // { + // RelativeSizeAxes = Axes.Both, + // Colour = new Color4(1, 1, 1, 0.5f), + // }, + // new SpriteText + // { + // Text = "Frosted Glass Effect", + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Colour = Color4.Black, + // } + // } + // }, + new Label("Background"), + new Label("FrostedGlassContainer") + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + } + }; + + const float circle_radius = 0.05f; + const float spacing = 0.01f; + + for (float xPos = 0; xPos < 1; xPos += circle_radius + spacing) + { + for (float yPos = 0; yPos < 1; yPos += circle_radius + spacing) + { + backgroundCircles.Add(new CircularProgress + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(circle_radius), + RelativePositionAxes = Axes.Both, + Position = new Vector2(xPos, yPos), + Progress = 1, + Colour = Color4.HotPink, + }); + } + } + } + + private partial class Label : Container + { + public Label(string text) + { + AutoSizeAxes = Axes.Both; + Margin = new MarginPadding(10); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + new SpriteText + { + Text = text, + Margin = new MarginPadding(10) + } + }; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneDisplayBoundsWindowBorder.cs b/osu.Framework.Tests/Visual/Platform/TestSceneDisplayBoundsWindowBorder.cs new file mode 100644 index 000000000..d96b71c27 --- /dev/null +++ b/osu.Framework.Tests/Visual/Platform/TestSceneDisplayBoundsWindowBorder.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; + +namespace osu.Framework.Tests.Visual.Platform +{ + public partial class TestSceneDisplayBoundsWindowBorder : FrameworkTestScene + { + [Test] + public void TestDisplayUsableBounds() + { + AddStep("Set up scene", () => Child = new WindowDisplaysPreview(true) + { + RelativeSizeAxes = Axes.Both, + }); + } + + [Test] + public void TestWindowBorder() + { + AddStep("Set up scene", () => Child = new WindowDisplaysPreview(false, true) + { + RelativeSizeAxes = Axes.Both, + }); + } + + [Test] + public void TestBoth() + { + AddStep("Set up scene", () => Child = new WindowDisplaysPreview(true, true) + { + RelativeSizeAxes = Axes.Both, + }); + } + } +} diff --git a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs index 78fec929c..bba4f05f3 100644 --- a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs +++ b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -27,6 +28,7 @@ namespace osu.Framework.Tests.Visual.Platform private readonly Container paddedContainer; private readonly Container screenContainer; private readonly Container windowContainer; + private readonly Container? borderContainer; private Vector2 screenContainerOffset; private static readonly Color4 active_fill = new Color4(255, 138, 104, 255); @@ -35,13 +37,18 @@ namespace osu.Framework.Tests.Visual.Platform private static readonly Color4 screen_stroke = new Color4(244, 137, 25, 255); private static readonly Color4 window_fill = new Color4(95, 113, 197, 255); private static readonly Color4 window_stroke = new Color4(36, 59, 166, 255); + private static readonly Color4 window_border_fill = new Color4(85, 207, 89, 200); + private static readonly Color4 window_border_stroke = new Color4(50, 122, 53, 255); private IWindow? window; private readonly Bindable windowMode = new Bindable(); private readonly Bindable currentDisplay = new Bindable(); - public WindowDisplaysPreview() + private readonly bool showDisplayUsableBounds; + + public WindowDisplaysPreview(bool showDisplayUsableBounds = false, bool showWindowBorder = false) { + this.showDisplayUsableBounds = showDisplayUsableBounds; Child = new Container { RelativeSizeAxes = Axes.Both, @@ -85,6 +92,22 @@ namespace osu.Framework.Tests.Visual.Platform } } }; + + if (showWindowBorder) + { + screenContainer.Add(borderContainer = new Container + { + BorderColour = window_border_stroke, + BorderThickness = 20, + Masking = true, + Depth = -5, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = window_border_fill + } + }); + } } [BackgroundDependencyLoader] @@ -110,13 +133,13 @@ namespace osu.Framework.Tests.Visual.Platform private void refreshScreens(IEnumerable displays) { - screenContainer.RemoveAll(d => d != windowContainer, false); + screenContainer.RemoveAll(d => d != windowContainer && d != borderContainer, false); var bounds = new RectangleI(); foreach (var display in displays) { - screenContainer.Add(createScreen(display, window.AsNonNull().CurrentDisplayBindable.Value.Index)); + screenContainer.AddRange(createScreen(display, window.AsNonNull().CurrentDisplayBindable.Value.Index)); bounds = RectangleI.Union(bounds, new RectangleI(display.Bounds.X, display.Bounds.Y, display.Bounds.Width, display.Bounds.Height)); } @@ -130,11 +153,11 @@ namespace osu.Framework.Tests.Visual.Platform screenContainer.Size = bounds.Size; } - private Container createScreen(Display display, int activeDisplayIndex) + private IEnumerable createScreen(Display display, int activeDisplayIndex) { bool isActive = display.Index == activeDisplayIndex; - return new Container + yield return new Container { X = display.Bounds.X, Y = display.Bounds.Y, @@ -166,6 +189,30 @@ namespace osu.Framework.Tests.Visual.Platform } } }; + + if (showDisplayUsableBounds) + { + yield return new Container + { + X = display.UsableBounds.X, + Y = display.UsableBounds.Y, + Width = display.UsableBounds.Width, + Height = display.UsableBounds.Height, + + BorderColour = isActive ? Color4.MediumPurple : Color4.Green, + BorderThickness = 20, + Masking = true, + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = (isActive ? Color4.MediumPurple : Color4.Green).Opacity(0.2f) + }, + } + }; + } } private string modeName(DisplayMode mode) => $"{mode.Size.Width}x{mode.Size.Height}@{mode.RefreshRate}"; @@ -183,6 +230,17 @@ namespace osu.Framework.Tests.Visual.Platform windowContainer.Height = fullscreen ? currentBounds.Height : window.Size.Height; windowContainer.Position -= screenContainerOffset; windowCaption.Text = $"{windowMode}\nSize: {window.Size.Width}x{window.Size.Height}\nClient: {window.ClientSize.Width}x{window.ClientSize.Height}"; + + if (borderContainer != null) + { + var borderSize = window.BorderSize.Value; + + borderContainer.X = window.Position.X - borderSize.Left; + borderContainer.Y = window.Position.Y - borderSize.Top; + borderContainer.Width = windowContainer.Width + borderSize.TotalHorizontal; + borderContainer.Height = windowContainer.Height + borderSize.TotalVertical; + borderContainer.Position -= screenContainerOffset; + } } protected override void Update() diff --git a/osu.Framework/Extensions/InputKeyExtensions.cs b/osu.Framework/Extensions/InputKeyExtensions.cs index 50b83acd0..0fc997247 100644 --- a/osu.Framework/Extensions/InputKeyExtensions.cs +++ b/osu.Framework/Extensions/InputKeyExtensions.cs @@ -38,5 +38,43 @@ namespace osu.Framework.Extensions return false; } } + + /// + /// If is a physical key which is covered by another virtual key, returns that virtual key. + /// Otherwise, returns . + /// + /// + /// + /// > InputKey.LShift.GetVirtualKey() + /// Shift + /// > InputKey.RSuper.GetVirtualKey() + /// Super + /// > InputKey.A.GetVirtualKey() + /// null + /// + /// + public static InputKey? GetVirtualKey(this InputKey key) + { + switch (key) + { + case InputKey.LShift: + case InputKey.RShift: + return InputKey.Shift; + + case InputKey.LControl: + case InputKey.RControl: + return InputKey.Control; + + case InputKey.LAlt: + case InputKey.RAlt: + return InputKey.Alt; + + case InputKey.LSuper: + case InputKey.RSuper: + return InputKey.Super; + } + + return null; + } } } diff --git a/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs b/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs deleted file mode 100644 index aef25fdad..000000000 --- a/osu.Framework/Graphics/Containers/AcrylicBlurLayer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using osuTK.Graphics; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Allocation; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A specialized layer for acrylic blur effects that fills the entire area and applies blur to background content. - /// This layer handles the actual blurring logic using a custom shader and manages background capture automatically. - /// - internal partial class AcrylicBlurLayer : Drawable - { - /// - /// The strength of the blur effect. - /// - public float BlurStrength { get; set; } = 10f; - - /// - /// The tint colour applied over the blurred background. - /// - public Color4 TintColour { get; set; } = Color4.White; - - /// - /// The darkening factor for depth effect. - /// - public float DarkenFactor { get; set; } = 0.1f; - - [Resolved] - private IRenderer? renderer { get; set; } - - [Resolved] - private ShaderManager shaderManager { get; set; } = null!; - - public AcrylicBlurLayer() - { - RelativeSizeAxes = Axes.Both; - } - - protected override DrawNode CreateDrawNode() => new AcrylicBlurLayerDrawNode(this); - } -} diff --git a/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs b/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs deleted file mode 100644 index c6e5deab3..000000000 --- a/osu.Framework/Graphics/Containers/AcrylicBlurLayerDrawNode.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Shaders.Types; -using osuTK; -using osuTK.Graphics; -using System.Runtime.InteropServices; -using osu.Framework.Graphics.Textures; - -namespace osu.Framework.Graphics.Containers -{ - internal partial class AcrylicBlurLayer - { - private class AcrylicBlurLayerDrawNode : DrawNode - { - protected new AcrylicBlurLayer Source => (AcrylicBlurLayer)base.Source; - - private RectangleF drawRectangle; - private IShader acrylicShader; - private IFrameBuffer backgroundBuffer; - private IUniformBuffer acrylicParametersBuffer; - - public AcrylicBlurLayerDrawNode(AcrylicBlurLayer source) - : base(source) - { - } - - public override void ApplyState() - { - base.ApplyState(); - drawRectangle = Source.ScreenSpaceDrawQuad.AABBFloat; - } - - protected override void Draw(IRenderer renderer) - { - // 检查依赖是否可用 - if (Source.renderer == null || Source.shaderManager == null) - { - // 如果依赖不可用,跳过绘制 - return; - } - - // 获取或创建背景缓冲区(延迟到绘制线程) - if (backgroundBuffer == null) - { - try - { - backgroundBuffer = renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear); - } - catch - { - // 如果创建失败,跳过绘制 - return; - } - } - - // 捕获当前屏幕到缓冲区 - try - { - renderer.CaptureScreenToFrameBuffer(backgroundBuffer); - } - catch - { - // 如果捕获失败,使用固定的背景色 - renderer.Clear(new ClearInfo(Color4.Gray)); - return; - } - - // 尝试加载毛玻璃着色器 - if (acrylicShader == null) - { - try - { - acrylicShader = Source.shaderManager.Load("AcrylicBlur", "Texture"); - acrylicParametersBuffer = renderer.CreateUniformBuffer(); - } - catch (Exception ex) - { - // 如果加载失败,使用备用方案:直接绘制背景 - Console.WriteLine($"Failed to load acrylic shader: {ex.Message}"); - renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(Source.TintColour)); - return; - } - } - - // 使用着色器绘制 - if (acrylicShader != null && acrylicParametersBuffer != null) - { - acrylicParametersBuffer.Data = acrylicParametersBuffer.Data with - { - TexSize = backgroundBuffer.Size, - Radius = (int)Source.BlurStrength, - Sigma = Source.BlurStrength / 2f, - BlurDirection = Vector2.One, - TintColour = new Vector4(Source.TintColour.R, Source.TintColour.G, Source.TintColour.B, Source.TintColour.A), - DarkenFactor = Source.DarkenFactor - }; - - acrylicShader.BindUniformBlock("m_AcrylicParameters", acrylicParametersBuffer); - acrylicShader.Bind(); - - // 绘制背景到当前区域 - renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(new Color4(1, 1, 1, 1))); - - acrylicShader.Unbind(); - } - else - { - // 备用方案:直接绘制背景 - renderer.DrawFrameBuffer(backgroundBuffer, drawRectangle, ColourInfo.SingleColour(Source.TintColour)); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - acrylicParametersBuffer?.Dispose(); - backgroundBuffer?.Dispose(); - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - private record struct AcrylicParameters - { - public UniformVector2 TexSize; - public UniformInt Radius; - public UniformFloat Sigma; - public UniformVector2 BlurDirection; - public UniformVector4 TintColour; - public UniformFloat DarkenFactor; - private readonly UniformPadding12 pad1; - } - } - } -} diff --git a/osu.Framework/Graphics/Containers/AcrylicContainer.cs b/osu.Framework/Graphics/Containers/AcrylicContainer.cs deleted file mode 100644 index 1ae23d4be..000000000 --- a/osu.Framework/Graphics/Containers/AcrylicContainer.cs +++ /dev/null @@ -1,82 +0,0 @@ -using osuTK.Graphics; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Shapes; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container that applies a true acrylic/mica effect by blurring the content behind it. - /// This implementation uses a layered approach with a blur background layer and a darkening overlay. - /// The effect is applied regardless of drawing order and adapts to background changes in real-time. - /// - public partial class AcrylicContainer : Container - { - /// - /// The strength of the blur effect applied to the background content. - /// - public float BlurStrength { get; set; } = 10f; - - /// - /// The tint colour applied over the blurred background. - /// - public Color4 TintColour { get; set; } = Color4.White; - - /// - /// The darkening factor applied to create depth. - /// - public float DarkenFactor { get; set; } = 0.1f; - - private AcrylicBlurLayer blurLayer = null!; - private Box darkenLayer = null!; - - [Resolved] - private IRenderer? renderer { get; set; } - - [Resolved] - private ShaderManager shaderManager { get; set; } = null!; - - /// - /// Constructs a new acrylic container. - /// - public AcrylicContainer() - { - // 默认不设置RelativeSizeAxes,让用户决定 - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // 添加虚化背景层 - Add(blurLayer = new AcrylicBlurLayer - { - RelativeSizeAxes = Axes.Both, - BlurStrength = BlurStrength, - TintColour = TintColour, - DarkenFactor = DarkenFactor - }); - - // 添加暗化层 - Add(darkenLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(0, 0, 0, DarkenFactor), - Depth = -1 // 确保在虚化层之上 - }); - } - - protected override void Update() - { - base.Update(); - - // 同步属性到层 - blurLayer.BlurStrength = BlurStrength; - blurLayer.TintColour = TintColour; - blurLayer.DarkenFactor = DarkenFactor; - - darkenLayer.Colour = new Color4(0, 0, 0, DarkenFactor); - } - } -} diff --git a/osu.Framework/Graphics/Containers/AutoBackgroundCapture.cs b/osu.Framework/Graphics/Containers/AutoBackgroundCapture.cs deleted file mode 100644 index e804e9686..000000000 --- a/osu.Framework/Graphics/Containers/AutoBackgroundCapture.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 自动背景捕获组件 - 在游戏中自动管理背景缓冲区更新 - -using osu.Framework.Graphics.Rendering; -using osu.Framework.Allocation; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A component that automatically captures the screen background for acrylic effects. - /// Add this to your game once to enable automatic background capture for all AcrylicContainer instances. - /// - public partial class AutoBackgroundCapture : Drawable - { - [Resolved] - private IRenderer? renderer { get; set; } - - private bool initialized; - - protected override void LoadComplete() - { - base.LoadComplete(); - // 初始化延迟到Update方法中进行 - } - - protected override void Update() - { - base.Update(); - - // 延迟初始化,确保在正确的线程中 - if (!initialized && renderer != null) - { - BackgroundBufferManager.Instance.Initialize(renderer); - initialized = true; - } - - // 自动更新背景缓冲区 - BackgroundBufferManager.Instance.UpdateBackgroundBuffer(); - } - } -} diff --git a/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs b/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs deleted file mode 100644 index 87a19cdc9..000000000 --- a/osu.Framework/Graphics/Containers/BackgroundBufferManager.cs +++ /dev/null @@ -1,87 +0,0 @@ -// 全局背景缓冲区管理器 - 自动管理背景捕获 - -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Textures; - -namespace osu.Framework.Graphics.Containers -{ - public class BackgroundBufferManager - { - private static BackgroundBufferManager? instance; - private static readonly object lockObject = new object(); - - public static BackgroundBufferManager Instance - { - get - { - if (instance == null) - { - lock (lockObject) - { - instance ??= new BackgroundBufferManager(); - } - } - - return instance; - } - } - - private IFrameBuffer? backgroundBuffer; - private IRenderer? renderer; - private bool initialized; - - // 初始化管理器(在游戏启动时调用一次) - public void Initialize(IRenderer renderer) - { - this.renderer = renderer; - initialized = false; - } - - // 获取背景缓冲区 - 不再自动创建,只返回已存在的 - public IFrameBuffer? GetBackgroundBuffer() - { - return backgroundBuffer; - } - - // 获取或创建背景缓冲区(在绘制线程调用 - 延迟创建模式) - public IFrameBuffer GetOrCreateBackgroundBuffer(IRenderer renderer) - { - if (backgroundBuffer == null) - { - this.renderer = renderer; - backgroundBuffer = renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear); - initialized = true; - } - - return backgroundBuffer; - } - - // 确保背景缓冲区已创建(在绘制线程安全的地方调用) - public void EnsureBackgroundBuffer() - { - if (!initialized && renderer != null) - { - backgroundBuffer ??= renderer.CreateFrameBuffer(null, TextureFilteringMode.Linear); - initialized = true; - } - } - - // 更新背景缓冲区(在UpdateAfterChildren中调用) - public void UpdateBackgroundBuffer() - { - // 暂时禁用屏幕捕获,直到实现正确的API - // if (renderer is Renderer concreteRenderer && backgroundBuffer != null) - // { - // concreteRenderer.CaptureScreenToFrameBuffer(backgroundBuffer); - // } - } - - // 清理资源 - public void Dispose() - { - backgroundBuffer?.Dispose(); - backgroundBuffer = null; - renderer = null; - } - } -} diff --git a/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs b/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs deleted file mode 100644 index 004ef875f..000000000 --- a/osu.Framework/Graphics/Containers/FrostedGlassContainer.cs +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Utils; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Layout; - -namespace osu.Framework.Graphics.Containers -{ - /// - /// A container that renders the background (from the screen) to an internal framebuffer, applies blur, and then - /// blits the framebuffer to the screen, allowing for frosted glass effects on the background. - /// If all children are of a specific non- type, use the - /// generic version . - /// - public partial class FrostedGlassContainer : FrostedGlassContainer - { - /// - public FrostedGlassContainer(RenderBufferFormat[] formats = null, bool pixelSnapping = false) - : base(formats, pixelSnapping) - { - } - } - - /// - /// A container that renders the background (from the screen) to an internal framebuffer, applies blur, and then - /// blits the framebuffer to the screen, allowing for frosted glass effects on the background. - /// - public partial class FrostedGlassContainer : Container, IBufferedContainer, IBufferedDrawable - where T : Drawable - { - 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 => 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 => blurRotation; - set - { - if (blurRotation == value) - return; - - blurRotation = value; - ForceRedraw(); - } - } - - private ColourInfo effectColour = Color4.White; - - /// - /// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is . - /// - public ColourInfo EffectColour - { - get => effectColour; - set - { - if (effectColour.Equals(value)) - return; - - effectColour = value; - Invalidate(Invalidation.DrawNode); - } - } - - private BlendingParameters effectBlending = BlendingParameters.Inherit; - - /// - /// The to use after applying all effects. Default is . - /// - public BlendingParameters EffectBlending - { - get => effectBlending; - set - { - if (effectBlending == value) - return; - - effectBlending = 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 => backgroundColour; - set - { - if (backgroundColour == value) - return; - - backgroundColour = value; - ForceRedraw(); - } - } - - private Vector2 frameBufferScale = Vector2.One; - - public Vector2 FrameBufferScale - { - get => frameBufferScale; - set - { - if (frameBufferScale == value) - return; - - frameBufferScale = value; - ForceRedraw(); - } - } - - private float grayscaleStrength; - - public float GrayscaleStrength - { - get => grayscaleStrength; - set - { - if (grayscaleStrength == value) - return; - - grayscaleStrength = value; - ForceRedraw(); - } - } - - /// - /// Forces a redraw of the framebuffer before it is blitted the next time. - /// - public void ForceRedraw() => Invalidate(Invalidation.DrawNode); - - /// - /// In order to signal the draw thread to re-draw the frosted glass 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; - - private readonly BufferedContainerDrawNodeSharedData sharedData; - - public IShader TextureShader { get; private set; } - - private IShader blurShader; - private IShader grayscaleShader; - - /// - /// Constructs an empty frosted glass container. - /// - /// The render buffer formats attached to the frame buffer of this . - /// - /// Whether the frame buffer position should be snapped to the nearest pixel when blitting. - /// This amounts to setting the texture filtering mode to "nearest". - /// - public FrostedGlassContainer(RenderBufferFormat[] formats = null, bool pixelSnapping = false) - { - sharedData = new BufferedContainerDrawNodeSharedData(formats, pixelSnapping, false); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders) - { - TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); - blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); - grayscaleShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.GRAYSCALE); - } - - protected override DrawNode CreateDrawNode() => new FrostedGlassDrawNode(this, sharedData); - - /// - /// The blending which uses for the effect. - /// - public BlendingParameters DrawEffectBlending - { - get - { - BlendingParameters blending = EffectBlending; - - blending.CopyFromParent(Blending); - blending.ApplyDefaultToInherited(); - - return blending; - } - } - - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - bool result = base.OnInvalidate(invalidation, source); - - if ((invalidation & Invalidation.DrawNode) > 0) - { - ++updateVersion; - result = true; - } - - return result; - } - - public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo; - - // Children should not receive the true colour to avoid colour doubling when the frame-buffers are rendered to the back-buffer. - public override DrawColourInfo DrawColourInfo - { - get - { - var blending = Blending; - blending.ApplyDefaultToInherited(); - - return new DrawColourInfo(Color4.White, blending); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - sharedData.Dispose(); - } - } -} diff --git a/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs b/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs deleted file mode 100644 index 8c1b86f6a..000000000 --- a/osu.Framework/Graphics/Containers/FrostedGlassContainer_DrawNode.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using System; -using System.Runtime.InteropServices; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Shaders.Types; -using osu.Framework.Utils; -using osu.Framework.Statistics; -using osu.Framework.Allocation; - -namespace osu.Framework.Graphics.Containers -{ - public partial class FrostedGlassContainer - { - private class FrostedGlassDrawNode : BufferedDrawNode, ICompositeDrawNode - { - protected new FrostedGlassContainer Source => (FrostedGlassContainer)base.Source; - - protected new CompositeDrawableDrawNode Child => (CompositeDrawableDrawNode)base.Child; - - private ColourInfo effectColour; - private BlendingParameters effectBlending; - - private Vector2 blurSigma; - private Vector2I blurRadius; - private float blurRotation; - private float grayscaleStrength; - - private long updateVersion; - private IShader blurShader; - private IShader grayscaleShader; - - public FrostedGlassDrawNode(FrostedGlassContainer source, BufferedContainerDrawNodeSharedData sharedData) - : base(source, new CompositeDrawableDrawNode(source), sharedData) - { - } - - public override void ApplyState() - { - base.ApplyState(); - - updateVersion = Source.updateVersion; - - effectColour = Source.EffectColour; - effectBlending = Source.DrawEffectBlending; - - blurSigma = Source.BlurSigma; - blurRadius = new Vector2I(Blur.KernelSize(blurSigma.X), Blur.KernelSize(blurSigma.Y)); - blurRotation = Source.BlurRotation; - grayscaleStrength = Source.GrayscaleStrength; - - blurShader = Source.blurShader; - grayscaleShader = Source.grayscaleShader; - } - - protected override long GetDrawVersion() => updateVersion; - - protected override void PopulateContents(IRenderer renderer) - { - // Capture the screen to the main buffer - renderer.CaptureScreenToFrameBuffer(SharedData.MainBuffer); - - // Then apply effects - base.PopulateContents(renderer); - } - - protected override void DrawContents(IRenderer renderer) - { - renderer.SetBlend(effectBlending); - - ColourInfo finalEffectColour = DrawColourInfo.Colour; - finalEffectColour.ApplyChild(effectColour); - - renderer.DrawFrameBuffer(SharedData.CurrentEffectBuffer, DrawRectangle, finalEffectColour); - - // Draw children on top - DrawOther(Child, renderer); - } - - private IUniformBuffer blurParametersBuffer; - - private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation) - { - blurParametersBuffer ??= renderer.CreateUniformBuffer(); - - IFrameBuffer current = SharedData.CurrentEffectBuffer; - IFrameBuffer target = SharedData.GetNextEffectBuffer(); - - renderer.SetBlend(BlendingParameters.None); - - using (BindFrameBuffer(target)) - { - float radians = float.DegreesToRadians(blurRotation); - - blurParametersBuffer.Data = blurParametersBuffer.Data with - { - Radius = kernelRadius, - Sigma = sigma, - TexSize = current.Size, - Direction = new Vector2(MathF.Cos(radians), MathF.Sin(radians)) - }; - - blurShader.BindUniformBlock("m_BlurParameters", blurParametersBuffer); - blurShader.Bind(); - renderer.DrawFrameBuffer(current, new RectangleF(0, 0, current.Texture.Width, current.Texture.Height), ColourInfo.SingleColour(Color4.White)); - blurShader.Unbind(); - } - } - - public List Children - { - get => Child.Children; - set => Child.Children = value; - } - - public bool AddChildDrawNodes => RequiresRedraw; - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - blurParametersBuffer?.Dispose(); - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - private record struct BlurParameters - { - public UniformVector2 TexSize; - public UniformInt Radius; - public UniformFloat Sigma; - public UniformVector2 Direction; - private readonly UniformPadding8 pad1; - } - } - - private class BufferedContainerDrawNodeSharedData : BufferedDrawNodeSharedData - { - public BufferedContainerDrawNodeSharedData(RenderBufferFormat[] mainBufferFormats, bool pixelSnapping, bool clipToRootNode) - : base(2, mainBufferFormats, pixelSnapping, clipToRootNode) - { - } - } - } -} diff --git a/osu.Framework/Graphics/Containers/TextFlowContainer.cs b/osu.Framework/Graphics/Containers/TextFlowContainer.cs index 8fc1dfeed..cecbb1c84 100644 --- a/osu.Framework/Graphics/Containers/TextFlowContainer.cs +++ b/osu.Framework/Graphics/Containers/TextFlowContainer.cs @@ -535,9 +535,9 @@ namespace osu.Framework.Graphics.Containers // Things will be accounted for accurately later. // All calls to `spacingFactor()` in the original code thus reduce to returning (0,0). if (c.RelativeAnchorPosition != Vector2.Zero) - throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {RelativeAnchorPosition}s. Only (0,0) is supported."); + throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {nameof(RelativeAnchorPosition)}s. Only (0,0) is supported."); if (c.RelativeOriginPosition != Vector2.Zero) - throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {RelativeOriginPosition}s. Only (0,0) is supported."); + throw new InvalidOperationException($"All drawables in a {nameof(TextFlowContainer)} must not specify custom {nameof(RelativeOriginPosition)}s. Only (0,0) is supported."); // Populate running variables with sane initial values. if (i == 0) diff --git a/osu.Framework/Graphics/Drawables/ScreenBlurDrawable.cs b/osu.Framework/Graphics/Drawables/ScreenBlurDrawable.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs index 85c69475d..73a02e943 100644 --- a/osu.Framework/Graphics/Lines/Path.cs +++ b/osu.Framework/Graphics/Lines/Path.cs @@ -10,9 +10,11 @@ using osuTK; using osu.Framework.Graphics.Shaders; using osu.Framework.Allocation; using System.Collections.Generic; +using System.Runtime.InteropServices; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Layout; using osuTK.Graphics; @@ -34,8 +36,8 @@ namespace osu.Framework.Graphics.Lines [BackgroundDependencyLoader] private void load(ShaderManager shaders) { - TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); - pathShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE); + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Path"); + pathShader = shaders.Load("PathPrepass", "PathPrepass"); } private readonly List vertices = new List(); @@ -291,20 +293,12 @@ namespace osu.Framework.Graphics.Lines // The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer. public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending); - private Color4 backgroundColour = new Color4(0, 0, 0, 0); + private static readonly Color4 background_colour = new Color4(0, 0, 0, 0); /// /// The background colour to be used for the frame buffer this path is rendered to. /// - public virtual Color4 BackgroundColour - { - get => backgroundColour; - set - { - backgroundColour = value; - Invalidate(Invalidation.DrawNode); - } - } + public Color4 BackgroundColour => background_colour; public long PathInvalidationID { get; private set; } @@ -321,7 +315,7 @@ namespace osu.Framework.Graphics.Lines return result; } - private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderBufferFormat.D16 }, clipToRootNode: true); + private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(clipToRootNode: true); protected override DrawNode CreateDrawNode() => new PathBufferedDrawNode(this, new PathDrawNode(this), sharedData); @@ -335,11 +329,38 @@ namespace osu.Framework.Graphics.Lines } private long pathInvalidationID = -1; + private Texture texture; + private Vector4 textureRect; + private IUniformBuffer parametersBuffer; public override void ApplyState() { base.ApplyState(); pathInvalidationID = Source.PathInvalidationID; + texture = Source.Texture; + + var rect = texture.GetTextureRect(); + textureRect = new Vector4(rect.Left, rect.Top, rect.Width, rect.Height); + } + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new PathTextureParameters + { + TexRect1 = textureRect, + }; + shader.BindUniformBlock("m_PathTextureParameters", parametersBuffer); + + texture?.Bind(1); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct PathTextureParameters + { + public UniformVector4 TexRect1; } protected override long GetDrawVersion() => pathInvalidationID; diff --git a/osu.Framework/Graphics/Lines/Path_DrawNode.cs b/osu.Framework/Graphics/Lines/Path_DrawNode.cs index aae041d7e..428714213 100644 --- a/osu.Framework/Graphics/Lines/Path_DrawNode.cs +++ b/osu.Framework/Graphics/Lines/Path_DrawNode.cs @@ -2,17 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Textures; using osuTK; using System; using System.Collections.Generic; -using osuTK.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using System.Diagnostics; +using System.Runtime.InteropServices; using osu.Framework.Utils; +using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Lines { @@ -26,12 +25,10 @@ namespace osu.Framework.Graphics.Lines private readonly List segments = new List(); - private Texture? texture; - private Vector2 drawSize; private float radius; private IShader? pathShader; - private IVertexBatch? triangleBatch; + private IVertexBatch? triangleBatch; public PathDrawNode(Path source) : base(source) @@ -45,8 +42,6 @@ namespace osu.Framework.Graphics.Lines segments.Clear(); segments.AddRange(Source.segments); - texture = Source.Texture; - drawSize = Source.DrawSize; radius = Source.PathRadius; pathShader = Source.pathShader; } @@ -55,136 +50,65 @@ namespace osu.Framework.Graphics.Lines { base.Draw(renderer); - if (texture?.Available != true || segments.Count == 0 || pathShader == null) + if (segments.Count == 0 || pathShader == null || radius == 0f) return; // We multiply the size args 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. - triangleBatch ??= renderer.CreateLinearBatch(max_res * 200 * 3, 10, PrimitiveTopology.Triangles); + triangleBatch ??= renderer.CreateLinearBatch(max_res * 200 * 3, 10, PrimitiveTopology.Triangles); renderer.PushLocalMatrix(DrawInfo.Matrix); - renderer.PushDepthInfo(DepthInfo.Default); - // Blending is removed to allow for correct blending between the wedges of the path. - renderer.SetBlend(BlendingParameters.None); + renderer.SetBlend(new BlendingParameters + { + Source = BlendingType.One, + Destination = BlendingType.One, + SourceAlpha = BlendingType.One, + DestinationAlpha = BlendingType.One, + RGBEquation = BlendingEquation.Max, + AlphaEquation = BlendingEquation.Max, + }); pathShader.Bind(); - texture.Bind(); - updateVertexBuffer(); pathShader.Unbind(); - renderer.PopDepthInfo(); renderer.PopLocalMatrix(); } - private Vector2 pointOnCircle(float angle) => new Vector2(MathF.Cos(angle), MathF.Sin(angle)); - - private Vector2 relativePosition(Vector2 localPos) => Vector2.Divide(localPos, drawSize); - - private Color4 colourAt(Vector2 localPos) => DrawColourInfo.Colour.TryExtractSingleColour(out SRGBColour colour) - ? colour.SRGB - : DrawColourInfo.Colour.Interpolate(relativePosition(localPos)).SRGB; - - private void addSegmentQuads(SegmentWithThickness segment, RectangleF texRect) + private void addCap(Line cap) { - Debug.Assert(triangleBatch != null); + // The provided line is perpendicular to the end/start of a segment. + // To get the remaining quad positions we are expanding said segment by the path radius. + Vector2 ortho = cap.OrthogonalDirection; + if (float.IsNaN(ortho.X) || float.IsNaN(ortho.Y)) + ortho = Vector2.UnitY; - // Each segment of the path 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 in order to properly handle self-overlap using the depth buffer. - Vector3 firstMiddlePoint = new Vector3(segment.Guide.StartPoint.X, segment.Guide.StartPoint.Y, 1); - Vector3 secondMiddlePoint = new Vector3(segment.Guide.EndPoint.X, segment.Guide.EndPoint.Y, 1); - Color4 firstMiddleColour = colourAt(segment.Guide.StartPoint); - Color4 secondMiddleColour = colourAt(segment.Guide.EndPoint); + Vector2 v2 = cap.StartPoint + ortho * radius; + Vector2 v3 = cap.EndPoint + ortho * radius; - // Each of the quads (mentioned above) is rendered as 2 triangles: - // Outer quad, triangle 1 - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeRight.EndPoint.X, segment.EdgeRight.EndPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeRight.EndPoint) - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeRight.StartPoint.X, segment.EdgeRight.StartPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeRight.StartPoint) - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = firstMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour - }); - - // Outer quad, triangle 2 - triangleBatch.Add(new TexturedVertex3D - { - Position = firstMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = secondMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeRight.EndPoint.X, segment.EdgeRight.EndPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeRight.EndPoint) - }); - - // Inner quad, triangle 1 - triangleBatch.Add(new TexturedVertex3D - { - Position = firstMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = secondMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeLeft.EndPoint.X, segment.EdgeLeft.EndPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeLeft.EndPoint) - }); - - // Inner quad, triangle 2 - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeLeft.EndPoint.X, segment.EdgeLeft.EndPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeLeft.EndPoint) - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(segment.EdgeLeft.StartPoint.X, segment.EdgeLeft.StartPoint.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segment.EdgeLeft.StartPoint) - }); - triangleBatch.Add(new TexturedVertex3D - { - Position = firstMiddlePoint, - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour - }); + drawQuad + ( + new Quad(cap.StartPoint, v2, cap.EndPoint, v3), + new Quad(new Vector2(0, -1), new Vector2(1, -1), new Vector2(0, 1), Vector2.One) + ); } - private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight, Line prevSegmentLeft, Line prevSegmentRight, RectangleF texRect) + private void addSegmentQuad(DrawableSegment segment) { - Debug.Assert(triangleBatch != null); + drawQuad + ( + segment.DrawQuad, + new Quad(new Vector2(0, -1), new Vector2(0, -1), new Vector2(0, 1), new Vector2(0, 1)) + ); + } + + private void addConnectionBetween(DrawableSegment segment, DrawableSegment prevSegment) + { + float thetaDiff = segment.Guide.Theta - prevSegment.Guide.Theta; if (Math.Abs(thetaDiff) > MathF.PI) thetaDiff = -Math.Sign(thetaDiff) * 2 * MathF.PI + thetaDiff; @@ -192,83 +116,129 @@ namespace osu.Framework.Graphics.Lines if (thetaDiff == 0f) return; - Vector2 origin = (segmentLeft.StartPoint + segmentRight.StartPoint) / 2; - - // Use segment end points instead of calculating start/end via theta to guarantee - // that the vertices have the exact same position as the quads, which prevents - // possible pixel gaps during rasterization. - Vector2 current = thetaDiff > 0f ? prevSegmentRight.EndPoint : prevSegmentLeft.EndPoint; - Vector2 end = thetaDiff > 0f ? segmentRight.StartPoint : segmentLeft.StartPoint; - - Line start = thetaDiff > 0f ? new Line(prevSegmentLeft.EndPoint, prevSegmentRight.EndPoint) : new Line(prevSegmentRight.EndPoint, prevSegmentLeft.EndPoint); - float theta0 = start.Theta; - float thetaStep = Math.Sign(thetaDiff) * MathF.PI / max_res; - int stepCount = (int)MathF.Ceiling(thetaDiff / thetaStep); - - Color4 originColour = colourAt(origin); - Color4 currentColour = colourAt(current); - - for (int i = 1; i <= stepCount; i++) + // more than 90 degrees - add end cap to the previous segment + if (Math.Abs(thetaDiff) > Math.PI * 0.5) { - // Center point - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(origin.X, origin.Y, 1), - TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = originColour - }); - - // First outer point - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(current.X, current.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour - }); - - current = i < stepCount ? origin + pointOnCircle(theta0 + i * thetaStep) * radius : end; - currentColour = colourAt(current); - - // Second outer point - triangleBatch.Add(new TexturedVertex3D - { - Position = new Vector3(current.X, current.Y, 0), - TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour - }); + addEndCap(prevSegment); + return; } + + Vector2 origin = segment.Guide.StartPoint; + Line end = thetaDiff > 0f ? new Line(segment.BottomLeft, segment.TopLeft) : new Line(segment.TopLeft, segment.BottomLeft); + Line start = thetaDiff > 0f ? new Line(prevSegment.TopRight, prevSegment.BottomRight) : new Line(prevSegment.BottomRight, prevSegment.TopRight); + + // position of a vertex which is located slightly below segments intersection + Vector2 innerVertex = Vector2.Lerp(start.StartPoint, end.EndPoint, 0.5f); + + // at this small angle curvature of the connection isn't noticeable, we can get away with a single triangle + if (Math.Abs(thetaDiff) < Math.PI / max_res) + { + drawTriangle(new Triangle(start.EndPoint, innerVertex, end.StartPoint), origin); + return; + } + + // 2 triangles for the remaining cases + Vector2 middle1 = Vector2.Lerp(start.EndPoint, end.StartPoint, 0.5f); + Vector2 outerVertex = Vector2.Lerp(origin, middle1, radius / (float)Math.Cos(Math.Abs(thetaDiff) * 0.5) / Vector2.Distance(origin, middle1)); + drawQuad(new Quad(start.EndPoint, outerVertex, innerVertex, end.StartPoint), origin); + } + + private void drawTriangle(Triangle triangle, Vector2 origin) + { + drawTriangle + ( + triangle, + new Triangle + ( + Vector2.Divide(triangle.P0 - origin, radius), + Vector2.Divide(triangle.P1 - origin, radius), + Vector2.Divide(triangle.P2 - origin, radius) + ) + ); + } + + private void drawTriangle(Triangle triangle, Triangle relativePos) + { + Debug.Assert(triangleBatch != null); + + triangleBatch.Add(new PathVertex + { + Position = triangle.P0, + RelativePos = relativePos.P0 + }); + triangleBatch.Add(new PathVertex + { + Position = triangle.P1, + RelativePos = relativePos.P1 + }); + triangleBatch.Add(new PathVertex + { + Position = triangle.P2, + RelativePos = relativePos.P2 + }); + } + + private void drawQuad(Quad quad, Vector2 origin) + { + drawQuad + ( + quad, + new Quad + ( + Vector2.Divide(quad.TopLeft - origin, radius), + Vector2.Divide(quad.TopRight - origin, radius), + Vector2.Divide(quad.BottomLeft - origin, radius), + Vector2.Divide(quad.BottomRight - origin, radius) + ) + ); + } + + private void drawQuad(Quad quad, Quad relativePos) + { + Debug.Assert(triangleBatch != null); + + triangleBatch.Add(new PathVertex + { + Position = quad.TopLeft, + RelativePos = relativePos.TopLeft + }); + triangleBatch.Add(new PathVertex + { + Position = quad.TopRight, + RelativePos = relativePos.TopRight + }); + triangleBatch.Add(new PathVertex + { + Position = quad.BottomLeft, + RelativePos = relativePos.BottomLeft + }); + + triangleBatch.Add(new PathVertex + { + Position = quad.BottomLeft, + RelativePos = relativePos.BottomLeft + }); + triangleBatch.Add(new PathVertex + { + Position = quad.TopRight, + RelativePos = relativePos.TopRight + }); + triangleBatch.Add(new PathVertex + { + Position = quad.BottomRight, + RelativePos = relativePos.BottomRight + }); } private void updateVertexBuffer() { - // Explanation of the terms "left" and "right": - // "Left" and "right" are used here in terms of a typical (Cartesian) coordinate system. - // So "left" corresponds to positive angles (anti-clockwise), and "right" corresponds - // to negative angles (clockwise). - // - // Note that this is not the same as the actually used coordinate system, in which the - // y-axis is flipped. In this system, "left" corresponds to negative angles (clockwise) - // and "right" corresponds to positive angles (anti-clockwise). - // - // Using a Cartesian system makes the calculations more consistent with typical math, - // such as in angle<->coordinate conversions and ortho vectors. For example, the x-unit - // vector (1, 0) has the orthogonal y-unit vector (0, 1). This would be "left" in the - // Cartesian system. But in the actual system, it's "right" and clockwise. Where - // this becomes confusing is during debugging, because OpenGL uses a Cartesian system. - // So to make debugging a bit easier (i.e. w/ RenderDoc or Nsight), this code uses terms - // that make sense in the realm of OpenGL, rather than terms which are technically - // accurate in the actually used "flipped" system. - - Debug.Assert(texture != null); Debug.Assert(segments.Count > 0); - RectangleF texRect = texture.GetTextureRect(new RectangleF(0.5f, 0.5f, texture.Width - 1, texture.Height - 1)); - Line? segmentToDraw = null; SegmentStartLocation location = SegmentStartLocation.Outside; SegmentStartLocation modifiedLocation = SegmentStartLocation.Outside; SegmentStartLocation nextLocation = SegmentStartLocation.End; - SegmentWithThickness? lastDrawnSegment = null; + DrawableSegment? lastDrawnSegment = null; for (int i = 0; i < segments.Count; i++) { @@ -309,9 +279,9 @@ namespace osu.Framework.Graphics.Lines } else // Otherwise draw the expanded segment { - SegmentWithThickness s = new SegmentWithThickness(segmentToDraw.Value, radius, location, modifiedLocation); - addSegmentQuads(s, texRect); - connect(s, lastDrawnSegment, texRect); + DrawableSegment s = new DrawableSegment(segmentToDraw.Value, radius, location, modifiedLocation); + addSegmentQuad(s); + connect(s, lastDrawnSegment); lastDrawnSegment = s; segmentToDraw = segments[i]; @@ -328,22 +298,22 @@ namespace osu.Framework.Graphics.Lines // Finish drawing last segment (if exists) if (segmentToDraw.HasValue) { - SegmentWithThickness s = new SegmentWithThickness(segmentToDraw.Value, radius, location, modifiedLocation); - addSegmentQuads(s, texRect); - connect(s, lastDrawnSegment, texRect); - addEndCap(s, texRect); + DrawableSegment s = new DrawableSegment(segmentToDraw.Value, radius, location, modifiedLocation); + addSegmentQuad(s); + connect(s, lastDrawnSegment); + addEndCap(s); } } /// /// Connects the start of the segment to the end of a previous one. /// - private void connect(SegmentWithThickness segment, SegmentWithThickness? prevSegment, RectangleF texRect) + private void connect(DrawableSegment segment, DrawableSegment? prevSegment) { if (!prevSegment.HasValue) { // Nothing to connect to - add start cap - addStartCap(segment, texRect); + addStartCap(segment); return; } @@ -352,13 +322,13 @@ namespace osu.Framework.Graphics.Lines default: case SegmentStartLocation.End: // Segment starts at the end of the previous one - addConnectionBetween(segment, prevSegment.Value, texRect); + addConnectionBetween(segment, prevSegment.Value); break; case SegmentStartLocation.Start: case SegmentStartLocation.Middle: // Segment starts at the start or the middle of the previous one - add end cap to the previous segment - addEndCap(prevSegment.Value, texRect); + addEndCap(prevSegment.Value); break; case SegmentStartLocation.Outside: @@ -372,39 +342,19 @@ namespace osu.Framework.Graphics.Lines // line since horizontal one will pass through it. However, that wouldn't be the case if horizontal line was located at // the middle and so end cap would be required. if (segment.StartLocation != SegmentStartLocation.End) - addEndCap(prevSegment.Value, texRect); + addEndCap(prevSegment.Value); // add start cap to the current one - addStartCap(segment, texRect); + addStartCap(segment); break; } } - private void addConnectionBetween(SegmentWithThickness segment, SegmentWithThickness prevSegment, RectangleF texRect) - { - float thetaDiff = segment.Guide.Theta - prevSegment.Guide.Theta; - addSegmentCaps(thetaDiff, segment.EdgeLeft, segment.EdgeRight, prevSegment.EdgeLeft, prevSegment.EdgeRight, texRect); - } + private void addEndCap(DrawableSegment segment) => + addCap(new Line(segment.TopRight, segment.BottomRight)); - private void addEndCap(SegmentWithThickness segment, RectangleF texRect) - { - // Explanation of semi-circle caps: - // Semi-circles are essentially 180 degree caps. So to create these caps, we - // can simply "fake" a segment that's 180 degrees flipped. This works because - // we are taking advantage of the fact that a path which makes a 180 degree - // bend would have a semi-circle cap. - - Line flippedLeft = new Line(segment.EdgeRight.EndPoint, segment.EdgeRight.StartPoint); - Line flippedRight = new Line(segment.EdgeLeft.EndPoint, segment.EdgeLeft.StartPoint); - addSegmentCaps(MathF.PI, flippedLeft, flippedRight, segment.EdgeLeft, segment.EdgeRight, texRect); - } - - private void addStartCap(SegmentWithThickness segment, RectangleF texRect) - { - Line flippedLeft = new Line(segment.EdgeRight.EndPoint, segment.EdgeRight.StartPoint); - Line flippedRight = new Line(segment.EdgeLeft.EndPoint, segment.EdgeLeft.StartPoint); - addSegmentCaps(MathF.PI, segment.EdgeLeft, segment.EdgeRight, flippedLeft, flippedRight, texRect); - } + private void addStartCap(DrawableSegment segment) => + addCap(new Line(segment.BottomLeft, segment.TopLeft)); private static float progressFor(Line line, float length, Vector2 point) { @@ -427,38 +377,53 @@ namespace osu.Framework.Graphics.Lines Outside } - private readonly struct SegmentWithThickness + private readonly struct DrawableSegment { /// - /// The line defining this . + /// The line defining this . /// public Line Guide { get; } /// - /// The line parallel to and located on the left side of it. + /// The draw quad of this . /// - public Line EdgeLeft { get; } + public Quad DrawQuad { get; } /// - /// The line parallel to and located on the right side of it. + /// The top-left position of the of this . /// - public Line EdgeRight { get; } + public Vector2 TopLeft => DrawQuad.TopLeft; /// - /// Position of this relative to the previous one. + /// The top-right position of the of this . + /// + public Vector2 TopRight => DrawQuad.TopRight; + + /// + /// The bottom-left position of the of this . + /// + public Vector2 BottomLeft => DrawQuad.BottomLeft; + + /// + /// The bottom-right position of the of this . + /// + public Vector2 BottomRight => DrawQuad.BottomRight; + + /// + /// Position of this relative to the previous one. /// public SegmentStartLocation StartLocation { get; } /// - /// Position of this modified relative to the previous one. + /// Position of this modified relative to the previous one. /// public SegmentStartLocation ModifiedStartLocation { get; } - /// The line defining this . - /// The distance at which and will be located from the . - /// Position of this relative to the previous one. - /// Position of this modified relative to the previous one. - public SegmentWithThickness(Line guide, float distance, SegmentStartLocation startLocation, SegmentStartLocation modifiedStartLocation) + /// The line defining this . + /// The path radius. + /// Position of this relative to the previous one. + /// Position of this modified relative to the previous one. + public DrawableSegment(Line guide, float radius, SegmentStartLocation startLocation, SegmentStartLocation modifiedStartLocation) { Guide = guide; StartLocation = startLocation; @@ -468,10 +433,34 @@ namespace osu.Framework.Graphics.Lines if (float.IsNaN(ortho.X) || float.IsNaN(ortho.Y)) ortho = Vector2.UnitY; - EdgeLeft = new Line(Guide.StartPoint + ortho * distance, Guide.EndPoint + ortho * distance); - EdgeRight = new Line(Guide.StartPoint - ortho * distance, Guide.EndPoint - ortho * distance); + DrawQuad = new Quad + ( + Guide.StartPoint + ortho * radius, + Guide.EndPoint + ortho * radius, + Guide.StartPoint - ortho * radius, + Guide.EndPoint - ortho * radius + ); } } + + [StructLayout(LayoutKind.Sequential)] + public struct PathVertex : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + + /// + /// A position of a vertex, where distance from that position to (0,0) defines it's colour. + /// Distance 0 means white and 1 means black. + /// This position is being interpolated between vertices and final colour is being applied in the fragment shader. + /// + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 RelativePos; + + public readonly bool Equals(PathVertex other) => + Position.Equals(other.Position) + && RelativePos.Equals(other.RelativePos); + } } } } diff --git a/osu.Framework/Graphics/Lines/SmoothPath.cs b/osu.Framework/Graphics/Lines/SmoothPath.cs index e01202032..6a683109f 100644 --- a/osu.Framework/Graphics/Lines/SmoothPath.cs +++ b/osu.Framework/Graphics/Lines/SmoothPath.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osuTK.Graphics; @@ -38,18 +37,6 @@ namespace osu.Framework.Graphics.Lines } } - private Color4? customBackgroundColour; - - /// - /// The background colour to be used for the frame buffer this path is rendered to. - /// For , this automatically defaults to the colour at 0 (the outermost colour of the path) to avoid aliasing issues. - /// - public override Color4 BackgroundColour - { - get => customBackgroundColour ?? base.BackgroundColour; - set => customBackgroundColour = base.BackgroundColour = value; - } - private readonly Cached textureCache = new Cached(); protected void InvalidateTexture() @@ -63,7 +50,7 @@ namespace osu.Framework.Graphics.Lines if (textureCache.IsValid) return; - int textureWidth = (int)PathRadius * 2; + int textureWidth = (int)Math.Max(PathRadius, 1) * 2; //initialise background var raw = new Image(textureWidth, 1); @@ -89,9 +76,6 @@ namespace osu.Framework.Graphics.Lines Texture = texture; } - if (customBackgroundColour == null) - base.BackgroundColour = ColourAt(0).Opacity(0); - textureCache.Validate(); } diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index c522756af..a51d34870 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -375,13 +375,6 @@ namespace osu.Framework.Graphics.OpenGL return image; } - public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer) - { - frameBuffer.Bind(); - GL.CopyTexSubImage2D(All.Texture2D, 0, 0, 0, 0, 0, frameBuffer.Texture.Width, frameBuffer.Texture.Height); - frameBuffer.Unbind(); - } - protected internal override Image ExtractFrameBufferData(IFrameBuffer frameBuffer) { int width = frameBuffer.Texture.Width; diff --git a/osu.Framework/Graphics/Rendering/Deferred/DeferredRenderer.cs b/osu.Framework/Graphics/Rendering/Deferred/DeferredRenderer.cs index bc37250e5..61968a257 100644 --- a/osu.Framework/Graphics/Rendering/Deferred/DeferredRenderer.cs +++ b/osu.Framework/Graphics/Rendering/Deferred/DeferredRenderer.cs @@ -191,12 +191,6 @@ namespace osu.Framework.Graphics.Rendering.Deferred protected internal override Image TakeScreenshot() => VeldridDevice.TakeScreenshot(); - public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer) - { - // TODO: Implement screen capture for DeferredRenderer - // This is a placeholder implementation - } - void IRenderer.EnterDrawNode(DrawNode node) { drawNodeStack.Push(node); diff --git a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs index 1f40da1fb..0dff42beb 100644 --- a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs @@ -25,11 +25,6 @@ namespace osu.Framework.Graphics.Rendering.Dummy protected internal override Image TakeScreenshot() => new Image(1, 1); - public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer) - { - // Dummy implementation - do nothing - } - protected override IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType) => new DummyShaderPart(); diff --git a/osu.Framework/Graphics/Rendering/IRenderer.cs b/osu.Framework/Graphics/Rendering/IRenderer.cs index c916bddf1..78ab6a979 100644 --- a/osu.Framework/Graphics/Rendering/IRenderer.cs +++ b/osu.Framework/Graphics/Rendering/IRenderer.cs @@ -350,12 +350,6 @@ namespace osu.Framework.Graphics.Rendering /// protected internal Image TakeScreenshot(); - /// - /// Captures the current screen content to a framebuffer for post-processing effects. - /// - /// The framebuffer to capture to. - void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer); - /// /// Returns an image containing the content of a framebuffer. /// @@ -398,13 +392,8 @@ namespace osu.Framework.Graphics.Rendering /// The texture's horizontal wrap mode. /// The texture's vertex wrap mode. /// The . - Texture CreateTexture(int width, - int height, - bool manualMipmaps = false, - TextureFilteringMode filteringMode = TextureFilteringMode.Linear, - WrapMode wrapModeS = WrapMode.None, - WrapMode wrapModeT = WrapMode.None, - Color4? initialisationColour = null); + Texture CreateTexture(int width, int height, bool manualMipmaps = false, TextureFilteringMode filteringMode = TextureFilteringMode.Linear, WrapMode wrapModeS = WrapMode.None, + WrapMode wrapModeT = WrapMode.None, Color4? initialisationColour = null); /// /// Creates a new video texture. @@ -471,7 +460,7 @@ namespace osu.Framework.Graphics.Rendering { } -#region TextureVisualiser Support + #region TextureVisualiser Support /// /// An event which is invoked every time a is created. @@ -483,6 +472,6 @@ namespace osu.Framework.Graphics.Rendering /// internal Texture[] GetAllTextures(); -#endregion + #endregion } } diff --git a/osu.Framework/Graphics/Rendering/IScreenContentProvider.cs b/osu.Framework/Graphics/Rendering/IScreenContentProvider.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index d8cd64499..b58ae4e00 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -328,10 +328,8 @@ namespace osu.Framework.Graphics.Rendering protected internal abstract Image TakeScreenshot(); /// - /// Captures the current screen content to a framebuffer for post-processing effects. + /// Returns an image containing the content of a framebuffer. /// - /// The framebuffer to capture to. - public abstract void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer); protected internal virtual Image? ExtractFrameBufferData(IFrameBuffer frameBuffer) => null; /// diff --git a/osu.Framework/Graphics/Transforms/CubicBezierEasingFunction.cs b/osu.Framework/Graphics/Transforms/CubicBezierEasingFunction.cs new file mode 100644 index 000000000..189da2ea8 --- /dev/null +++ b/osu.Framework/Graphics/Transforms/CubicBezierEasingFunction.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Framework.Graphics.Transforms +{ + /// + /// An easing function that creates a smooth transition using a cubic bezier curve. + /// + public readonly struct CubicBezierEasingFunction : IEasingFunction + { + public readonly double X1; + public readonly double Y1; + public readonly double X2; + public readonly double Y2; + + /// x position of the first control point. Must be in 0-1 range + /// y position of the first control point + /// x position of the second control point. Must be in 0-1 range + /// y position of the second control point + public CubicBezierEasingFunction(double x1, double y1, double x2, double y2) + { + if (Precision.DefinitelyBigger(0, x1) || Precision.DefinitelyBigger(x1, 1)) + throw new ArgumentOutOfRangeException(nameof(x1), "Must be within [0, 1] range."); + + if (Precision.DefinitelyBigger(0, x2) || Precision.DefinitelyBigger(x2, 1)) + throw new ArgumentOutOfRangeException(nameof(x2), "Must be within [0, 1] range."); + + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } + + /// position of the first control point. + /// position of the second control point. + /// + /// The x psoition of both control points must be in 0-1 range. + /// + public CubicBezierEasingFunction(Vector2 p1, Vector2 p2) + : this(p1.X, p1.Y, p2.X, p2.Y) + { + } + + /// + /// Constructs an easing function with initializes to 0 and to 1, which will perfectly flatten out the curve on both ends. + /// + /// ease-in strength, in 0-1 range + /// ease-out strength, in 0-1 range + public CubicBezierEasingFunction(double easeIn, double easeOut) + : this(easeIn, 0, 1 - easeOut, 1) + { + } + + private static double evaluateBezier(double t, double a1, double a2) => (((1 - 3 * a2 + 3 * a1) * t + (3 * a2 - 6 * a1)) * t + 3 * a1) * t; + + private static double findTForX(double time, double x1, double x2) + { + double left = 0.0, right = 1.0, currentT = 0; + + for (int i = 0; i < 100; i++) + { + currentT = left + (right - left) / 2; + double currentX = evaluateBezier(currentT, x1, x2) - time; + + if (currentX > 0) + right = currentT; + else + left = currentT; + + if (Math.Abs(currentX) <= 0.0000001) + break; + } + + return currentT; + } + + public double ApplyEasing(double time) => evaluateBezier(findTForX(time, X1, X2), Y1, Y2); + } +} diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 2701f6dc6..ceda46a2a 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -103,20 +103,22 @@ namespace osu.Framework.Graphics.UserInterface // discard control/special characters. return false; - var currentNumberFormat = CultureInfo.CurrentCulture.NumberFormat; - - switch (InputProperties.Type) + if (InputProperties.Type.IsNumerical()) { - case TextInputType.Decimal: - return char.IsAsciiDigit(character) || currentNumberFormat.NumberDecimalSeparator.Contains(character); + bool validNumericalCharacter = false; - case TextInputType.Number: - case TextInputType.NumericalPassword: - return char.IsAsciiDigit(character); + var currentNumberFormat = CultureInfo.CurrentCulture.NumberFormat; - default: - return true; + validNumericalCharacter |= char.IsAsciiDigit(character); + validNumericalCharacter |= selectionLeft == 0 && currentNumberFormat.NegativeSign.Contains(character); + + if (InputProperties.Type == TextInputType.Decimal) + validNumericalCharacter |= currentNumberFormat.NumberDecimalSeparator.Contains(character); + + return validNumericalCharacter; } + + return true; } private bool readOnly; diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.ScreenContent.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.ScreenContent.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index fb2e05acc..403eeb10d 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -223,12 +223,6 @@ namespace osu.Framework.Graphics.Veldrid protected internal override Image TakeScreenshot() => veldridDevice.TakeScreenshot(); - public override void CaptureScreenToFrameBuffer(IFrameBuffer frameBuffer) - { - // TODO: Implement screen capture for Veldrid - // This is a placeholder implementation - } - protected internal override Image? ExtractFrameBufferData(IFrameBuffer frameBuffer) => ExtractTexture((VeldridTexture)frameBuffer.Texture.NativeTexture); diff --git a/osu.Framework/Input/Bindings/KeyBindingContainer.cs b/osu.Framework/Input/Bindings/KeyBindingContainer.cs index 8ef1e9bb0..f9467ddf1 100644 --- a/osu.Framework/Input/Bindings/KeyBindingContainer.cs +++ b/osu.Framework/Input/Bindings/KeyBindingContainer.cs @@ -328,8 +328,8 @@ namespace osu.Framework.Input.Bindings } } - if (handled != null) - Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); + // if (handled != null) + // Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug); return handled; } diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 4f30fc504..adef2a8eb 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -108,30 +108,6 @@ namespace osu.Framework.Input.Bindings return ContainsAll(Keys, pressedKeys.Keys, matchingMode); } - private static InputKey? getVirtualKey(InputKey key) - { - switch (key) - { - case InputKey.LShift: - case InputKey.RShift: - return InputKey.Shift; - - case InputKey.LControl: - case InputKey.RControl: - return InputKey.Control; - - case InputKey.LAlt: - case InputKey.RAlt: - return InputKey.Alt; - - case InputKey.LSuper: - case InputKey.RSuper: - return InputKey.Super; - } - - return null; - } - /// /// Check whether the provided set of pressed keys matches the candidate binding. /// @@ -192,7 +168,7 @@ namespace osu.Framework.Input.Bindings internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey) { return candidateKeyBinding.Contains(physicalKey) || - (getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey)); + (physicalKey.GetVirtualKey() is InputKey vKey && candidateKeyBinding.Contains(vKey)); } /// @@ -210,7 +186,7 @@ namespace osu.Framework.Input.Bindings foreach (var pk in pressedPhysicalKeys) { - if (getVirtualKey(pk) == candidateKey) + if (pk.GetVirtualKey() == candidateKey) return true; } diff --git a/osu.Framework/Input/Events/KeyDownEvent.cs b/osu.Framework/Input/Events/KeyDownEvent.cs index 8071e75a8..a706b79c5 100644 --- a/osu.Framework/Input/Events/KeyDownEvent.cs +++ b/osu.Framework/Input/Events/KeyDownEvent.cs @@ -20,6 +20,6 @@ namespace osu.Framework.Input.Events Repeat = repeat; } - public override string ToString() => $"{GetType().ReadableName()}({Key}, {Repeat})"; + // public override string ToString() => $"{GetType().ReadableName()}({Key}, {Repeat})"; } } diff --git a/osu.Framework/Input/Handlers/Pen/PenHandler.cs b/osu.Framework/Input/Handlers/Pen/PenHandler.cs index eeee2665b..04d0e6dfd 100644 --- a/osu.Framework/Input/Handlers/Pen/PenHandler.cs +++ b/osu.Framework/Input/Handlers/Pen/PenHandler.cs @@ -45,24 +45,20 @@ namespace osu.Framework.Input.Handlers.Pen return true; } - // Pen input is not necessarily direct on mobile platforms (specifically Android, where external tablets are supported), - // but until users experience issues with this, consider it "direct" for now. - private static readonly TabletPenDeviceType device_type = RuntimeInfo.IsMobile ? TabletPenDeviceType.Direct : TabletPenDeviceType.Unknown; - - private void handlePenMove(Vector2 position, bool pressed) + private void handlePenMove(TabletPenDeviceType deviceType, Vector2 position, bool pressed) { - if (pressed && device_type == TabletPenDeviceType.Direct) + if (pressed && deviceType == TabletPenDeviceType.Direct) enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), true)); else - enqueueInput(new MousePositionAbsoluteInputFromPen { DeviceType = device_type, Position = position }); + enqueueInput(new MousePositionAbsoluteInputFromPen { DeviceType = deviceType, Position = position }); } - private void handlePenTouch(bool pressed, Vector2 position) + private void handlePenTouch(TabletPenDeviceType deviceType, bool pressed, Vector2 position) { - if (device_type == TabletPenDeviceType.Direct) + if (deviceType == TabletPenDeviceType.Direct) enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), pressed)); else - enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = device_type }); + enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = deviceType }); } private void handlePenButton(TabletPenButton button, bool pressed) diff --git a/osu.Framework/Input/TextInputType.cs b/osu.Framework/Input/TextInputType.cs index f4fde0371..76c4aba41 100644 --- a/osu.Framework/Input/TextInputType.cs +++ b/osu.Framework/Input/TextInputType.cs @@ -66,6 +66,20 @@ namespace osu.Framework.Input } } + public static bool IsNumerical(this TextInputType type) + { + switch (type) + { + case TextInputType.Number: + case TextInputType.NumericalPassword: + case TextInputType.Decimal: + return true; + + default: + return false; + } + } + public static bool SupportsIme(this TextInputType type) => type == TextInputType.Name || type == TextInputType.Text; } } diff --git a/osu.Framework/Platform/Display.cs b/osu.Framework/Platform/Display.cs index 171a4af7d..da1b65385 100644 --- a/osu.Framework/Platform/Display.cs +++ b/osu.Framework/Platform/Display.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Drawing; using System.Linq; @@ -24,6 +25,12 @@ namespace osu.Framework.Platform /// public Rectangle Bounds { get; } + /// + /// The current usable bounds of the display in screen space. + /// This is smaller and contained within . + /// + public Rectangle UsableBounds { get; } + /// /// The available s on this display, or empty if the display mode cannot be configured (e.g. mobile displays). /// @@ -34,11 +41,13 @@ namespace osu.Framework.Platform /// public int Index { get; } - public Display(int index, string? name, Rectangle bounds, DisplayMode[] displayModes) + public Display(int index, string? name, Rectangle bounds, Rectangle usableBounds, DisplayMode[] displayModes) { + Debug.Assert(bounds.Contains(usableBounds)); Index = index; Name = name; Bounds = bounds; + UsableBounds = usableBounds; DisplayModes = displayModes; } diff --git a/osu.Framework/Platform/IWindow.cs b/osu.Framework/Platform/IWindow.cs index 1ac2fe764..78d800ba5 100644 --- a/osu.Framework/Platform/IWindow.cs +++ b/osu.Framework/Platform/IWindow.cs @@ -8,6 +8,7 @@ using System.Drawing; using System.IO; using osu.Framework.Bindables; using osu.Framework.Configuration; +using osu.Framework.Graphics; using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; namespace osu.Framework.Platform @@ -146,6 +147,15 @@ namespace osu.Framework.Platform /// BindableSafeArea SafeAreaPadding { get; } + /// + /// The size of the window decoration and border, relative to . + /// + /// + /// This may include the invisible resize border, even when maximised. + /// Usually 0 when in borderless or fullscreen. + /// + IBindable BorderSize { get; } + /// /// The s supported by this implementation. /// diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs index 4621367f4..2324d0891 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ImageExtensions; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Threading; using SixLabors.ImageSharp; @@ -38,6 +39,10 @@ namespace osu.Framework.Platform.SDL2 public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea(); + protected readonly Bindable BorderSize = new Bindable(); + + IBindable IWindow.BorderSize => BorderSize; + public virtual Point PointToClient(Point point) => point; public virtual Point PointToScreen(Point point) => point; diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs index 2584309b5..0522f8b33 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs @@ -10,6 +10,7 @@ using System.Drawing; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Configuration; +using osu.Framework.Graphics; using osu.Framework.Logging; using osuTK; using static SDL2.SDL; @@ -354,6 +355,12 @@ namespace osu.Framework.Platform.SDL2 return false; } + if (SDL_GetDisplayUsableBounds(displayIndex, out var usableBounds) < 0) + { + Logger.Log($"Failed to get usable display bounds for display at index ({displayIndex}). Assuming whole display is usable. SDL Error: {SDL_GetError()}"); + usableBounds = rect; + } + DisplayMode[] displayModes = Array.Empty(); if (RuntimeInfo.IsDesktop) @@ -379,7 +386,11 @@ namespace osu.Framework.Platform.SDL2 .ToArray(); } - display = new Display(displayIndex, SDL_GetDisplayName(displayIndex), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); + display = new Display(displayIndex, + SDL_GetDisplayName(displayIndex), + new Rectangle(rect.x, rect.y, rect.w, rect.h), + new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h), + displayModes); return true; } @@ -590,6 +601,27 @@ namespace osu.Framework.Platform.SDL2 currentDisplay = Displays.ElementAtOrDefault(displayIndex) ?? PrimaryDisplay; CurrentDisplayBindable.Value = currentDisplay; } + + if (tryGetBorderSize(out var borderSize)) + BorderSize.Value = borderSize; + } + + private bool tryGetBorderSize(out MarginPadding borderSize) + { + if (SDL_GetWindowBordersSize(SDLWindowHandle, out int top, out int left, out int bottom, out int right) < 0) + { + borderSize = default; + return false; + } + + borderSize = new MarginPadding + { + Top = top, + Left = left, + Bottom = bottom, + Right = right + }; + return true; } /// diff --git a/osu.Framework/Platform/SDL3/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs index 3467f9f21..158a71e82 100644 --- a/osu.Framework/Platform/SDL3/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -6,6 +6,7 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.StateChanges; using osuTK.Input; using SDL; using static SDL.SDL3; @@ -1048,6 +1049,21 @@ namespace osu.Framework.Platform.SDL3 return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); } + public static TabletPenDeviceType ToTabletPenDeviceType(this SDL_PenDeviceType type) + { + switch (type) + { + case SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_DIRECT: + return TabletPenDeviceType.Direct; + + case SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_INDIRECT: + return TabletPenDeviceType.Indirect; + + default: + return TabletPenDeviceType.Unknown; + } + } + public static string ReadableName(this SDL_LogCategory category) { switch (category) diff --git a/osu.Framework/Platform/SDL3/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs index 19111e92d..79d627500 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ImageExtensions; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Threading; using SDL; @@ -40,6 +41,10 @@ namespace osu.Framework.Platform.SDL3 public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea(); + protected readonly Bindable BorderSize = new Bindable(); + + IBindable IWindow.BorderSize => BorderSize; + public virtual Point PointToClient(Point point) => point; public virtual Point PointToScreen(Point point) => point; diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index be0283f2f..ff947e408 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -11,6 +11,7 @@ using osu.Framework.Configuration; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Input; +using osu.Framework.Input.StateChanges; using osu.Framework.Input.States; using osu.Framework.Logging; using osuTK; @@ -530,14 +531,16 @@ namespace osu.Framework.Platform.SDL3 private void handleKeymapChangedEvent() => KeymapChanged?.Invoke(); + private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ToTabletPenDeviceType(); + private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion) { - PenMove?.Invoke(new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN)); + PenMove?.Invoke(getPenType(evtPenMotion.which), new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN)); } private void handlePenTouchEvent(SDL_PenTouchEvent evtPenTouch) { - PenTouch?.Invoke(evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale); + PenTouch?.Invoke(getPenType(evtPenTouch.which), evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale); } /// @@ -745,13 +748,13 @@ namespace osu.Framework.Platform.SDL3 /// /// Invoked when a pen moves. Passes pen position and whether the pen is touching the tablet surface. /// - public event Action? PenMove; + public event Action? PenMove; /// /// Invoked when a pen touches (true) or lifts (false) from the tablet surface. /// Also passes the current position of the pen. /// - public event Action? PenTouch; + public event Action? PenTouch; /// /// Invoked when a pen button is pressed (true) or released (false). diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs index 42b79980d..d203a8719 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs @@ -10,6 +10,7 @@ using System.Drawing; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Configuration; +using osu.Framework.Graphics; using osu.Framework.Logging; using osuTK; using SDL; @@ -360,6 +361,14 @@ namespace osu.Framework.Platform.SDL3 return false; } + SDL_Rect usableBounds; + + if (!SDL_GetDisplayUsableBounds(displayID, &usableBounds)) + { + Logger.Log($"Failed to get usable display bounds for display at index ({displayIndex}). Assuming whole display is usable. SDL Error: {SDL_GetError()}"); + usableBounds = rect; + } + DisplayMode[] displayModes = Array.Empty(); if (RuntimeInfo.IsDesktop) @@ -382,7 +391,11 @@ namespace osu.Framework.Platform.SDL3 displayModes[i] = modes[i].ToDisplayMode(displayIndex); } - display = new Display(displayIndex, SDL_GetDisplayName(displayID), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); + display = new Display(displayIndex, + SDL_GetDisplayName(displayID), + new Rectangle(rect.x, rect.y, rect.w, rect.h), + new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h), + displayModes); return true; } @@ -605,6 +618,29 @@ namespace osu.Framework.Platform.SDL3 CurrentDisplayBindable.Value = currentDisplay; } + + if (tryGetBorderSize(out var borderSize)) + BorderSize.Value = borderSize; + } + + private unsafe bool tryGetBorderSize(out MarginPadding borderSize) + { + int top, left, bottom, right; + + if (!SDL_GetWindowBordersSize(SDLWindowHandle, &top, &left, &bottom, &right)) + { + borderSize = default; + return false; + } + + borderSize = new MarginPadding + { + Top = top, + Left = left, + Bottom = bottom, + Right = right + }; + return true; } private static bool tryGetDisplayIndex(SDL_DisplayID id, out int index) diff --git a/osu.Framework/Resources/Shaders/blur.frag b/osu.Framework/Resources/Shaders/blur.frag deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs b/osu.Framework/Resources/Shaders/sh_AcrylicBlur.fs deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/Resources/Shaders/sh_AcrylicBlur.vs b/osu.Framework/Resources/Shaders/sh_AcrylicBlur.vs deleted file mode 100644 index 54ba6f9a6..000000000 --- a/osu.Framework/Resources/Shaders/sh_AcrylicBlur.vs +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef ACRYLIC_BLUR_VS -#define ACRYLIC_BLUR_VS - -#include "sh_Utils.h" - -layout(location = 0) in highp vec2 m_Position; -layout(location = 1) in lowp vec4 m_Colour; -layout(location = 2) in highp vec2 m_TexCoord; -layout(location = 3) in highp vec4 m_TexRect; -layout(location = 4) in mediump vec2 m_BlendRange; -layout(location = 5) in highp float m_BackbufferDrawDepth; - -layout(location = 0) out highp vec2 v_MaskingPosition; -layout(location = 1) out lowp vec4 v_Colour; -layout(location = 2) out highp vec2 v_TexCoord; -layout(location = 3) out highp vec4 v_TexRect; -layout(location = 4) out mediump vec2 v_BlendRange; - -void main(void) -{ - // Transform from screen space to masking space. - highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); - v_MaskingPosition = maskingPos.xy / maskingPos.z; - - v_Colour = m_Colour; - v_TexCoord = m_TexCoord; - v_TexRect = m_TexRect; - v_BlendRange = m_BlendRange; - - gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); - - if (g_BackbufferDraw) - gl_Position.z = m_BackbufferDrawDepth; -} - -#endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_Path.fs b/osu.Framework/Resources/Shaders/sh_Path.fs new file mode 100644 index 000000000..92d5a9752 --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_Path.fs @@ -0,0 +1,37 @@ +#ifndef PATH_FS +#define PATH_FS + +#include "sh_Utils.h" +#include "sh_Masking.h" +#include "sh_TextureWrapping.h" + +layout(location = 2) in mediump vec2 v_TexCoord; + +// FrameBuffer texture +layout(set = 0, binding = 0) uniform lowp texture2D m_Texture; +layout(set = 0, binding = 1) uniform lowp sampler m_Sampler; + +// Path texture +layout(set = 1, binding = 0) uniform lowp texture2D m_Texture1; +layout(set = 1, binding = 1) uniform lowp sampler m_Sampler1; + +layout(std140, set = 2, binding = 0) uniform m_PathTextureParameters +{ + highp vec4 TexRect1; +}; + +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + lowp vec4 frameBuffer = texture(sampler2D(m_Texture, m_Sampler), v_TexCoord, -0.9); + if (frameBuffer.r == 0.0) + { + o_Colour = vec4(0.0); + return; + } + lowp vec4 pathCol = texture(sampler2D(m_Texture1, m_Sampler1), TexRect1.xy + vec2(frameBuffer.r, 0.0) * TexRect1.zw, -0.9); + o_Colour = getRoundedColor(vec4(pathCol.rgb, pathCol.a * frameBuffer.a), v_TexCoord); +} + +#endif diff --git a/osu.Framework/Resources/Shaders/sh_PathPrepass.fs b/osu.Framework/Resources/Shaders/sh_PathPrepass.fs new file mode 100644 index 000000000..5172d6844 --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_PathPrepass.fs @@ -0,0 +1,14 @@ +#ifndef PATH_PREPASS_FS +#define PATH_PREPASS_FS + +layout(location = 0) in highp vec2 v_RelativePos; + +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + highp float dst = clamp(distance(v_RelativePos, vec2(0.0)), 0.0, 1.0); + o_Colour = vec4(vec3(1.0 - dst), float(dst < 1.0)); +} + +#endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_PathPrepass.vs b/osu.Framework/Resources/Shaders/sh_PathPrepass.vs new file mode 100644 index 000000000..68de4bd68 --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_PathPrepass.vs @@ -0,0 +1,17 @@ +#ifndef PATH_PREPASS_VS +#define PATH_PREPASS_VS + +#include "sh_Utils.h" + +layout(location = 0) in highp vec2 m_Position; +layout(location = 1) in highp vec2 m_RelativePos; + +layout(location = 0) out highp vec2 v_RelativePos; + +void main(void) +{ + v_RelativePos = m_RelativePos; + gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0); +} + +#endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/texture.vert b/osu.Framework/Resources/Shaders/texture.vert deleted file mode 100644 index e69de29bb..000000000 diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index e7e4803d2..bf3213f77 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -1,4 +1,4 @@ - + net8.0 Library @@ -38,8 +38,8 @@ - - + +