A game framework written with osu! in mind.

Merge branch 'master' into masking-negative-size

authored by

Dan Balasescu and committed by
GitHub
28e4fcef 45d33827

+2200 -541
+2 -2
.config/dotnet-tools.json
··· 20 20 "jb" 21 21 ] 22 22 }, 23 - "nvika": { 24 - "version": "2.0.0", 23 + "smoogipoo.nvika": { 24 + "version": "1.0.1", 25 25 "commands": [ 26 26 "nvika" 27 27 ]
+2 -4
.github/workflows/ci.yml
··· 91 91 - name: InspectCode 92 92 run: dotnet jb inspectcode $(pwd)/osu-framework.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN 93 93 94 - - name: ReSharper 95 - uses: glassechidna/resharper-action@master 96 - with: 97 - report: ${{github.workspace}}/inspectcodereport.xml 94 + - name: NVika 95 + run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+4 -3
.github/workflows/report-nunit.yml
··· 2 2 # See: 3 3 # * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories 4 4 # * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token 5 - name: "Test Report" 5 + name: Annotate CI run with test results 6 6 on: 7 7 workflow_run: 8 8 workflows: ["Continuous Integration"] 9 9 types: 10 10 - completed 11 11 jobs: 12 - report: 12 + annotate: 13 + name: Annotate CI run with test results 13 14 runs-on: ubuntu-latest 14 15 if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} 15 16 strategy: ··· 21 22 - { prettyname: Linux } 22 23 threadingMode: ['SingleThread', 'MultiThreaded'] 23 24 steps: 24 - - name: Continuous Integration Test Report 25 + - name: Annotate CI run with test results 25 26 uses: dorny/test-reporter@v1.4.2 26 27 with: 27 28 artifact: osu-framework-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
+1 -1
CodeAnalysis/BannedSymbols.txt
··· 3 3 M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead. 4 4 T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. 5 5 M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead. 6 - F:System.UriKind.RelativeOrAbsolute;Incompatible results when run on mono, see https://www.mono-project.com/docs/faq/known-issues/urikind-relativeorabsolute/ 6 + F:System.UriKind.RelativeOrAbsolute;Incompatible results when run on mono (see https://www.mono-project.com/docs/faq/known-issues/urikind-relativeorabsolute/). Use Validation.TryParseUri(string, out Uri?) instead.
+11
InspectCode.ps1
··· 1 + dotnet tool restore 2 + 3 + # Temporarily disabled until the tool is upgraded to 5.0. 4 + # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. 5 + # - cmd: dotnet format --dry-run --check 6 + 7 + dotnet CodeFileSanity 8 + dotnet jb inspectcode "osu-framework.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN 9 + dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors 10 + 11 + exit $LASTEXITCODE
+6
InspectCode.sh
··· 1 + #!/bin/bash 2 + 3 + dotnet tool restore 4 + dotnet CodeFileSanity 5 + dotnet jb inspectcode "osu-framework.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN 6 + dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+1 -1
README.md
··· 31 31 32 32 ### Code analysis 33 33 34 - Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install resharper or use rider to get inline support in your IDE of choice. 34 + Code analysis can be run with `powershell ./InspectCode.ps1` or `InspectCode.sh`. 35 35 36 36 ## Contributing 37 37
+5 -3
build/build.cake
··· 123 123 }); 124 124 }); 125 125 126 - Task("DotnetFormat") 127 - .Does(() => DotNetCoreTool(sln.FullPath, "format", "--dry-run --check")); 126 + // Temporarily disabled until the tool is upgraded to 5.0. 127 + // The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. 128 + // Task("DotnetFormat") 129 + // .Does(() => DotNetCoreTool(sln.FullPath, "format", "--dry-run --check")); 128 130 129 131 Task("PackFramework") 130 132 .Does(() => { ··· 222 224 .IsDependentOn("Clean") 223 225 .IsDependentOn("DetermineAppveyorBuildProperties") 224 226 .IsDependentOn("CodeFileSanity") 225 - .IsDependentOn("DotnetFormat") 227 + //.IsDependentOn("DotnetFormat") <- To be uncommented after fixing the task. 226 228 .IsDependentOn("InspectCode") 227 229 .IsDependentOn("Test") 228 230 .IsDependentOn("DetermineAppveyorDeployProperties")
+7 -2
osu.Framework.Tests/Audio/TestAudioAdjustments.cs
··· 125 125 public void TestAdjustBoundComponentBeforeBind() 126 126 { 127 127 var adjustments = new AudioAdjustments(); 128 - var adjustments2 = new AudioAdjustments(); 129 128 130 - adjustments2.Volume.Value = 0.5f; 129 + var adjustments2 = new AudioAdjustments 130 + { 131 + Volume = 132 + { 133 + Value = 0.5f 134 + } 135 + }; 131 136 132 137 adjustments.BindAdjustments(adjustments2); 133 138
+23
osu.Framework.Tests/Bindables/BindableSerializationTest.cs
··· 51 51 } 52 52 53 53 [Test] 54 + public void TestClassWithInitialisationFromCtorArgs() 55 + { 56 + var toSerialize = new CustomObjWithCtorInit 57 + { 58 + Bindable1 = { Value = 5 } 59 + }; 60 + 61 + var deserialized = JsonConvert.DeserializeObject<CustomObjWithCtorInit>(JsonConvert.SerializeObject(toSerialize)); 62 + 63 + Assert.AreEqual(toSerialize.Bindable1.Value, deserialized?.Bindable1.Value); 64 + } 65 + 66 + [Test] 54 67 public void TestIntWithBounds() 55 68 { 56 69 var toSerialize = new CustomObj2 ··· 121 134 JsonConvert.PopulateObject(serialized, obj); 122 135 123 136 Assert.IsTrue(valueChanged); 137 + } 138 + 139 + private class CustomObjWithCtorInit 140 + { 141 + public readonly Bindable<int> Bindable1 = new Bindable<int>(); 142 + 143 + public CustomObjWithCtorInit(int bindable1 = 0) 144 + { 145 + Bindable1.Value = bindable1; 146 + } 124 147 } 125 148 126 149 private class CustomObj
+21 -13
osu.Framework.Tests/Graphics/ColourTest.cs
··· 147 147 148 148 private static readonly object[][] valid_hex_colours = 149 149 { 150 - new object[] { Colour4.White, Colour4.FromHex("#fff") }, 151 - new object[] { Colour4.Red, Colour4.FromHex("#ff0000") }, 152 - new object[] { Colour4.Yellow.Opacity(half_alpha), Colour4.FromHex("ffff0080") }, 153 - new object[] { Colour4.Lime.Opacity(half_alpha), Colour4.FromHex("00ff0080") }, 154 - new object[] { new Colour4(17, 34, 51, 255), Colour4.FromHex("123") }, 155 - new object[] { new Colour4(17, 34, 51, 255), Colour4.FromHex("#123") }, 156 - new object[] { new Colour4(17, 34, 51, 68), Colour4.FromHex("1234") }, 157 - new object[] { new Colour4(17, 34, 51, 68), Colour4.FromHex("#1234") }, 158 - new object[] { new Colour4(18, 52, 86, 255), Colour4.FromHex("123456") }, 159 - new object[] { new Colour4(18, 52, 86, 255), Colour4.FromHex("#123456") }, 160 - new object[] { new Colour4(18, 52, 86, 120), Colour4.FromHex("12345678") }, 161 - new object[] { new Colour4(18, 52, 86, 120), Colour4.FromHex("#12345678") } 150 + new object[] { Colour4.White, "#fff" }, 151 + new object[] { Colour4.Red, "#ff0000" }, 152 + new object[] { Colour4.Yellow.Opacity(half_alpha), "ffff0080" }, 153 + new object[] { Colour4.Lime.Opacity(half_alpha), "00ff0080" }, 154 + new object[] { new Colour4(17, 34, 51, 255), "123" }, 155 + new object[] { new Colour4(17, 34, 51, 255), "#123" }, 156 + new object[] { new Colour4(17, 34, 51, 68), "1234" }, 157 + new object[] { new Colour4(17, 34, 51, 68), "#1234" }, 158 + new object[] { new Colour4(18, 52, 86, 255), "123456" }, 159 + new object[] { new Colour4(18, 52, 86, 255), "#123456" }, 160 + new object[] { new Colour4(18, 52, 86, 120), "12345678" }, 161 + new object[] { new Colour4(18, 52, 86, 120), "#12345678" } 162 162 }; 163 163 164 164 [TestCaseSource(nameof(valid_hex_colours))] 165 - public void TestFromHex(Colour4 expected, Colour4 actual) => Assert.AreEqual(expected, actual); 165 + public void TestFromHex(Colour4 expectedColour, string hexCode) 166 + { 167 + Assert.AreEqual(expectedColour, Colour4.FromHex(hexCode)); 168 + 169 + Assert.True(Colour4.TryParseHex(hexCode, out var actualColour)); 170 + Assert.AreEqual(expectedColour, actualColour); 171 + } 166 172 167 173 [TestCase("1")] 168 174 [TestCase("#1")] ··· 179 185 { 180 186 // Assert.Catch allows any exception type, contrary to .Throws<T>() (which expects exactly T) 181 187 Assert.Catch(() => Colour4.FromHex(invalidColour)); 188 + 189 + Assert.False(Colour4.TryParseHex(invalidColour, out _)); 182 190 } 183 191 184 192 [Test]
+5 -5
osu.Framework.Tests/IO/FontStoreTest.cs
··· 18 18 public void OneTimeSetUp() 19 19 { 20 20 storage = new TemporaryNativeStorage("fontstore-test"); 21 - fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly), "Resources.Fonts.OpenSans"); 21 + fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly), "Resources.Fonts.Roboto"); 22 22 } 23 23 24 24 [Test] 25 25 public void TestNestedScaleAdjust() 26 26 { 27 - using (var fontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans-Regular") { CacheStorage = storage }, scaleAdjust: 100)) 28 - using (var nestedFontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans-Bold") { CacheStorage = storage }, 10)) 27 + using (var fontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "Roboto-Regular") { CacheStorage = storage }, scaleAdjust: 100)) 28 + using (var nestedFontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "Roboto-Bold") { CacheStorage = storage }, 10)) 29 29 { 30 30 fontStore.AddStore(nestedFontStore); 31 31 32 - var normalGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans-Regular", 'a'); 32 + var normalGlyph = (TexturedCharacterGlyph)fontStore.Get("Roboto-Regular", 'a'); 33 33 Assert.That(normalGlyph, Is.Not.Null); 34 34 35 - var boldGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans-Bold", 'a'); 35 + var boldGlyph = (TexturedCharacterGlyph)fontStore.Get("Roboto-Bold", 'a'); 36 36 Assert.That(boldGlyph, Is.Not.Null); 37 37 38 38 Assert.That(normalGlyph.Scale, Is.EqualTo(1f / 100));
+96
osu.Framework.Tests/Localisation/LocalisableEnumAttributeTest.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using NUnit.Framework; 6 + using osu.Framework.Extensions; 7 + using osu.Framework.Localisation; 8 + 9 + namespace osu.Framework.Tests.Localisation 10 + { 11 + [TestFixture] 12 + public class LocalisableEnumAttributeTest 13 + { 14 + [TestCase(EnumA.Item1, "Item1")] 15 + [TestCase(EnumA.Item2, "B")] 16 + public void TestNonLocalisableEnumReturnsDescriptionOrToString(EnumA value, string expected) 17 + { 18 + Assert.That(value.GetLocalisableDescription().ToString(), Is.EqualTo(expected)); 19 + } 20 + 21 + [TestCase(EnumB.Item1, "A")] 22 + [TestCase(EnumB.Item2, "B")] 23 + public void TestLocalisableEnumReturnsMappedValue(EnumB value, string expected) 24 + { 25 + Assert.That(value.GetLocalisableDescription().ToString(), Is.EqualTo(expected)); 26 + } 27 + 28 + [Test] 29 + public void TestLocalisableEnumWithInvalidBaseTypeThrows() 30 + { 31 + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed 32 + Assert.Throws<ArgumentException>(() => EnumC.Item1.GetLocalisableDescription().ToString()); 33 + } 34 + 35 + [Test] 36 + public void TestLocalisableEnumWithInvalidGenericTypeThrows() 37 + { 38 + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed 39 + Assert.Throws<InvalidOperationException>(() => EnumD.Item1.GetLocalisableDescription().ToString()); 40 + } 41 + 42 + public enum EnumA 43 + { 44 + Item1, 45 + 46 + [System.ComponentModel.Description("B")] 47 + Item2 48 + } 49 + 50 + [LocalisableEnum(typeof(EnumBEnumLocalisationMapper))] 51 + public enum EnumB 52 + { 53 + Item1, 54 + Item2 55 + } 56 + 57 + private class EnumBEnumLocalisationMapper : EnumLocalisationMapper<EnumB> 58 + { 59 + public override LocalisableString Map(EnumB value) 60 + { 61 + switch (value) 62 + { 63 + case EnumB.Item1: 64 + return "A"; 65 + 66 + case EnumB.Item2: 67 + return "B"; 68 + 69 + default: 70 + throw new ArgumentOutOfRangeException(nameof(value), value, null); 71 + } 72 + } 73 + } 74 + 75 + [LocalisableEnum(typeof(EnumCEnumLocalisationMapper))] 76 + public enum EnumC 77 + { 78 + Item1, 79 + } 80 + 81 + private class EnumCEnumLocalisationMapper 82 + { 83 + } 84 + 85 + [LocalisableEnum(typeof(EnumDEnumLocalisationMapper))] 86 + public enum EnumD 87 + { 88 + Item1, 89 + } 90 + 91 + private class EnumDEnumLocalisationMapper : EnumLocalisationMapper<EnumA> 92 + { 93 + public override LocalisableString Map(EnumA value) => "A"; 94 + } 95 + } 96 + }
+17
osu.Framework.Tests/Localisation/LocalisationTest.cs
··· 38 38 } 39 39 40 40 [Test] 41 + public void TestConfigSettingRetainedWhenAddingNewLanguage() 42 + { 43 + config.SetValue(FrameworkSetting.Locale, "ja-JP"); 44 + 45 + // ensure that adding a new language which doesn't match the user's choice doesn't cause the configuration value to get reset. 46 + manager.AddLanguage("po", new FakeStorage("po-OP")); 47 + Assert.AreEqual("ja-JP", config.Get<string>(FrameworkSetting.Locale)); 48 + 49 + var localisedText = manager.GetLocalisedString(new TranslatableString(FakeStorage.LOCALISABLE_STRING_EN, FakeStorage.LOCALISABLE_STRING_EN)); 50 + Assert.AreEqual(FakeStorage.LOCALISABLE_STRING_EN, localisedText.Value); 51 + 52 + // ensure that if the user's selection is added in a further AddLanguage call, the manager correctly translates strings. 53 + manager.AddLanguage("ja-JP", new FakeStorage("ja-JP")); 54 + Assert.AreEqual(FakeStorage.LOCALISABLE_STRING_JA_JP, localisedText.Value); 55 + } 56 + 57 + [Test] 41 58 public void TestNotLocalised() 42 59 { 43 60 manager.AddLanguage("ja-JP", new FakeStorage("ja-JP"));
+43 -4
osu.Framework.Tests/Platform/GameHostSuspendTest.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using System.Collections.Generic; 4 5 using System.Threading; 5 6 using System.Threading.Tasks; 6 7 using NUnit.Framework; 8 + using osu.Framework.Bindables; 9 + using osu.Framework.Configuration; 10 + using osu.Framework.Development; 7 11 using osu.Framework.Platform; 12 + using osu.Framework.Threading; 8 13 9 14 namespace osu.Framework.Tests.Platform 10 15 { ··· 16 21 17 22 private const int timeout = 10000; 18 23 19 - [Test] 20 - public void TestPauseResume() 24 + [TestCase(ExecutionMode.SingleThread)] 25 + [TestCase(ExecutionMode.MultiThreaded)] 26 + public void TestPauseResume(ExecutionMode threadMode) 21 27 { 22 28 var gameCreated = new ManualResetEventSlim(); 29 + 30 + IBindable<GameThreadState> updateThreadState = null; 23 31 24 32 var task = Task.Run(() => 25 33 { 26 - using (host = new HeadlessGameHost(@"host", false)) 34 + using (host = new ExecutionModeGameHost(@"host", threadMode)) 27 35 { 28 36 game = new TestTestGame(); 29 37 gameCreated.Set(); ··· 36 44 37 45 // check scheduling is working before suspend 38 46 var completed = new ManualResetEventSlim(); 39 - game.Schedule(() => completed.Set()); 47 + game.Schedule(() => 48 + { 49 + updateThreadState = host.UpdateThread.State.GetBoundCopy(); 50 + updateThreadState.BindValueChanged(state => 51 + { 52 + if (state.NewValue != GameThreadState.Starting) 53 + Assert.IsTrue(ThreadSafety.IsUpdateThread); 54 + }); 55 + completed.Set(); 56 + }); 57 + 40 58 Assert.IsTrue(completed.Wait(timeout / 10)); 59 + Assert.AreEqual(GameThreadState.Running, updateThreadState.Value); 41 60 42 61 host.Suspend(); 43 62 ··· 45 64 int gameUpdates = 0; 46 65 game.Scheduler.AddDelayed(() => ++gameUpdates, 0, true); 47 66 Assert.That(() => gameUpdates, Is.LessThan(2).After(timeout / 10)); 67 + Assert.AreEqual(GameThreadState.Paused, updateThreadState.Value); 48 68 49 69 // check that scheduler doesn't process while suspended.. 50 70 completed.Reset(); ··· 54 74 // ..and does after resume. 55 75 host.Resume(); 56 76 Assert.IsTrue(completed.Wait(timeout / 10)); 77 + Assert.AreEqual(GameThreadState.Running, updateThreadState.Value); 57 78 58 79 game.Exit(); 59 80 Assert.IsTrue(task.Wait(timeout)); 81 + Assert.AreEqual(GameThreadState.Exited, updateThreadState.Value); 82 + } 83 + 84 + private class ExecutionModeGameHost : HeadlessGameHost 85 + { 86 + private readonly ExecutionMode threadMode; 87 + 88 + public ExecutionModeGameHost(string name, ExecutionMode threadMode) 89 + : base(name) 90 + { 91 + this.threadMode = threadMode; 92 + } 93 + 94 + protected override void SetupConfig(IDictionary<FrameworkSetting, object> defaultOverrides) 95 + { 96 + base.SetupConfig(defaultOverrides); 97 + Config.SetValue(FrameworkSetting.ExecutionMode, threadMode); 98 + } 60 99 } 61 100 62 101 private class TestTestGame : TestGame
+31
osu.Framework.Tests/Visual/Platform/TestSceneExecutionModes.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using NUnit.Framework; 5 + using osu.Framework.Allocation; 6 + using osu.Framework.Bindables; 7 + using osu.Framework.Configuration; 8 + using osu.Framework.Platform; 9 + 10 + namespace osu.Framework.Tests.Visual.Platform 11 + { 12 + [Ignore("This test does not cover correct GL context acquire/release when run headless.")] 13 + public class TestSceneExecutionModes : FrameworkTestScene 14 + { 15 + private Bindable<ExecutionMode> executionMode; 16 + 17 + [BackgroundDependencyLoader] 18 + private void load(FrameworkConfigManager configManager) 19 + { 20 + executionMode = configManager.GetBindable<ExecutionMode>(FrameworkSetting.ExecutionMode); 21 + } 22 + 23 + [Test] 24 + public void ToggleModeSmokeTest() 25 + { 26 + AddRepeatStep("toggle execution mode", () => executionMode.Value = executionMode.Value == ExecutionMode.MultiThreaded 27 + ? ExecutionMode.SingleThread 28 + : ExecutionMode.MultiThreaded, 2); 29 + } 30 + } 31 + }
+3
osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs
··· 24 24 private readonly SpriteText currentActualSize = new SpriteText(); 25 25 private readonly SpriteText currentDisplayMode = new SpriteText(); 26 26 private readonly SpriteText currentWindowMode = new SpriteText(); 27 + private readonly SpriteText currentWindowState = new SpriteText(); 27 28 private readonly SpriteText supportedWindowModes = new SpriteText(); 28 29 private readonly Dropdown<Display> displaysDropdown; 29 30 ··· 45 46 currentActualSize, 46 47 currentDisplayMode, 47 48 currentWindowMode, 49 + currentWindowState, 48 50 supportedWindowModes, 49 51 displaysDropdown = new BasicDropdown<Display> { Width = 600 } 50 52 }, ··· 143 145 144 146 currentActualSize.Text = $"Window size: {window?.ClientSize}"; 145 147 currentDisplayMode.Text = $"Display mode: {window?.CurrentDisplayMode}"; 148 + currentWindowState.Text = $"Window State: {window?.WindowState}"; 146 149 } 147 150 148 151 private void testResolution(int w, int h)
+2 -1
osu.Framework.Tests/Visual/Sprites/TestSceneSpriteIcon.cs
··· 11 11 using osu.Framework.Graphics.Cursor; 12 12 using osu.Framework.Graphics.Shapes; 13 13 using osu.Framework.Graphics.Sprites; 14 + using osu.Framework.Localisation; 14 15 using osuTK; 15 16 using osuTK.Graphics; 16 17 ··· 75 76 76 77 private class Icon : Container, IHasTooltip 77 78 { 78 - public string TooltipText { get; } 79 + public LocalisableString TooltipText { get; } 79 80 80 81 public SpriteIcon SpriteIcon { get; } 81 82
+1 -1
osu.Framework.Tests/Visual/Sprites/TestSceneSpriteText.cs
··· 49 49 SpriteText text = new SpriteText 50 50 { 51 51 Text = $@"Font testy at size {i}", 52 - Font = new FontUsage("OpenSans", i, i % 4 > 1 ? "Bold" : "Regular", i % 2 == 1), 52 + Font = new FontUsage("Roboto", i, i % 4 > 1 ? "Bold" : "Regular", i % 2 == 1), 53 53 AllowMultiline = true, 54 54 RelativeSizeAxes = Axes.X, 55 55 };
+21
osu.Framework.Tests/Visual/UserInterface/TestSceneColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Graphics; 5 + using osu.Framework.Graphics.UserInterface; 6 + using osu.Framework.Testing; 7 + 8 + namespace osu.Framework.Tests.Visual.UserInterface 9 + { 10 + public class TestSceneColourPicker : FrameworkTestScene 11 + { 12 + [SetUpSteps] 13 + public void SetUpSteps() 14 + { 15 + ColourPicker colourPicker = null; 16 + 17 + AddStep("create picker", () => Child = colourPicker = new BasicColourPicker()); 18 + AddStep("set colour externally", () => colourPicker.Current.Value = Colour4.CornflowerBlue); 19 + } 20 + } 21 + }
+155
osu.Framework.Tests/Visual/UserInterface/TestSceneHSVColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System.Linq; 5 + using NUnit.Framework; 6 + using osu.Framework.Graphics; 7 + using osu.Framework.Graphics.UserInterface; 8 + using osu.Framework.Testing; 9 + using osu.Framework.Utils; 10 + using osuTK; 11 + using osuTK.Graphics; 12 + using osuTK.Input; 13 + 14 + namespace osu.Framework.Tests.Visual.UserInterface 15 + { 16 + public class TestSceneHSVColourPicker : ManualInputManagerTestScene 17 + { 18 + private TestHSVColourPicker colourPicker; 19 + 20 + [SetUpSteps] 21 + public void SetUpSteps() 22 + { 23 + AddStep("create picker", () => Child = colourPicker = new TestHSVColourPicker()); 24 + } 25 + 26 + [Test] 27 + public void HueSelectorInput() 28 + { 29 + assertHue(0); 30 + 31 + AddStep("click selector centre", () => 32 + { 33 + InputManager.MoveMouseTo(colourPicker.HueControl.ScreenSpaceDrawQuad.Centre); 34 + InputManager.Click(MouseButton.Left); 35 + }); 36 + assertHue(0.5f); 37 + 38 + AddStep("click right edge of selector", () => 39 + { 40 + InputManager.MoveMouseTo(new Vector2(colourPicker.HueControl.ScreenSpaceDrawQuad.TopRight.X - 1, colourPicker.HueControl.ScreenSpaceDrawQuad.Centre.Y)); 41 + InputManager.Click(MouseButton.Left); 42 + }); 43 + assertHue(1); 44 + 45 + AddStep("drag back to start", () => 46 + { 47 + InputManager.PressButton(MouseButton.Left); 48 + InputManager.MoveMouseTo(new Vector2(colourPicker.HueControl.ScreenSpaceDrawQuad.TopLeft.X + 1, colourPicker.HueControl.ScreenSpaceDrawQuad.Centre.Y)); 49 + InputManager.ReleaseButton(MouseButton.Left); 50 + }); 51 + assertHue(0); 52 + } 53 + 54 + [Test] 55 + public void SaturationValueSelectorInput() 56 + { 57 + AddStep("set initial colour", () => colourPicker.Current.Value = Color4.Red); 58 + assertSaturationAndValue(1, 1, 0); 59 + 60 + AddStep("click top left corner", () => 61 + { 62 + InputManager.MoveMouseTo(colourPicker.SaturationValueControl.ScreenSpaceDrawQuad.TopLeft + new Vector2(1)); 63 + InputManager.Click(MouseButton.Left); 64 + }); 65 + assertSaturationAndValue(0, 1); 66 + 67 + AddStep("drag to center", () => 68 + { 69 + InputManager.PressButton(MouseButton.Left); 70 + InputManager.MoveMouseTo(colourPicker.SaturationValueControl.ScreenSpaceDrawQuad.Centre); 71 + InputManager.ReleaseButton(MouseButton.Left); 72 + }); 73 + assertSaturationAndValue(0.5f, 0.5f); 74 + 75 + AddStep("click bottom left corner", () => 76 + { 77 + InputManager.MoveMouseTo(colourPicker.SaturationValueControl.ScreenSpaceDrawQuad.BottomLeft + new Vector2(1, -1)); 78 + InputManager.Click(MouseButton.Left); 79 + }); 80 + assertSaturationAndValue(0, 0); 81 + 82 + AddStep("click bottom right corner", () => 83 + { 84 + InputManager.MoveMouseTo(colourPicker.SaturationValueControl.ScreenSpaceDrawQuad.BottomRight - new Vector2(1)); 85 + InputManager.Click(MouseButton.Left); 86 + }); 87 + assertSaturationAndValue(1, 0); 88 + 89 + AddStep("change hue", () => 90 + { 91 + InputManager.MoveMouseTo(colourPicker.HueControl.ScreenSpaceDrawQuad.Centre); 92 + InputManager.Click(MouseButton.Left); 93 + }); 94 + assertHue(0.5f); 95 + } 96 + 97 + [Test] 98 + public void TestExternalChange() 99 + { 100 + const float hue = 0.34f; 101 + const float saturation = 0.46f; 102 + const float value = 0.84f; 103 + Colour4 colour = Colour4.FromHSV(hue, saturation, value); 104 + 105 + AddStep("set colour", () => colourPicker.Current.Value = colour); 106 + 107 + assertHue(hue); 108 + assertSaturationAndValue(saturation, value); 109 + } 110 + 111 + [Test] 112 + public void TestHueUnchangedIfSaturationAlmostZero() 113 + { 114 + AddStep("change colour", () => colourPicker.Current.Value = Colour4.FromHSV(0.5f, 0.5f, 0.5f)); 115 + AddStep("set saturation to 0", () => colourPicker.SaturationValueControl.Saturation.Value = 0); 116 + AddAssert("hue is unchanged", () => colourPicker.HueControl.Hue.Value == 0.5f); 117 + } 118 + 119 + [Test] 120 + public void TestHueDoesNotWrapAround() 121 + { 122 + // the hue of 1 is special, because it's equivalent to hue of 0. 123 + // we want to make sure there is never a jump from 1 to 0, since it doesn't actually do anything. 124 + AddStep("set hue to 1", () => colourPicker.HueControl.Hue.Value = 1); 125 + AddStep("click saturation value control", () => 126 + { 127 + InputManager.MoveMouseTo(colourPicker.SaturationValueControl.ScreenSpaceDrawQuad.Centre); 128 + InputManager.Click(MouseButton.Left); 129 + }); 130 + assertHue(1, 0); 131 + 132 + AddStep("set hue to 1", () => colourPicker.HueControl.Hue.Value = 1); 133 + AddStep("set colour externally", () => colourPicker.Current.Value = Colour4.Red); 134 + assertHue(1, 0); 135 + } 136 + 137 + private void assertHue(float hue, float tolerance = 0.005f) 138 + { 139 + AddAssert($"hue selector has {hue}", () => Precision.AlmostEquals(colourPicker.HueControl.Hue.Value, hue, tolerance)); 140 + AddAssert($"saturation/value selector has {hue}", () => Precision.AlmostEquals(colourPicker.SaturationValueControl.Hue.Value, hue, tolerance)); 141 + } 142 + 143 + private void assertSaturationAndValue(float saturation, float value, float tolerance = 0.005f) 144 + { 145 + AddAssert($"saturation is {saturation}", () => Precision.AlmostEquals(colourPicker.SaturationValueControl.Saturation.Value, saturation, tolerance)); 146 + AddAssert($"value is {value}", () => Precision.AlmostEquals(colourPicker.SaturationValueControl.Value.Value, value, tolerance)); 147 + } 148 + 149 + private class TestHSVColourPicker : BasicHSVColourPicker 150 + { 151 + public HueSelector HueControl => this.ChildrenOfType<HueSelector>().Single(); 152 + public SaturationValueSelector SaturationValueControl => this.ChildrenOfType<SaturationValueSelector>().Single(); 153 + } 154 + } 155 + }
+112
osu.Framework.Tests/Visual/UserInterface/TestSceneHexColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System.Linq; 5 + using NUnit.Framework; 6 + using osu.Framework.Graphics; 7 + using osu.Framework.Graphics.Containers; 8 + using osu.Framework.Graphics.Shapes; 9 + using osu.Framework.Graphics.Sprites; 10 + using osu.Framework.Graphics.UserInterface; 11 + using osu.Framework.Testing; 12 + using osuTK; 13 + using osuTK.Input; 14 + 15 + namespace osu.Framework.Tests.Visual.UserInterface 16 + { 17 + public class TestSceneHexColourPicker : ManualInputManagerTestScene 18 + { 19 + private TestHexColourPicker hexColourPicker; 20 + private SpriteText currentText; 21 + private Box currentPreview; 22 + 23 + [SetUpSteps] 24 + public void SetUpSteps() 25 + { 26 + AddStep("create content", () => 27 + { 28 + Child = new FillFlowContainer 29 + { 30 + RelativeSizeAxes = Axes.Both, 31 + Direction = FillDirection.Vertical, 32 + Spacing = new Vector2(0, 10), 33 + Children = new Drawable[] 34 + { 35 + hexColourPicker = new TestHexColourPicker(), 36 + new FillFlowContainer 37 + { 38 + RelativeSizeAxes = Axes.X, 39 + AutoSizeAxes = Axes.Y, 40 + Direction = FillDirection.Horizontal, 41 + Spacing = new Vector2(10, 0), 42 + Children = new Drawable[] 43 + { 44 + currentText = new SpriteText(), 45 + new Container 46 + { 47 + Width = 50, 48 + RelativeSizeAxes = Axes.Y, 49 + Child = currentPreview = new Box 50 + { 51 + RelativeSizeAxes = Axes.Both 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }; 58 + 59 + hexColourPicker.Current.BindValueChanged(colour => 60 + { 61 + currentText.Text = $"Current.Value = {colour.NewValue.ToHex()}"; 62 + currentPreview.Colour = colour.NewValue; 63 + }, true); 64 + }); 65 + } 66 + 67 + [Test] 68 + public void TestExternalChange() 69 + { 70 + Colour4 colour = Colour4.Yellow; 71 + 72 + AddStep("set current colour", () => hexColourPicker.Current.Value = colour); 73 + 74 + AddAssert("hex code updated", () => hexColourPicker.HexCodeTextBox.Text == colour.ToHex()); 75 + assertPreviewUpdated(colour); 76 + } 77 + 78 + [Test] 79 + public void TestTextBoxBehaviour() 80 + { 81 + clickTextBox(); 82 + AddStep("insert valid colour", () => hexColourPicker.HexCodeTextBox.Text = "#ff00ff"); 83 + assertPreviewUpdated(Colour4.Magenta); 84 + AddAssert("current not changed yet", () => hexColourPicker.Current.Value == Colour4.White); 85 + 86 + AddStep("commit text", () => InputManager.Key(Key.Enter)); 87 + AddAssert("current updated", () => hexColourPicker.Current.Value == Colour4.Magenta); 88 + 89 + clickTextBox(); 90 + AddStep("insert invalid colour", () => hexColourPicker.HexCodeTextBox.Text = "c0d0"); 91 + AddStep("commit text", () => InputManager.Key(Key.Enter)); 92 + AddAssert("current not changed", () => hexColourPicker.Current.Value == Colour4.Magenta); 93 + AddAssert("old hex code restored", () => hexColourPicker.HexCodeTextBox.Text == "#FF00FF"); 94 + } 95 + 96 + private void clickTextBox() 97 + => AddStep("click text box", () => 98 + { 99 + InputManager.MoveMouseTo(hexColourPicker.HexCodeTextBox); 100 + InputManager.Click(MouseButton.Left); 101 + }); 102 + 103 + private void assertPreviewUpdated(Colour4 expected) 104 + => AddAssert("preview colour updated", () => hexColourPicker.Preview.Current.Value == expected); 105 + 106 + private class TestHexColourPicker : BasicHexColourPicker 107 + { 108 + public TextBox HexCodeTextBox => this.ChildrenOfType<TextBox>().Single(); 109 + public ColourPreview Preview => this.ChildrenOfType<ColourPreview>().Single(); 110 + } 111 + } 112 + }
+14 -1
osu.Framework.Tests/Visual/UserInterface/TestSceneMarkdownContainer.cs
··· 277 277 AddAssert("has correct link", () => markdownContainer.Links[0].Url == "https://some.test.url/file"); 278 278 } 279 279 280 + [Test] 281 + public void TestAbsoluteLinkWithDifferentScheme() 282 + { 283 + AddStep("set content", () => 284 + { 285 + markdownContainer.DocumentUrl = "https://some.test.url/some/path/2"; 286 + markdownContainer.RootUrl = "https://some.test.url/some/"; 287 + markdownContainer.Text = "[link](mailto:contact@ppy.sh)"; 288 + }); 289 + 290 + AddAssert("has correct link", () => markdownContainer.Links[0].Url == "mailto:contact@ppy.sh"); 291 + } 292 + 280 293 private class TestMarkdownContainer : MarkdownContainer 281 294 { 282 295 public new string DocumentUrl ··· 298 311 UrlAdded = url => Links.Add(url) 299 312 }; 300 313 301 - public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With("OpenSans", weight: "Regular")); 314 + public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With("Roboto", weight: "Regular")); 302 315 303 316 private class TestMarkdownTextFlowContainer : MarkdownTextFlowContainer 304 317 {
+1
osu.Framework.Tests/Visual/UserInterface/TestSceneScreenStack.cs
··· 240 240 TestScreen screen1 = null; 241 241 242 242 AddStep("push once", () => stack.Push(screen1 = new TestScreen())); 243 + AddUntilStep("wait for screen to be loaded", () => screen1.IsLoaded); 243 244 AddStep("exit", () => screen1.Exit()); 244 245 AddStep("push again fails", () => Assert.Throws<InvalidOperationException>(() => stack.Push(screen1))); 245 246 AddAssert("stack in valid state", () => stack.CurrentScreen == baseScreen);
+6 -5
osu.Framework.Tests/Visual/UserInterface/TestSceneTooltip.cs
··· 7 7 using osu.Framework.Graphics.Shapes; 8 8 using osu.Framework.Graphics.Sprites; 9 9 using osu.Framework.Graphics.UserInterface; 10 + using osu.Framework.Localisation; 10 11 using osuTK; 11 12 using osuTK.Graphics; 12 13 ··· 166 167 167 168 private class CustomContent 168 169 { 169 - public readonly string Text; 170 + public readonly LocalisableString Text; 170 171 171 172 public CustomContent(string text) 172 173 { ··· 212 213 213 214 private class TooltipSpriteText : Container, IHasTooltip 214 215 { 215 - public string TooltipText { get; } 216 + public LocalisableString TooltipText { get; } 216 217 217 218 public TooltipSpriteText(string displayedContent) 218 219 : this(displayedContent, displayedContent) ··· 246 247 247 248 private class TooltipTooltipContainer : TooltipContainer, IHasTooltip 248 249 { 249 - public string TooltipText { get; set; } 250 + public LocalisableString TooltipText { get; set; } 250 251 251 252 public TooltipTooltipContainer(string tooltipText) 252 253 { ··· 256 257 257 258 private class TooltipTextbox : BasicTextBox, IHasTooltip 258 259 { 259 - public string TooltipText => Text; 260 + public LocalisableString TooltipText => Text; 260 261 } 261 262 262 263 private class TooltipBox : Box, IHasTooltip 263 264 { 264 - public string TooltipText { get; set; } 265 + public LocalisableString TooltipText { get; set; } 265 266 } 266 267 267 268 private class RectangleCursorContainer : CursorContainer
+1 -1
osu.Framework/Audio/Track/Waveform.cs
··· 13 13 namespace osu.Framework.Audio.Track 14 14 { 15 15 /// <summary> 16 - /// Procsses audio sample data such that it can then be consumed to generate waveform plots of the audio. 16 + /// Processes audio sample data such that it can then be consumed to generate waveform plots of the audio. 17 17 /// </summary> 18 18 public class Waveform : IDisposable 19 19 {
+1 -1
osu.Framework/Bindables/NonNullableBindable.cs
··· 22 22 set 23 23 { 24 24 if (value == null) 25 - throw new ArgumentNullException(nameof(Value), $"Cannot set {nameof(Value)} of a {nameof(NonNullableBindable<T>)} to null."); 25 + throw new ArgumentNullException(nameof(value), $"Cannot set {nameof(Value)} of a {nameof(NonNullableBindable<T>)} to null."); 26 26 27 27 base.Value = value; 28 28 }
+55
osu.Framework/Extensions/ExtensionMethods.cs
··· 4 4 using System; 5 5 using System.Collections.Generic; 6 6 using System.ComponentModel; 7 + using System.Diagnostics; 7 8 using System.Drawing; 8 9 using System.IO; 9 10 using System.Linq; 10 11 using System.Reflection; 11 12 using System.Security.Cryptography; 12 13 using System.Text; 14 + using osu.Framework.Extensions.TypeExtensions; 15 + using osu.Framework.Localisation; 13 16 using osu.Framework.Platform; 14 17 using osuTK; 15 18 ··· 173 176 } 174 177 } 175 178 179 + /// <summary> 180 + /// Returns the description of a given enum value, via (in order): 181 + /// <list type="number"> 182 + /// <item> 183 + /// <description>Any <see cref="LocalisableEnumAttribute"/> attached to the enum type.</description> 184 + /// </item> 185 + /// <item> 186 + /// <description><see cref="GetDescription"/></description> 187 + /// </item> 188 + /// </list> 189 + /// </summary> 190 + /// <exception cref="InvalidOperationException">When the enum type has an attached <see cref="LocalisableEnumAttribute"/> 191 + /// and the <see cref="EnumLocalisationMapper{T}"/> could not be instantiated.</exception> 192 + /// <exception cref="InvalidOperationException">When the enum type has an attached <see cref="LocalisableEnumAttribute"/> 193 + /// and the type handled by the <see cref="EnumLocalisationMapper{T}"/> is not <typeparamref name="T"/>.</exception> 194 + public static LocalisableString GetLocalisableDescription<T>(this T value) 195 + where T : Enum 196 + { 197 + var enumType = value.GetType(); 198 + 199 + var mapperType = enumType.GetCustomAttribute<LocalisableEnumAttribute>()?.MapperType; 200 + if (mapperType == null) 201 + return GetDescription(value); 202 + 203 + var mapperInstance = Activator.CreateInstance(mapperType); 204 + if (mapperInstance == null) 205 + throw new InvalidOperationException($"Could not create the {nameof(EnumLocalisationMapper<T>)} for enum type {enumType.ReadableName()}"); 206 + 207 + var mapMethod = mapperType.GetMethod(nameof(EnumLocalisationMapper<T>.Map), BindingFlags.Instance | BindingFlags.Public); 208 + Debug.Assert(mapMethod != null); 209 + 210 + var expectedMappingType = mapMethod.GetParameters()[0].ParameterType; 211 + if (expectedMappingType != enumType) 212 + throw new InvalidOperationException($"Cannot use {mapperType.ReadableName()} (maps {expectedMappingType.ReadableName()} enum values) to map {enumType.ReadableName()} enum values."); 213 + 214 + var mappedValue = mapMethod.Invoke(mapperInstance, new object[] { value }); 215 + Debug.Assert(mappedValue != null); 216 + 217 + return (LocalisableString)mappedValue; 218 + } 219 + 220 + /// <summary> 221 + /// Returns the description of a given object, via (in order): 222 + /// <list type="number"> 223 + /// <item> 224 + /// <description>Any attached <see cref="DescriptionAttribute"/>.</description> 225 + /// </item> 226 + /// <item> 227 + /// <description>The object's <see cref="object.ToString()"/>.</description> 228 + /// </item> 229 + /// </list> 230 + /// </summary> 176 231 public static string GetDescription(this object value) 177 232 => value.GetType() 178 233 .GetField(value.ToString())?
+9 -4
osu.Framework/Game.cs
··· 157 157 // note that currently this means there could be two async font load operations. 158 158 Fonts.AddStore(localFonts = new FontStore(useAtlas: false)); 159 159 160 - addFont(localFonts, Resources, @"Fonts/OpenSans/OpenSans-Regular"); 161 - addFont(localFonts, Resources, @"Fonts/OpenSans/OpenSans-Bold"); 162 - addFont(localFonts, Resources, @"Fonts/OpenSans/OpenSans-RegularItalic"); 163 - addFont(localFonts, Resources, @"Fonts/OpenSans/OpenSans-BoldItalic"); 160 + // Roboto (FrameworkFont.Regular) 161 + addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-Regular"); 162 + addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-RegularItalic"); 163 + addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-Bold"); 164 + addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-BoldItalic"); 165 + 166 + // RobotoCondensed (FrameworkFont.Condensed) 167 + addFont(localFonts, Resources, @"Fonts/RobotoCondensed/RobotoCondensed-Regular"); 168 + addFont(localFonts, Resources, @"Fonts/RobotoCondensed/RobotoCondensed-Bold"); 164 169 165 170 addFont(Fonts, Resources, @"Fonts/FontAwesome5/FontAwesome-Solid"); 166 171 addFont(Fonts, Resources, @"Fonts/FontAwesome5/FontAwesome-Regular");
+60 -21
osu.Framework/Graphics/Colour4.cs
··· 283 283 /// <exception cref="ArgumentException">If <paramref name="hex"/> is not a supported colour code.</exception> 284 284 public static Colour4 FromHex(string hex) 285 285 { 286 + if (!TryParseHex(hex, out var colour)) 287 + throw new ArgumentException($"{hex} is not a valid colour hex string.", nameof(hex)); 288 + 289 + return colour; 290 + } 291 + 292 + /// <summary> 293 + /// Attempts to convert an RGB or RGBA-formatted hex colour code into a <see cref="Colour4"/>. 294 + /// Supported colour code formats: 295 + /// <list type="bullet"> 296 + /// <item><description>RGB</description></item> 297 + /// <item><description>#RGB</description></item> 298 + /// <item><description>RGBA</description></item> 299 + /// <item><description>#RGBA</description></item> 300 + /// <item><description>RRGGBB</description></item> 301 + /// <item><description>#RRGGBB</description></item> 302 + /// <item><description>RRGGBBAA</description></item> 303 + /// <item><description>#RRGGBBAA</description></item> 304 + /// </list> 305 + /// </summary> 306 + /// <param name="hex">The hex code.</param> 307 + /// <param name="colour">The <see cref="Colour4"/> representing the colour, if parsing succeeded.</param> 308 + /// <returns>Whether the input could be parsed as a hex code.</returns> 309 + public static bool TryParseHex(string hex, out Colour4 colour) 310 + { 286 311 var hexSpan = hex.StartsWith('#') ? hex.AsSpan(1) : hex.AsSpan(); 287 312 313 + bool parsed = true; 314 + byte r = 255, g = 255, b = 255, a = 255; 315 + 288 316 switch (hexSpan.Length) 289 317 { 290 318 default: 291 - throw new ArgumentException($"Invalid hex string length {hex.Length}, expected 3, 4, 6, or 8.", nameof(hex)); 319 + parsed = false; 320 + break; 292 321 293 322 case 3: 294 - return new Colour4( 295 - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), 296 - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), 297 - (byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17), 298 - 255); 323 + parsed &= byte.TryParse(hexSpan.Slice(0, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r); 324 + parsed &= byte.TryParse(hexSpan.Slice(1, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g); 325 + parsed &= byte.TryParse(hexSpan.Slice(2, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b); 326 + 327 + r *= 17; 328 + g *= 17; 329 + b *= 17; 330 + break; 299 331 300 332 case 6: 301 - return new Colour4( 302 - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), 303 - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), 304 - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), 305 - 255); 333 + parsed &= byte.TryParse(hexSpan.Slice(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r); 334 + parsed &= byte.TryParse(hexSpan.Slice(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g); 335 + parsed &= byte.TryParse(hexSpan.Slice(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b); 336 + break; 306 337 307 338 case 4: 308 - return new Colour4( 309 - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), 310 - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), 311 - (byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17), 312 - (byte)(byte.Parse(hexSpan.Slice(3, 1), NumberStyles.HexNumber) * 17)); 339 + parsed &= byte.TryParse(hexSpan.Slice(0, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r); 340 + parsed &= byte.TryParse(hexSpan.Slice(1, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g); 341 + parsed &= byte.TryParse(hexSpan.Slice(2, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b); 342 + parsed &= byte.TryParse(hexSpan.Slice(3, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out a); 343 + 344 + r *= 17; 345 + g *= 17; 346 + b *= 17; 347 + a *= 17; 348 + break; 313 349 314 350 case 8: 315 - return new Colour4( 316 - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), 317 - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), 318 - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), 319 - byte.Parse(hexSpan.Slice(6, 2), NumberStyles.HexNumber)); 351 + parsed &= byte.TryParse(hexSpan.Slice(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r); 352 + parsed &= byte.TryParse(hexSpan.Slice(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g); 353 + parsed &= byte.TryParse(hexSpan.Slice(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b); 354 + parsed &= byte.TryParse(hexSpan.Slice(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out a); 355 + break; 320 356 } 357 + 358 + colour = new Colour4(r, g, b, a); 359 + return parsed; 321 360 } 322 361 323 362 /// <summary>
+2 -2
osu.Framework/Graphics/Containers/GridContainer.cs
··· 73 73 set 74 74 { 75 75 if (value == null) 76 - throw new ArgumentNullException(nameof(RowDimensions)); 76 + throw new ArgumentNullException(nameof(value)); 77 77 78 78 if (rowDimensions == value) 79 79 return; ··· 94 94 set 95 95 { 96 96 if (value == null) 97 - throw new ArgumentNullException(nameof(ColumnDimensions)); 97 + throw new ArgumentNullException(nameof(value)); 98 98 99 99 if (columnDimensions == value) 100 100 return;
+4 -3
osu.Framework/Graphics/Containers/Markdown/MarkdownContainer.cs
··· 12 12 using osu.Framework.Caching; 13 13 using osu.Framework.Extensions.EnumExtensions; 14 14 using osu.Framework.Graphics.Sprites; 15 + using osu.Framework.Utils; 15 16 using osuTK; 16 17 17 18 namespace osu.Framework.Graphics.Containers.Markdown ··· 177 178 if (string.IsNullOrEmpty(url)) 178 179 continue; 179 180 180 - // Can't use Uri.TryCreate with RelativeOrAbsolute, see https://www.mono-project.com/docs/faq/known-issues/urikind-relativeorabsolute/. 181 - bool isAbsolute = url.Contains("://"); 181 + if (!Validation.TryParseUri(url, out Uri linkUri)) 182 + continue; 182 183 183 - if (isAbsolute) 184 + if (linkUri.IsAbsoluteUri) 184 185 continue; 185 186 186 187 if (documentUri != null)
+18 -8
osu.Framework/Graphics/Containers/Markdown/MarkdownHeading.cs
··· 3 3 4 4 using Markdig.Syntax; 5 5 using osu.Framework.Allocation; 6 - using osuTK; 6 + using osu.Framework.Graphics.Sprites; 7 7 8 8 namespace osu.Framework.Graphics.Containers.Markdown 9 9 { ··· 36 36 MarkdownTextFlowContainer textFlow; 37 37 InternalChild = textFlow = CreateTextFlow(); 38 38 39 - textFlow.Scale = new Vector2(GetFontSizeByLevel(headingBlock.Level)); 40 39 textFlow.AddInlineText(headingBlock.Inline); 41 40 } 42 41 43 - public virtual MarkdownTextFlowContainer CreateTextFlow() => parentFlowComponent.CreateTextFlow(); 42 + public virtual MarkdownTextFlowContainer CreateTextFlow() => new MarkdownHeadingTextFlowContainer 43 + { 44 + FontSize = GetFontSizeByLevel(headingBlock.Level), 45 + }; 44 46 45 47 protected virtual float GetFontSizeByLevel(int level) 46 48 { 47 49 switch (level) 48 50 { 49 51 case 1: 50 - return 2.7f; 52 + return 54; 51 53 52 54 case 2: 53 - return 2; 55 + return 40; 54 56 55 57 case 3: 56 - return 1.5f; 58 + return 30; 57 59 58 60 case 4: 59 - return 1.3f; 61 + return 26; 60 62 61 63 default: 62 - return 1; 64 + return 20; 63 65 } 66 + } 67 + 68 + private class MarkdownHeadingTextFlowContainer : MarkdownTextFlowContainer 69 + { 70 + public float FontSize; 71 + 72 + protected override SpriteText CreateSpriteText() 73 + => base.CreateSpriteText().With(t => t.Font = t.Font.With(size: FontSize)); 64 74 } 65 75 } 66 76 }
+2 -1
osu.Framework/Graphics/Containers/Markdown/MarkdownLinkText.cs
··· 5 5 using osu.Framework.Allocation; 6 6 using osu.Framework.Graphics.Cursor; 7 7 using osu.Framework.Graphics.Sprites; 8 + using osu.Framework.Localisation; 8 9 using osu.Framework.Platform; 9 10 using osuTK.Graphics; 10 11 ··· 18 19 /// </code> 19 20 public class MarkdownLinkText : CompositeDrawable, IHasTooltip, IMarkdownTextComponent 20 21 { 21 - public string TooltipText => Url; 22 + public LocalisableString TooltipText => Url; 22 23 23 24 [Resolved] 24 25 private IMarkdownTextComponent parentTextComponent { get; set; }
+3 -1
osu.Framework/Graphics/Cursor/IHasTooltip.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using osu.Framework.Localisation; 5 + 4 6 namespace osu.Framework.Graphics.Cursor 5 7 { 6 8 /// <summary> ··· 12 14 /// <summary> 13 15 /// Tooltip text that shows when hovering the drawable. 14 16 /// </summary> 15 - string TooltipText { get; } 17 + LocalisableString TooltipText { get; } 16 18 } 17 19 }
+5 -4
osu.Framework/Graphics/Cursor/TooltipContainer.cs
··· 9 9 using osu.Framework.Graphics.Shapes; 10 10 using osu.Framework.Graphics.Sprites; 11 11 using osu.Framework.Input; 12 + using osu.Framework.Localisation; 12 13 using osuTK; 13 14 using osuTK.Graphics; 14 15 ··· 178 179 { 179 180 var targetContent = getTargetContent(target); 180 181 181 - if (targetContent is string strContent) 182 - return !string.IsNullOrEmpty(strContent); 182 + if (targetContent is LocalisableString localisableString) 183 + return !string.IsNullOrEmpty(localisableString.Data?.ToString()); 183 184 184 185 return targetContent != null; 185 186 } ··· 318 319 319 320 public virtual bool SetContent(object content) 320 321 { 321 - if (!(content is string contentString)) 322 + if (!(content is LocalisableString contentString)) 322 323 return false; 323 324 324 325 text.Text = contentString; ··· 344 345 }, 345 346 text = new SpriteText 346 347 { 347 - Font = new FontUsage(size: text_size), 348 + Font = FrameworkFont.Regular.With(size: text_size), 348 349 Padding = new MarginPadding(5), 349 350 } 350 351 };
+8 -4
osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs
··· 134 134 Origin = Anchor.BottomCentre, 135 135 Anchor = Anchor.CentreLeft, 136 136 Rotation = -90, 137 + Font = FrameworkFont.Regular, 137 138 }, 138 139 !hasCounters 139 140 ? new Container { Width = 2 } ··· 210 211 { 211 212 Colour = getColour(t), 212 213 Text = t.ToString(), 213 - Alpha = 0 214 + Alpha = 0, 215 + Font = FrameworkFont.Regular, 214 216 }, 215 217 }, 216 218 new SpriteText 217 219 { 218 220 Padding = new MarginPadding { Left = 4 }, 219 - Text = $@"{visible_ms_range}ms" 221 + Text = $@"{visible_ms_range}ms", 222 + Font = FrameworkFont.Regular, 220 223 }, 221 224 new SpriteText 222 225 { 223 226 Padding = new MarginPadding { Left = 4 }, 224 227 Text = @"0ms", 225 228 Anchor = Anchor.BottomLeft, 226 - Origin = Anchor.BottomLeft 229 + Origin = Anchor.BottomLeft, 230 + Font = FrameworkFont.Regular, 227 231 } 228 232 } 229 233 } ··· 576 580 Anchor = Anchor.BottomRight, 577 581 Rotation = -90, 578 582 Position = new Vector2(-bar_width - 1, 0), 579 - Font = new FontUsage(size: 16), 583 + Font = FrameworkFont.Regular.With(size: 16), 580 584 }, 581 585 box = new Box 582 586 {
+1 -1
osu.Framework/Graphics/Performance/FrameTimeDisplay.cs
··· 111 111 { 112 112 public CounterText() 113 113 { 114 - Font = new FontUsage(fixedWidth: true); 114 + Font = FrameworkFont.Regular.With(fixedWidth: true); 115 115 } 116 116 117 117 protected override char[] FixedWidthExcludeCharacters { get; } = { ',', '.', ' ' };
+72 -34
osu.Framework/Graphics/Shaders/ShaderManager.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + #nullable enable 5 + 4 6 using System; 5 7 using System.Collections.Concurrent; 6 8 using System.Collections.Generic; ··· 9 11 10 12 namespace osu.Framework.Graphics.Shaders 11 13 { 12 - public class ShaderManager 14 + public class ShaderManager : IDisposable 13 15 { 14 16 private const string shader_prefix = @"sh_"; 15 17 16 18 private readonly ConcurrentDictionary<string, ShaderPart> partCache = new ConcurrentDictionary<string, ShaderPart>(); 17 19 private readonly ConcurrentDictionary<(string, string), Shader> shaderCache = new ConcurrentDictionary<(string, string), Shader>(); 18 20 19 - private readonly ResourceStore<byte[]> store; 21 + private readonly IResourceStore<byte[]> store; 20 22 21 - public ShaderManager(ResourceStore<byte[]> store) 23 + /// <summary> 24 + /// Constructs a new <see cref="ShaderManager"/>. 25 + /// </summary> 26 + public ShaderManager(IResourceStore<byte[]> store) 22 27 { 23 28 this.store = store; 24 29 } 25 30 26 - private string getFileEnding(ShaderType type) 31 + /// <summary> 32 + /// Retrieves raw shader data from the store. 33 + /// Use <see cref="Load"/> to retrieve a usable <see cref="IShader"/> instead. 34 + /// </summary> 35 + /// <param name="name">The shader name.</param> 36 + public virtual byte[]? LoadRaw(string name) => store.Get(name); 37 + 38 + /// <summary> 39 + /// Retrieves a usable <see cref="IShader"/> given the vertex and fragment shaders. 40 + /// </summary> 41 + /// <param name="vertex">The vertex shader name.</param> 42 + /// <param name="fragment">The fragment shader name.</param> 43 + /// <param name="continuousCompilation"></param> 44 + public IShader Load(string vertex, string fragment, bool continuousCompilation = false) 27 45 { 28 - switch (type) 46 + var tuple = (vertex, fragment); 47 + 48 + if (shaderCache.TryGetValue(tuple, out Shader? shader)) 49 + return shader; 50 + 51 + List<ShaderPart> parts = new List<ShaderPart> 29 52 { 30 - case ShaderType.FragmentShader: 31 - return @".fs"; 53 + createShaderPart(vertex, ShaderType.VertexShader), 54 + createShaderPart(fragment, ShaderType.FragmentShader) 55 + }; 32 56 33 - case ShaderType.VertexShader: 34 - return @".vs"; 35 - } 57 + return shaderCache[tuple] = new Shader($"{vertex}/{fragment}", parts); 58 + } 59 + 60 + private ShaderPart createShaderPart(string name, ShaderType type, bool bypassCache = false) 61 + { 62 + name = ensureValidName(name, type); 63 + 64 + if (!bypassCache && partCache.TryGetValue(name, out ShaderPart? part)) 65 + return part; 36 66 37 - return string.Empty; 67 + byte[]? rawData = LoadRaw(name); 68 + 69 + part = new ShaderPart(name, rawData, type, this); 70 + 71 + //cache even on failure so we don't try and fail every time. 72 + partCache[name] = part; 73 + return part; 38 74 } 39 75 40 76 private string ensureValidName(string name, ShaderType type) 41 77 { 42 78 string ending = getFileEnding(type); 79 + 43 80 if (!name.StartsWith(shader_prefix, StringComparison.Ordinal)) 44 81 name = shader_prefix + name; 45 82 if (name.EndsWith(ending, StringComparison.Ordinal)) ··· 48 85 return name + ending; 49 86 } 50 87 51 - internal byte[] LoadRaw(string name) => store.Get(name); 88 + private string getFileEnding(ShaderType type) 89 + { 90 + switch (type) 91 + { 92 + case ShaderType.FragmentShader: 93 + return @".fs"; 52 94 53 - private ShaderPart createShaderPart(string name, ShaderType type, bool bypassCache = false) 54 - { 55 - name = ensureValidName(name, type); 95 + case ShaderType.VertexShader: 96 + return @".vs"; 97 + } 56 98 57 - if (!bypassCache && partCache.TryGetValue(name, out ShaderPart part)) 58 - return part; 99 + return string.Empty; 100 + } 59 101 60 - byte[] rawData = LoadRaw(name); 102 + #region IDisposable Support 61 103 62 - part = new ShaderPart(name, rawData, type, this); 104 + private bool isDisposed; 63 105 64 - //cache even on failure so we don't try and fail every time. 65 - partCache[name] = part; 66 - return part; 106 + public void Dispose() 107 + { 108 + Dispose(true); 109 + GC.SuppressFinalize(this); 67 110 } 68 111 69 - public IShader Load(string vertex, string fragment, bool continuousCompilation = false) 112 + protected virtual void Dispose(bool disposing) 70 113 { 71 - var tuple = (vertex, fragment); 72 - 73 - if (shaderCache.TryGetValue(tuple, out Shader shader)) 74 - return shader; 75 - 76 - List<ShaderPart> parts = new List<ShaderPart> 114 + if (!isDisposed) 77 115 { 78 - createShaderPart(vertex, ShaderType.VertexShader), 79 - createShaderPart(fragment, ShaderType.FragmentShader) 80 - }; 116 + isDisposed = true; 117 + store.Dispose(); 118 + } 119 + } 81 120 82 - return shaderCache[tuple] = new Shader($"{vertex}/{fragment}", parts); 83 - } 121 + #endregion 84 122 } 85 123 86 124 public static class VertexShaderDescriptor
+11
osu.Framework/Graphics/UserInterface/BasicColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + namespace osu.Framework.Graphics.UserInterface 5 + { 6 + public class BasicColourPicker : ColourPicker 7 + { 8 + protected override HSVColourPicker CreateHSVColourPicker() => new BasicHSVColourPicker(); 9 + protected override HexColourPicker CreateHexColourPicker() => new BasicHexColourPicker(); 10 + } 11 + }
+88
osu.Framework/Graphics/UserInterface/BasicHSVColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Graphics.Containers; 5 + using osu.Framework.Graphics.Shapes; 6 + using osuTK; 7 + 8 + namespace osu.Framework.Graphics.UserInterface 9 + { 10 + public class BasicHSVColourPicker : HSVColourPicker 11 + { 12 + public BasicHSVColourPicker() 13 + { 14 + Background.Colour = FrameworkColour.GreenDark; 15 + 16 + Content.Padding = new MarginPadding(20); 17 + Content.Spacing = new Vector2(0, 10); 18 + } 19 + 20 + protected override HueSelector CreateHueSelector() => new BasicHueSelector(); 21 + protected override SaturationValueSelector CreateSaturationValueSelector() => new BasicSaturationValueSelector(); 22 + 23 + public class BasicHueSelector : HueSelector 24 + { 25 + protected override Drawable CreateSliderNub() => new BasicHueSelectorNub(); 26 + } 27 + 28 + public class BasicHueSelectorNub : CompositeDrawable 29 + { 30 + public BasicHueSelectorNub() 31 + { 32 + InternalChild = new Container 33 + { 34 + RelativeSizeAxes = Axes.Y, 35 + Width = 8, 36 + Height = 1.2f, 37 + Anchor = Anchor.Centre, 38 + Origin = Anchor.Centre, 39 + Child = new Box 40 + { 41 + RelativeSizeAxes = Axes.Both, 42 + Alpha = 0, 43 + AlwaysPresent = true 44 + }, 45 + Masking = true, 46 + BorderColour = FrameworkColour.YellowGreen, 47 + BorderThickness = 4 48 + }; 49 + } 50 + } 51 + 52 + public class BasicSaturationValueSelector : SaturationValueSelector 53 + { 54 + protected override Marker CreateMarker() => new BasicMarker(); 55 + 56 + private class BasicMarker : Marker 57 + { 58 + private readonly Box colourPreview; 59 + 60 + public BasicMarker() 61 + { 62 + InternalChild = new Container 63 + { 64 + Size = new Vector2(15), 65 + Anchor = Anchor.Centre, 66 + Origin = Anchor.Centre, 67 + Masking = true, 68 + BorderColour = FrameworkColour.YellowGreen, 69 + BorderThickness = 4, 70 + Child = colourPreview = new Box 71 + { 72 + RelativeSizeAxes = Axes.Both 73 + } 74 + }; 75 + } 76 + 77 + protected override void LoadComplete() 78 + { 79 + base.LoadComplete(); 80 + 81 + Current.BindValueChanged(_ => updatePreview(), true); 82 + } 83 + 84 + private void updatePreview() => colourPreview.Colour = Current.Value; 85 + } 86 + } 87 + } 88 + }
+50
osu.Framework/Graphics/UserInterface/BasicHexColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Graphics.Shapes; 5 + 6 + namespace osu.Framework.Graphics.UserInterface 7 + { 8 + public class BasicHexColourPicker : HexColourPicker 9 + { 10 + public BasicHexColourPicker() 11 + { 12 + Background.Colour = FrameworkColour.GreenDarker; 13 + 14 + Padding = new MarginPadding(20); 15 + Spacing = 10; 16 + } 17 + 18 + protected override TextBox CreateHexCodeTextBox() => new BasicTextBox 19 + { 20 + Height = 40 21 + }; 22 + 23 + protected override ColourPreview CreateColourPreview() => new BasicColourPreview(); 24 + 25 + private class BasicColourPreview : ColourPreview 26 + { 27 + private readonly Box previewBox; 28 + 29 + public BasicColourPreview() 30 + { 31 + InternalChild = previewBox = new Box 32 + { 33 + RelativeSizeAxes = Axes.Both 34 + }; 35 + } 36 + 37 + protected override void LoadComplete() 38 + { 39 + base.LoadComplete(); 40 + 41 + Current.BindValueChanged(_ => updatePreview(), true); 42 + } 43 + 44 + private void updatePreview() 45 + { 46 + previewBox.Colour = Current.Value; 47 + } 48 + } 49 + } 50 + }
+6 -2
osu.Framework/Graphics/UserInterface/BasicTabControl.cs
··· 27 27 { 28 28 Margin = new MarginPadding(2), 29 29 Text = value.ToString(), 30 - Font = new FontUsage(size: 18), 30 + Font = FrameworkFont.Regular.With(size: 18), 31 31 }); 32 32 } 33 33 ··· 61 61 Foreground.RelativeSizeAxes = Axes.None; 62 62 Foreground.AutoSizeAxes = Axes.Both; 63 63 64 - Foreground.Child = new SpriteText { Text = "…" }; 64 + Foreground.Child = new SpriteText 65 + { 66 + Text = "…", 67 + Font = FrameworkFont.Regular 68 + }; 65 69 } 66 70 } 67 71 }
+74
osu.Framework/Graphics/UserInterface/ColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Bindables; 5 + using osu.Framework.Graphics.Containers; 6 + 7 + namespace osu.Framework.Graphics.UserInterface 8 + { 9 + /// <summary> 10 + /// A group of controls to be used for selecting a colour. 11 + /// Allows both for mouse-interactive input (via <see cref="HSVColourPicker"/>) and textual input (via <see cref="HexColourPicker"/>). 12 + /// </summary> 13 + public abstract class ColourPicker : CompositeDrawable, IHasCurrentValue<Colour4> 14 + { 15 + private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>(); 16 + 17 + public Bindable<Colour4> Current 18 + { 19 + get => current.Current; 20 + set => current.Current = value; 21 + } 22 + 23 + private readonly HSVColourPicker hsvColourPicker; 24 + private readonly HexColourPicker hexColourPicker; 25 + 26 + protected ColourPicker() 27 + { 28 + Current.Value = Colour4.White; 29 + AutoSizeAxes = Axes.Y; 30 + Width = 300; 31 + 32 + InternalChildren = new Drawable[] 33 + { 34 + new FillFlowContainer 35 + { 36 + RelativeSizeAxes = Axes.X, 37 + AutoSizeAxes = Axes.Y, 38 + Direction = FillDirection.Vertical, 39 + Children = new Drawable[] 40 + { 41 + hsvColourPicker = CreateHSVColourPicker().With(d => 42 + { 43 + d.RelativeSizeAxes = Axes.X; 44 + d.Width = 1; 45 + }), 46 + hexColourPicker = CreateHexColourPicker().With(d => 47 + { 48 + d.RelativeSizeAxes = Axes.X; 49 + d.Width = 1; 50 + }) 51 + } 52 + } 53 + }; 54 + } 55 + 56 + /// <summary> 57 + /// Creates the control that allows for interactively specifying the target colour, using the hue-saturation-value colour model. 58 + /// </summary> 59 + protected abstract HSVColourPicker CreateHSVColourPicker(); 60 + 61 + /// <summary> 62 + /// Creates the control that allows for specifying the target colour using a hex code. 63 + /// </summary> 64 + protected abstract HexColourPicker CreateHexColourPicker(); 65 + 66 + protected override void LoadComplete() 67 + { 68 + base.LoadComplete(); 69 + 70 + hsvColourPicker.Current = Current; 71 + hexColourPicker.Current = Current; 72 + } 73 + } 74 + }
+1 -1
osu.Framework/Graphics/UserInterface/Dropdown.cs
··· 162 162 return t.Text; 163 163 164 164 case Enum e: 165 - return e.GetDescription(); 165 + return e.GetLocalisableDescription(); 166 166 167 167 default: 168 168 return item?.ToString() ?? "null";
+80
osu.Framework/Graphics/UserInterface/HSVColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Bindables; 5 + using osu.Framework.Graphics.Containers; 6 + using osu.Framework.Graphics.Shapes; 7 + 8 + namespace osu.Framework.Graphics.UserInterface 9 + { 10 + /// <summary> 11 + /// Control that allows for specifying a colour using the hue-saturation-value (HSV) colour model. 12 + /// </summary> 13 + public abstract partial class HSVColourPicker : CompositeDrawable, IHasCurrentValue<Colour4> 14 + { 15 + private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>(); 16 + 17 + public Bindable<Colour4> Current 18 + { 19 + get => current.Current; 20 + set => current.Current = value; 21 + } 22 + 23 + /// <summary> 24 + /// The background of the control. 25 + /// </summary> 26 + protected Box Background { get; } 27 + 28 + /// <summary> 29 + /// Contains the elements of the colour picker. 30 + /// </summary> 31 + protected FillFlowContainer Content { get; } 32 + 33 + private readonly SaturationValueSelector saturationValueSelector; 34 + private readonly HueSelector hueSelector; 35 + 36 + protected HSVColourPicker() 37 + { 38 + Width = 300; 39 + AutoSizeAxes = Axes.Y; 40 + Current.Value = Colour4.White; 41 + 42 + InternalChildren = new Drawable[] 43 + { 44 + Background = new Box 45 + { 46 + RelativeSizeAxes = Axes.Both 47 + }, 48 + Content = new FillFlowContainer 49 + { 50 + RelativeSizeAxes = Axes.X, 51 + AutoSizeAxes = Axes.Y, 52 + Direction = FillDirection.Vertical, 53 + Children = new Drawable[] 54 + { 55 + saturationValueSelector = CreateSaturationValueSelector(), 56 + hueSelector = CreateHueSelector() 57 + } 58 + } 59 + }; 60 + } 61 + 62 + /// <summary> 63 + /// Creates the control to be used for interactively selecting the hue of the target colour. 64 + /// </summary> 65 + protected abstract HueSelector CreateHueSelector(); 66 + 67 + /// <summary> 68 + /// Creates the control to be used for interactively selecting the saturation and value of the target colour. 69 + /// </summary> 70 + protected abstract SaturationValueSelector CreateSaturationValueSelector(); 71 + 72 + protected override void LoadComplete() 73 + { 74 + base.LoadComplete(); 75 + 76 + saturationValueSelector.Current.BindTo(current); 77 + hueSelector.Hue.BindTo(saturationValueSelector.Hue); 78 + } 79 + } 80 + }
+109
osu.Framework/Graphics/UserInterface/HSVColourPicker_HueSelector.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Allocation; 5 + using osu.Framework.Bindables; 6 + using osu.Framework.Graphics.Containers; 7 + using osu.Framework.Graphics.Shaders; 8 + using osu.Framework.Graphics.Shapes; 9 + using osu.Framework.Input.Events; 10 + using osuTK; 11 + 12 + namespace osu.Framework.Graphics.UserInterface 13 + { 14 + public abstract partial class HSVColourPicker 15 + { 16 + public abstract class HueSelector : CompositeDrawable 17 + { 18 + public Bindable<float> Hue { get; } = new BindableFloat 19 + { 20 + MinValue = 0, 21 + MaxValue = 1 22 + }; 23 + 24 + /// <summary> 25 + /// The body of the hue slider. 26 + /// </summary> 27 + protected readonly Container SliderBar; 28 + 29 + private readonly Drawable nub; 30 + 31 + protected HueSelector() 32 + { 33 + AutoSizeAxes = Axes.Y; 34 + RelativeSizeAxes = Axes.X; 35 + 36 + InternalChildren = new[] 37 + { 38 + SliderBar = new Container 39 + { 40 + Height = 30, 41 + RelativeSizeAxes = Axes.X, 42 + Child = new HueSelectorBackground 43 + { 44 + RelativeSizeAxes = Axes.Both 45 + } 46 + }, 47 + nub = CreateSliderNub().With(d => 48 + { 49 + d.RelativeSizeAxes = Axes.Y; 50 + d.RelativePositionAxes = Axes.X; 51 + }) 52 + }; 53 + } 54 + 55 + /// <summary> 56 + /// Creates the nub which will be used for the hue slider. 57 + /// </summary> 58 + protected abstract Drawable CreateSliderNub(); 59 + 60 + protected override void LoadComplete() 61 + { 62 + base.LoadComplete(); 63 + 64 + Hue.BindValueChanged(_ => Scheduler.AddOnce(updateNubPosition), true); 65 + } 66 + 67 + private void updateNubPosition() 68 + { 69 + nub.Position = new Vector2(Hue.Value, 0); 70 + } 71 + 72 + protected override bool OnMouseDown(MouseDownEvent e) 73 + { 74 + handleMouseInput(e.ScreenSpaceMousePosition); 75 + return true; 76 + } 77 + 78 + protected override bool OnDragStart(DragStartEvent e) 79 + { 80 + handleMouseInput(e.ScreenSpaceMousePosition); 81 + return true; 82 + } 83 + 84 + protected override void OnDrag(DragEvent e) 85 + { 86 + handleMouseInput(e.ScreenSpaceMousePosition); 87 + } 88 + 89 + private void handleMouseInput(Vector2 mousePosition) 90 + { 91 + var localSpacePosition = ToLocalSpace(mousePosition); 92 + Hue.Value = localSpacePosition.X / DrawWidth; 93 + } 94 + 95 + private class HueSelectorBackground : Box, ITexturedShaderDrawable 96 + { 97 + public new IShader TextureShader { get; private set; } 98 + public new IShader RoundedTextureShader { get; private set; } 99 + 100 + [BackgroundDependencyLoader] 101 + private void load(ShaderManager shaders) 102 + { 103 + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "HueSelectorBackground"); 104 + RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "HueSelectorBackgroundRounded"); 105 + } 106 + } 107 + } 108 + } 109 + }
+204
osu.Framework/Graphics/UserInterface/HSVColourPicker_SaturationValueSelector.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Bindables; 5 + using osu.Framework.Graphics.Colour; 6 + using osu.Framework.Graphics.Containers; 7 + using osu.Framework.Graphics.Shapes; 8 + using osu.Framework.Input.Events; 9 + using osu.Framework.Utils; 10 + using osuTK; 11 + 12 + namespace osu.Framework.Graphics.UserInterface 13 + { 14 + public abstract partial class HSVColourPicker 15 + { 16 + public abstract class SaturationValueSelector : CompositeDrawable 17 + { 18 + public readonly Bindable<Colour4> Current = new Bindable<Colour4>(); 19 + 20 + public Bindable<float> Hue { get; } = new BindableFloat 21 + { 22 + MinValue = 0, 23 + MaxValue = 1 24 + }; 25 + 26 + public Bindable<float> Saturation { get; } = new BindableFloat 27 + { 28 + MinValue = 0, 29 + MaxValue = 1 30 + }; 31 + 32 + public Bindable<float> Value { get; } = new BindableFloat 33 + { 34 + MinValue = 0, 35 + MaxValue = 1 36 + }; 37 + 38 + /// <summary> 39 + /// The gradiented box serving as the selection area. 40 + /// </summary> 41 + protected Container SelectionArea { get; } 42 + 43 + private readonly Box hueBox; 44 + private readonly Drawable marker; 45 + 46 + protected SaturationValueSelector() 47 + { 48 + RelativeSizeAxes = Axes.X; 49 + 50 + InternalChildren = new[] 51 + { 52 + SelectionArea = new Container 53 + { 54 + RelativeSizeAxes = Axes.Both, 55 + Children = new[] 56 + { 57 + hueBox = new Box 58 + { 59 + Name = "Hue", 60 + RelativeSizeAxes = Axes.Both 61 + }, 62 + new Box 63 + { 64 + Name = "Saturation", 65 + RelativeSizeAxes = Axes.Both, 66 + Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.White.Opacity(0)) 67 + }, 68 + new Box 69 + { 70 + Name = "Value", 71 + RelativeSizeAxes = Axes.Both, 72 + Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0), Colour4.Black) 73 + }, 74 + } 75 + }, 76 + marker = CreateMarker().With(d => 77 + { 78 + d.Current.BindTo(Current); 79 + 80 + d.Origin = Anchor.Centre; 81 + d.RelativePositionAxes = Axes.Both; 82 + }) 83 + }; 84 + } 85 + 86 + /// <summary> 87 + /// Creates the marker which will be used for selecting the final colour from the gamut. 88 + /// </summary> 89 + protected abstract Marker CreateMarker(); 90 + 91 + protected override void LoadComplete() 92 + { 93 + base.LoadComplete(); 94 + 95 + Current.BindValueChanged(_ => currentChanged(), true); 96 + 97 + Hue.BindValueChanged(_ => Scheduler.AddOnce(hueChanged), true); 98 + Saturation.BindValueChanged(_ => Scheduler.AddOnce(saturationChanged), true); 99 + Value.BindValueChanged(_ => Scheduler.AddOnce(valueChanged), true); 100 + } 101 + 102 + // As Current and {Hue,Saturation,Value} are mutually bound together, 103 + // using unprotected value change callbacks can end up causing partial colour updates (e.g. only the hue changing when Current is set), 104 + // or circular updates (e.g. Hue.Changed -> Current.Changed -> Hue.Changed). 105 + // To prevent this, this flag is set on every original change on each of the four bindables, 106 + // and any subsequent value change callbacks are supposed to not mutate any of those bindables further if the flag is set. 107 + private bool changeInProgress; 108 + 109 + private void currentChanged() 110 + { 111 + if (changeInProgress) 112 + return; 113 + 114 + var asHSV = Current.Value.ToHSV(); 115 + 116 + changeInProgress = true; 117 + 118 + Saturation.Value = asHSV.Y; 119 + Value.Value = asHSV.Z; 120 + 121 + if (shouldUpdateHue(asHSV.X)) 122 + Hue.Value = asHSV.X; 123 + 124 + changeInProgress = false; 125 + } 126 + 127 + private bool shouldUpdateHue(float newHue) 128 + { 129 + // there are two situations in which a hue value change is possibly unwanted. 130 + // * if saturation is near-zero, it may not be really possible to accurately measure the hue of the colour, 131 + // as hsv(x, 0, y) == hsv(z, 0, y) for any x,y,z. 132 + // * similarly, the hues of 0 and 1 are functionally equivalent, 133 + // as hsv(0, x, y) == hsv(1, x, y) for any x,y. 134 + // in those cases, just keep the hue as it was, as the colour will still be roughly the same to the point of being imperceptible, 135 + // and doing this will prevent UX idiosyncrasies (such as the hue slider jumping to 0 for no apparent reason). 136 + return Precision.DefinitelyBigger(Saturation.Value, 0) 137 + && !Precision.AlmostEquals(Hue.Value - newHue, 1); 138 + } 139 + 140 + private void hueChanged() 141 + { 142 + hueBox.Colour = Colour4.FromHSV(Hue.Value, 1, 1); 143 + updateCurrent(); 144 + } 145 + 146 + private void saturationChanged() 147 + { 148 + marker.X = Saturation.Value; 149 + updateCurrent(); 150 + } 151 + 152 + private void valueChanged() 153 + { 154 + marker.Y = 1 - Value.Value; 155 + updateCurrent(); 156 + } 157 + 158 + private void updateCurrent() 159 + { 160 + if (changeInProgress) 161 + return; 162 + 163 + changeInProgress = true; 164 + Current.Value = Colour4.FromHSV(Hue.Value, Saturation.Value, Value.Value); 165 + changeInProgress = false; 166 + } 167 + 168 + protected override void Update() 169 + { 170 + base.Update(); 171 + 172 + // manually preserve aspect ratio. 173 + // Fill{Mode,AspectRatio} do not work here, because they require RelativeSizeAxes = Both, 174 + // which in turn causes BypassAutoSizeAxes to be set to Both, and so the parent ignores the child height and assumes 0. 175 + Height = DrawWidth; 176 + } 177 + 178 + protected override bool OnMouseDown(MouseDownEvent e) 179 + { 180 + handleMouseInput(e.ScreenSpaceMousePosition); 181 + return true; 182 + } 183 + 184 + protected override bool OnDragStart(DragStartEvent e) => true; 185 + 186 + protected override void OnDrag(DragEvent e) 187 + { 188 + handleMouseInput(e.ScreenSpaceMousePosition); 189 + } 190 + 191 + private void handleMouseInput(Vector2 mousePosition) 192 + { 193 + var localSpacePosition = ToLocalSpace(mousePosition); 194 + Saturation.Value = localSpacePosition.X / DrawWidth; 195 + Value.Value = 1 - localSpacePosition.Y / DrawHeight; 196 + } 197 + 198 + protected abstract class Marker : CompositeDrawable 199 + { 200 + public IBindable<Colour4> Current { get; } = new Bindable<Colour4>(); 201 + } 202 + } 203 + } 204 + }
+145
osu.Framework/Graphics/UserInterface/HexColourPicker.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Bindables; 5 + using osu.Framework.Graphics.Containers; 6 + using osu.Framework.Graphics.Shapes; 7 + 8 + namespace osu.Framework.Graphics.UserInterface 9 + { 10 + public abstract class HexColourPicker : CompositeDrawable, IHasCurrentValue<Colour4> 11 + { 12 + private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>(); 13 + 14 + public Bindable<Colour4> Current 15 + { 16 + get => current.Current; 17 + set => current.Current = value; 18 + } 19 + 20 + public new MarginPadding Padding 21 + { 22 + get => content.Padding; 23 + set => content.Padding = value; 24 + } 25 + 26 + /// <summary> 27 + /// Sets the spacing between the hex input text box and the colour preview. 28 + /// </summary> 29 + public float Spacing 30 + { 31 + get => spacer.Width; 32 + set => spacer.Width = value; 33 + } 34 + 35 + /// <summary> 36 + /// The background of the control. 37 + /// </summary> 38 + protected readonly Box Background; 39 + 40 + private readonly Container content; 41 + 42 + private readonly TextBox hexCodeTextBox; 43 + private readonly Drawable spacer; 44 + private readonly ColourPreview colourPreview; 45 + 46 + protected HexColourPicker() 47 + { 48 + Current.Value = Colour4.White; 49 + 50 + Width = 300; 51 + AutoSizeAxes = Axes.Y; 52 + 53 + InternalChildren = new Drawable[] 54 + { 55 + Background = new Box 56 + { 57 + RelativeSizeAxes = Axes.Both 58 + }, 59 + content = new Container 60 + { 61 + RelativeSizeAxes = Axes.X, 62 + AutoSizeAxes = Axes.Y, 63 + Child = new GridContainer 64 + { 65 + RelativeSizeAxes = Axes.X, 66 + AutoSizeAxes = Axes.Y, 67 + ColumnDimensions = new[] 68 + { 69 + new Dimension(), 70 + new Dimension(GridSizeMode.AutoSize), 71 + new Dimension() 72 + }, 73 + RowDimensions = new[] 74 + { 75 + new Dimension(GridSizeMode.AutoSize) 76 + }, 77 + Content = new[] 78 + { 79 + new[] 80 + { 81 + hexCodeTextBox = CreateHexCodeTextBox().With(d => 82 + { 83 + d.RelativeSizeAxes = Axes.X; 84 + d.CommitOnFocusLost = true; 85 + }), 86 + spacer = Empty(), 87 + colourPreview = CreateColourPreview().With(d => d.RelativeSizeAxes = Axes.Both) 88 + } 89 + } 90 + } 91 + } 92 + }; 93 + } 94 + 95 + /// <summary> 96 + /// Creates the text box to be used for specifying the hex code of the target colour. 97 + /// </summary> 98 + protected abstract TextBox CreateHexCodeTextBox(); 99 + 100 + /// <summary> 101 + /// Creates the control that will be used for displaying the preview of the target colour. 102 + /// </summary> 103 + protected abstract ColourPreview CreateColourPreview(); 104 + 105 + protected override void LoadComplete() 106 + { 107 + base.LoadComplete(); 108 + 109 + Current.BindValueChanged(_ => updateState(), true); 110 + 111 + hexCodeTextBox.Current.BindValueChanged(_ => tryPreviewColour()); 112 + hexCodeTextBox.OnCommit += commitColour; 113 + } 114 + 115 + private void updateState() 116 + { 117 + hexCodeTextBox.Text = Current.Value.ToHex(); 118 + colourPreview.Current.Value = Current.Value; 119 + } 120 + 121 + private void tryPreviewColour() 122 + { 123 + if (!Colour4.TryParseHex(hexCodeTextBox.Text, out var colour) || colour.A < 1) 124 + return; 125 + 126 + colourPreview.Current.Value = colour; 127 + } 128 + 129 + private void commitColour(TextBox sender, bool newText) 130 + { 131 + if (!Colour4.TryParseHex(sender.Text, out var colour) || colour.A < 1) 132 + { 133 + Current.TriggerChange(); // restore previous value. 134 + return; 135 + } 136 + 137 + Current.Value = colour; 138 + } 139 + 140 + public abstract class ColourPreview : CompositeDrawable 141 + { 142 + public Bindable<Colour4> Current = new Bindable<Colour4>(); 143 + } 144 + } 145 + }
+1 -1
osu.Framework/Graphics/Visualisation/DrawableTransform.cs
··· 26 26 { 27 27 applied = new Box { Size = new Vector2(height) }, 28 28 appliedToEnd = new Box { X = height + 2, Size = new Vector2(height) }, 29 - text = new SpriteText { X = (height + 2) * 2, Font = new FontUsage(size: height) }, 29 + text = new SpriteText { X = (height + 2) * 2, Font = FrameworkFont.Regular.With(size: height) }, 30 30 }; 31 31 } 32 32
+2 -2
osu.Framework/Graphics/Visualisation/LogOverlay.cs
··· 181 181 Shadow = true, 182 182 ShadowColour = Color4.Black, 183 183 Margin = new MarginPadding { Left = 5, Right = 5 }, 184 - Font = new FontUsage(size: font_size), 184 + Font = FrameworkFont.Regular.With(size: font_size), 185 185 Text = entry.Target?.ToString() ?? entry.LoggerName, 186 186 } 187 187 } ··· 196 196 Child = new SpriteText 197 197 { 198 198 RelativeSizeAxes = Axes.X, 199 - Font = new FontUsage(size: font_size), 199 + Font = FrameworkFont.Regular.With(size: font_size), 200 200 Text = entry.Message 201 201 } 202 202 }
+3
osu.Framework/Graphics/Visualisation/PropertyDisplay.cs
··· 140 140 { 141 141 Text = info.Name, 142 142 Colour = FrameworkColour.Yellow, 143 + Font = FrameworkFont.Regular 143 144 }, 144 145 new SpriteText 145 146 { 146 147 Text = $@"[{type.Name}]:", 147 148 Colour = FrameworkColour.YellowGreen, 149 + Font = FrameworkFont.Regular 148 150 }, 149 151 valueText = new SpriteText 150 152 { 151 153 Colour = Color4.White, 154 + Font = FrameworkFont.Regular 152 155 }, 153 156 } 154 157 }
+4 -3
osu.Framework/Graphics/Visualisation/TextureVisualiser.cs
··· 10 10 using osu.Framework.Graphics.OpenGL.Vertices; 11 11 using osu.Framework.Graphics.Shapes; 12 12 using osu.Framework.Graphics.Sprites; 13 + using osu.Framework.Localisation; 13 14 using osu.Framework.Utils; 14 15 using osuTK; 15 16 using osuTK.Graphics; ··· 118 119 { 119 120 Anchor = Anchor.TopCentre, 120 121 Origin = Anchor.TopCentre, 121 - Font = FontUsage.Default.With(size: 16) 122 + Font = FrameworkFont.Regular.With(size: 16) 122 123 }, 123 124 new Container 124 125 { ··· 138 139 { 139 140 Anchor = Anchor.TopCentre, 140 141 Origin = Anchor.TopCentre, 141 - Font = FontUsage.Default.With(size: 16), 142 + Font = FrameworkFont.Regular.With(size: 16), 142 143 }, 143 144 } 144 145 }; ··· 264 265 protected internal override bool CanDrawOpaqueInterior => false; 265 266 } 266 267 267 - public string TooltipText 268 + public LocalisableString TooltipText 268 269 { 269 270 get 270 271 {
+1
osu.Framework/Graphics/Visualisation/TreeContainer.cs
··· 38 38 AddInternal(waitingText = new SpriteText 39 39 { 40 40 Text = @"Waiting for target selection...", 41 + Font = FrameworkFont.Regular, 41 42 Anchor = Anchor.Centre, 42 43 Origin = Anchor.Centre, 43 44 });
+2 -2
osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs
··· 145 145 Position = new Vector2(24, 0), 146 146 Children = new Drawable[] 147 147 { 148 - text = new SpriteText(), 149 - text2 = new SpriteText() 148 + text = new SpriteText { Font = FrameworkFont.Regular }, 149 + text2 = new SpriteText { Font = FrameworkFont.Regular }, 150 150 } 151 151 }, 152 152 }
+1 -1
osu.Framework/Input/PassThroughInputManager.cs
··· 28 28 /// <summary> 29 29 /// If there's an InputManager above us, decide whether we should use their available state. 30 30 /// </summary> 31 - public bool UseParentInput 31 + public virtual bool UseParentInput 32 32 { 33 33 get => useParentInput; 34 34 set
+29
osu.Framework/Localisation/EnumLocalisationMapper.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + 6 + namespace osu.Framework.Localisation 7 + { 8 + /// <summary> 9 + /// Describes the values of an <see cref="Enum"/> type by <see cref="LocalisableString"/>s. 10 + /// </summary> 11 + /// <typeparam name="T">The <see cref="Enum"/> type.</typeparam> 12 + public abstract class EnumLocalisationMapper<T> : IEnumLocalisationMapper 13 + where T : Enum 14 + { 15 + /// <summary> 16 + /// Describes a <typeparamref name="T"/> value by a <see cref="LocalisableString"/>. 17 + /// </summary> 18 + /// <param name="value">The value to map.</param> 19 + /// <returns>The <see cref="LocalisableString"/> describing <paramref name="value"/>.</returns> 20 + public abstract LocalisableString Map(T value); 21 + } 22 + 23 + /// <summary> 24 + /// Marker class for <see cref="EnumLocalisationMapper{T}"/>. Do not use. 25 + /// </summary> 26 + internal interface IEnumLocalisationMapper 27 + { 28 + } 29 + }
+34
osu.Framework/Localisation/LocalisableEnumAttribute.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using osu.Framework.Extensions; 6 + using osu.Framework.Extensions.TypeExtensions; 7 + 8 + namespace osu.Framework.Localisation 9 + { 10 + /// <summary> 11 + /// Indicates that the values of an enum have <see cref="LocalisableString"/> descriptions. 12 + /// The descriptions can be retrieved through <see cref="ExtensionMethods.GetLocalisableDescription{T}"/>. 13 + /// </summary> 14 + [AttributeUsage(AttributeTargets.Enum)] 15 + public sealed class LocalisableEnumAttribute : Attribute 16 + { 17 + /// <summary> 18 + /// The <see cref="EnumLocalisationMapper{T}"/> type that maps enum values to <see cref="LocalisableString"/>s. 19 + /// </summary> 20 + public readonly Type MapperType; 21 + 22 + /// <summary> 23 + /// Creates a new <see cref="LocalisableEnumAttribute"/>. 24 + /// </summary> 25 + /// <param name="mapperType">The <see cref="EnumLocalisationMapper{T}"/> type that maps enum values to <see cref="LocalisableString"/>s.</param> 26 + public LocalisableEnumAttribute(Type mapperType) 27 + { 28 + MapperType = mapperType; 29 + 30 + if (!typeof(IEnumLocalisationMapper).IsAssignableFrom(mapperType)) 31 + throw new ArgumentException($"Type \"{mapperType.ReadableName()}\" must inherit from {nameof(EnumLocalisationMapper<Enum>)}.", nameof(mapperType)); 32 + } 33 + } 34 + }
+4 -9
osu.Framework/Localisation/LocalisationManager.cs
··· 3 3 4 4 using System.Collections.Generic; 5 5 using System.Globalization; 6 - using JetBrains.Annotations; 7 6 using osu.Framework.Bindables; 8 7 using osu.Framework.Configuration; 9 8 ··· 37 36 /// Creates an <see cref="ILocalisedBindableString"/> which automatically updates its text according to information provided in <see cref="ILocalisedBindableString.Text"/>. 38 37 /// </summary> 39 38 /// <returns>The <see cref="ILocalisedBindableString"/>.</returns> 40 - [NotNull] 41 39 public ILocalisedBindableString GetLocalisedString(LocalisableString original) => new LocalisedBindableString(original, currentStorage, preferUnicode); 42 40 43 - private void updateLocale(ValueChangedEvent<string> args) 41 + private void updateLocale(ValueChangedEvent<string> locale) 44 42 { 45 43 if (locales.Count == 0) 46 44 return; 47 45 48 - var validLocale = locales.Find(l => l.Name == args.NewValue); 46 + var validLocale = locales.Find(l => l.Name == locale.NewValue); 49 47 50 48 if (validLocale == null) 51 49 { 52 - var culture = string.IsNullOrEmpty(args.NewValue) ? CultureInfo.CurrentCulture : new CultureInfo(args.NewValue); 50 + var culture = string.IsNullOrEmpty(locale.NewValue) ? CultureInfo.CurrentCulture : new CultureInfo(locale.NewValue); 53 51 54 52 for (var c = culture; !EqualityComparer<CultureInfo>.Default.Equals(c, CultureInfo.InvariantCulture); c = c.Parent) 55 53 { ··· 61 59 validLocale ??= locales[0]; 62 60 } 63 61 64 - if (validLocale.Name != args.NewValue) 65 - configLocale.Value = validLocale.Name; 66 - else 67 - currentStorage.Value = validLocale.Storage; 62 + currentStorage.Value = validLocale.Storage; 68 63 } 69 64 70 65 private class LocaleMapping
+7 -7
osu.Framework/Platform/GameHost.cs
··· 180 180 thread.UnhandledException = null; 181 181 } 182 182 183 - public DrawThread DrawThread; 184 - public GameThread UpdateThread; 185 - public InputThread InputThread; 186 - public AudioThread AudioThread; 183 + public DrawThread DrawThread { get; private set; } 184 + public GameThread UpdateThread { get; private set; } 185 + public InputThread InputThread { get; private set; } 186 + public AudioThread AudioThread { get; private set; } 187 187 188 188 private double maximumUpdateHz; 189 189 ··· 581 581 582 582 Window = CreateWindow(); 583 583 584 - ExecutionState = ExecutionState.Running; 585 - 586 584 populateInputHandlers(); 587 585 588 586 SetupConfig(game.GetFrameworkConfigDefaults() ?? new Dictionary<FrameworkSetting, object>()); 587 + 588 + ExecutionState = ExecutionState.Running; 589 589 590 590 initialiseInputHandlers(); 591 591 ··· 667 667 /// </summary> 668 668 public void Suspend() 669 669 { 670 - threadRunner.Suspend(); 671 670 suspended = true; 671 + threadRunner.Suspend(); 672 672 } 673 673 674 674 /// <summary>
+2 -6
osu.Framework/Platform/SDL2/SDL2Extensions.cs
··· 446 446 447 447 public static WindowState ToWindowState(this SDL.SDL_WindowFlags windowFlags) 448 448 { 449 - // NOTE: on macOS, SDL2 does not differentiate between "maximised" and "fullscreen desktop" 450 449 if (windowFlags.HasFlagFast(SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP) || 451 - windowFlags.HasFlagFast(SDL.SDL_WindowFlags.SDL_WINDOW_BORDERLESS) || 452 - windowFlags.HasFlagFast(SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED) && RuntimeInfo.OS == RuntimeInfo.Platform.macOS) 450 + windowFlags.HasFlagFast(SDL.SDL_WindowFlags.SDL_WINDOW_BORDERLESS)) 453 451 return WindowState.FullscreenBorderless; 454 452 455 453 if (windowFlags.HasFlagFast(SDL.SDL_WindowFlags.SDL_WINDOW_MINIMIZED)) ··· 475 473 return SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; 476 474 477 475 case WindowState.Maximised: 478 - return RuntimeInfo.OS == RuntimeInfo.Platform.macOS 479 - ? SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP 480 - : SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; 476 + return SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; 481 477 482 478 case WindowState.Minimised: 483 479 return SDL.SDL_WindowFlags.SDL_WINDOW_MINIMIZED;
+6 -1
osu.Framework/Platform/SDL2/SDL2GraphicsBackend.cs
··· 30 30 return context; 31 31 } 32 32 33 - protected override void MakeCurrent(IntPtr context) => SDL.SDL_GL_MakeCurrent(sdlWindowHandle, context); 33 + protected override void MakeCurrent(IntPtr context) 34 + { 35 + int result = SDL.SDL_GL_MakeCurrent(sdlWindowHandle, context); 36 + if (result < 0) 37 + throw new InvalidOperationException($"Failed to acquire GL context ({SDL.SDL_GetError()})"); 38 + } 34 39 35 40 public override void SwapBuffers() => SDL.SDL_GL_SwapWindow(sdlWindowHandle); 36 41
+9 -8
osu.Framework/Platform/SDL2DesktopWindow.cs
··· 434 434 435 435 if (e.type == SDL.SDL_EventType.SDL_WINDOWEVENT && e.window.windowEvent == SDL.SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED) 436 436 { 437 - // This function will be invoked before the SDL internal states are all changed. (as documented here: https://wiki.libsdl.org/SDL_SetEventFilter) 438 - // Therefore we should only update the client size without saving to config, as we don't know what state the window would end up in. 439 437 updateWindowSize(); 440 - return 0; 441 438 } 442 439 443 440 return 1; ··· 478 475 private void updateWindowSize() 479 476 { 480 477 SDL.SDL_GL_GetDrawableSize(SDLWindowHandle, out var w, out var h); 481 - 482 478 SDL.SDL_GetWindowSize(SDLWindowHandle, out var actualW, out var _); 479 + 483 480 Scale = (float)w / actualW; 481 + Size = new Size(w, h); 484 482 485 - Size = new Size(w, h); 483 + // This function may be invoked before the SDL internal states are all changed. (as documented here: https://wiki.libsdl.org/SDL_SetEventFilter) 484 + // Scheduling the store to config until after the event poll has run will ensure the window is in the correct state. 485 + eventScheduler.Add(storeWindowSizeToConfig, true); 486 486 } 487 487 488 488 /// <summary> ··· 914 914 915 915 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED: 916 916 updateWindowSize(); 917 - if (WindowState == WindowState.Normal) 918 - storeWindowSizeToConfig(); 919 - 920 917 break; 921 918 922 919 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_ENTER: ··· 1024 1021 break; 1025 1022 1026 1023 case WindowState.Maximised: 1024 + SDL.SDL_RestoreWindow(SDLWindowHandle); 1027 1025 SDL.SDL_MaximizeWindow(SDLWindowHandle); 1028 1026 1029 1027 SDL.SDL_GL_GetDrawableSize(SDLWindowHandle, out int w, out int h); ··· 1085 1083 1086 1084 private void storeWindowSizeToConfig() 1087 1085 { 1086 + if (WindowState != WindowState.Normal) 1087 + return; 1088 + 1088 1089 storingSizeToConfig = true; 1089 1090 sizeWindowed.Value = (Size / Scale).ToSize(); 1090 1091 storingSizeToConfig = false;
+18 -18
osu.Framework/Platform/ThreadRunner.cs
··· 57 57 } 58 58 } 59 59 60 + private readonly object startStopLock = new object(); 61 + 60 62 /// <summary> 61 63 /// Construct a new ThreadRunner instance. 62 64 /// </summary> ··· 107 109 lock (threads) 108 110 { 109 111 foreach (var t in threads) 110 - t.ProcessFrame(); 112 + t.RunSingleFrame(); 111 113 } 112 114 113 115 break; ··· 115 117 116 118 case ExecutionMode.MultiThreaded: 117 119 // still need to run the main/input thread on the window loop 118 - mainThread.ProcessFrame(); 120 + mainThread.RunSingleFrame(); 119 121 break; 120 122 } 121 123 } ··· 124 126 125 127 public void Suspend() 126 128 { 127 - pauseAllThreads(); 128 - 129 - // set the active execution mode back to null to set the state checking back to when it can be resumed. 130 - activeExecutionMode = null; 129 + lock (startStopLock) 130 + { 131 + pauseAllThreads(); 132 + activeExecutionMode = null; 133 + } 131 134 } 132 135 133 136 public void Stop() ··· 150 153 }); 151 154 152 155 // as the input thread isn't actually handled by a thread, the above join does not necessarily mean it has been completed to an exiting state. 153 - while (!mainThread.Exited) 154 - mainThread.ProcessFrame(); 156 + mainThread.WaitForState(GameThreadState.Exited); 155 157 156 158 ThreadSafety.ResetAllForCurrentThread(); 157 159 } 158 160 159 161 private void ensureCorrectExecutionMode() 160 162 { 161 - if (ExecutionMode == activeExecutionMode) 162 - return; 163 + // locking is required as this method may be called from two different threads. 164 + lock (startStopLock) 165 + { 166 + if (ExecutionMode == activeExecutionMode) 167 + return; 163 168 164 - // if null, we have not yet got an execution mode, so set this early to allow usage in GameThread.Initialize overrides. 165 - activeExecutionMode ??= ThreadSafety.ExecutionMode = ExecutionMode; 166 - Logger.Log($"Execution mode changed to {activeExecutionMode}"); 169 + activeExecutionMode = ThreadSafety.ExecutionMode = ExecutionMode; 170 + Logger.Log($"Execution mode changed to {activeExecutionMode}"); 171 + } 167 172 168 173 pauseAllThreads(); 169 174 ··· 173 178 { 174 179 // switch to multi-threaded 175 180 foreach (var t in Threads) 176 - { 177 181 t.Start(); 178 - t.Clock.Throttling = true; 179 - } 180 182 181 183 break; 182 184 } ··· 196 198 break; 197 199 } 198 200 } 199 - 200 - activeExecutionMode = ThreadSafety.ExecutionMode = ExecutionMode; 201 201 202 202 updateMainThreadRates(); 203 203 }
-202
osu.Framework/Resources/Fonts/OpenSans/LICENSE.txt
··· 1 - 2 - Apache License 3 - Version 2.0, January 2004 4 - http://www.apache.org/licenses/ 5 - 6 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 - 8 - 1. Definitions. 9 - 10 - "License" shall mean the terms and conditions for use, reproduction, 11 - and distribution as defined by Sections 1 through 9 of this document. 12 - 13 - "Licensor" shall mean the copyright owner or entity authorized by 14 - the copyright owner that is granting the License. 15 - 16 - "Legal Entity" shall mean the union of the acting entity and all 17 - other entities that control, are controlled by, or are under common 18 - control with that entity. For the purposes of this definition, 19 - "control" means (i) the power, direct or indirect, to cause the 20 - direction or management of such entity, whether by contract or 21 - otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 - outstanding shares, or (iii) beneficial ownership of such entity. 23 - 24 - "You" (or "Your") shall mean an individual or Legal Entity 25 - exercising permissions granted by this License. 26 - 27 - "Source" form shall mean the preferred form for making modifications, 28 - including but not limited to software source code, documentation 29 - source, and configuration files. 30 - 31 - "Object" form shall mean any form resulting from mechanical 32 - transformation or translation of a Source form, including but 33 - not limited to compiled object code, generated documentation, 34 - and conversions to other media types. 35 - 36 - "Work" shall mean the work of authorship, whether in Source or 37 - Object form, made available under the License, as indicated by a 38 - copyright notice that is included in or attached to the work 39 - (an example is provided in the Appendix below). 40 - 41 - "Derivative Works" shall mean any work, whether in Source or Object 42 - form, that is based on (or derived from) the Work and for which the 43 - editorial revisions, annotations, elaborations, or other modifications 44 - represent, as a whole, an original work of authorship. For the purposes 45 - of this License, Derivative Works shall not include works that remain 46 - separable from, or merely link (or bind by name) to the interfaces of, 47 - the Work and Derivative Works thereof. 48 - 49 - "Contribution" shall mean any work of authorship, including 50 - the original version of the Work and any modifications or additions 51 - to that Work or Derivative Works thereof, that is intentionally 52 - submitted to Licensor for inclusion in the Work by the copyright owner 53 - or by an individual or Legal Entity authorized to submit on behalf of 54 - the copyright owner. For the purposes of this definition, "submitted" 55 - means any form of electronic, verbal, or written communication sent 56 - to the Licensor or its representatives, including but not limited to 57 - communication on electronic mailing lists, source code control systems, 58 - and issue tracking systems that are managed by, or on behalf of, the 59 - Licensor for the purpose of discussing and improving the Work, but 60 - excluding communication that is conspicuously marked or otherwise 61 - designated in writing by the copyright owner as "Not a Contribution." 62 - 63 - "Contributor" shall mean Licensor and any individual or Legal Entity 64 - on behalf of whom a Contribution has been received by Licensor and 65 - subsequently incorporated within the Work. 66 - 67 - 2. Grant of Copyright License. Subject to the terms and conditions of 68 - this License, each Contributor hereby grants to You a perpetual, 69 - worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 - copyright license to reproduce, prepare Derivative Works of, 71 - publicly display, publicly perform, sublicense, and distribute the 72 - Work and such Derivative Works in Source or Object form. 73 - 74 - 3. Grant of Patent License. Subject to the terms and conditions of 75 - this License, each Contributor hereby grants to You a perpetual, 76 - worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 - (except as stated in this section) patent license to make, have made, 78 - use, offer to sell, sell, import, and otherwise transfer the Work, 79 - where such license applies only to those patent claims licensable 80 - by such Contributor that are necessarily infringed by their 81 - Contribution(s) alone or by combination of their Contribution(s) 82 - with the Work to which such Contribution(s) was submitted. If You 83 - institute patent litigation against any entity (including a 84 - cross-claim or counterclaim in a lawsuit) alleging that the Work 85 - or a Contribution incorporated within the Work constitutes direct 86 - or contributory patent infringement, then any patent licenses 87 - granted to You under this License for that Work shall terminate 88 - as of the date such litigation is filed. 89 - 90 - 4. Redistribution. You may reproduce and distribute copies of the 91 - Work or Derivative Works thereof in any medium, with or without 92 - modifications, and in Source or Object form, provided that You 93 - meet the following conditions: 94 - 95 - (a) You must give any other recipients of the Work or 96 - Derivative Works a copy of this License; and 97 - 98 - (b) You must cause any modified files to carry prominent notices 99 - stating that You changed the files; and 100 - 101 - (c) You must retain, in the Source form of any Derivative Works 102 - that You distribute, all copyright, patent, trademark, and 103 - attribution notices from the Source form of the Work, 104 - excluding those notices that do not pertain to any part of 105 - the Derivative Works; and 106 - 107 - (d) If the Work includes a "NOTICE" text file as part of its 108 - distribution, then any Derivative Works that You distribute must 109 - include a readable copy of the attribution notices contained 110 - within such NOTICE file, excluding those notices that do not 111 - pertain to any part of the Derivative Works, in at least one 112 - of the following places: within a NOTICE text file distributed 113 - as part of the Derivative Works; within the Source form or 114 - documentation, if provided along with the Derivative Works; or, 115 - within a display generated by the Derivative Works, if and 116 - wherever such third-party notices normally appear. The contents 117 - of the NOTICE file are for informational purposes only and 118 - do not modify the License. You may add Your own attribution 119 - notices within Derivative Works that You distribute, alongside 120 - or as an addendum to the NOTICE text from the Work, provided 121 - that such additional attribution notices cannot be construed 122 - as modifying the License. 123 - 124 - You may add Your own copyright statement to Your modifications and 125 - may provide additional or different license terms and conditions 126 - for use, reproduction, or distribution of Your modifications, or 127 - for any such Derivative Works as a whole, provided Your use, 128 - reproduction, and distribution of the Work otherwise complies with 129 - the conditions stated in this License. 130 - 131 - 5. Submission of Contributions. Unless You explicitly state otherwise, 132 - any Contribution intentionally submitted for inclusion in the Work 133 - by You to the Licensor shall be under the terms and conditions of 134 - this License, without any additional terms or conditions. 135 - Notwithstanding the above, nothing herein shall supersede or modify 136 - the terms of any separate license agreement you may have executed 137 - with Licensor regarding such Contributions. 138 - 139 - 6. Trademarks. This License does not grant permission to use the trade 140 - names, trademarks, service marks, or product names of the Licensor, 141 - except as required for reasonable and customary use in describing the 142 - origin of the Work and reproducing the content of the NOTICE file. 143 - 144 - 7. Disclaimer of Warranty. Unless required by applicable law or 145 - agreed to in writing, Licensor provides the Work (and each 146 - Contributor provides its Contributions) on an "AS IS" BASIS, 147 - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 - implied, including, without limitation, any warranties or conditions 149 - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 - PARTICULAR PURPOSE. You are solely responsible for determining the 151 - appropriateness of using or redistributing the Work and assume any 152 - risks associated with Your exercise of permissions under this License. 153 - 154 - 8. Limitation of Liability. In no event and under no legal theory, 155 - whether in tort (including negligence), contract, or otherwise, 156 - unless required by applicable law (such as deliberate and grossly 157 - negligent acts) or agreed to in writing, shall any Contributor be 158 - liable to You for damages, including any direct, indirect, special, 159 - incidental, or consequential damages of any character arising as a 160 - result of this License or out of the use or inability to use the 161 - Work (including but not limited to damages for loss of goodwill, 162 - work stoppage, computer failure or malfunction, or any and all 163 - other commercial damages or losses), even if such Contributor 164 - has been advised of the possibility of such damages. 165 - 166 - 9. Accepting Warranty or Additional Liability. While redistributing 167 - the Work or Derivative Works thereof, You may choose to offer, 168 - and charge a fee for, acceptance of support, warranty, indemnity, 169 - or other liability obligations and/or rights consistent with this 170 - License. However, in accepting such obligations, You may act only 171 - on Your own behalf and on Your sole responsibility, not on behalf 172 - of any other Contributor, and only if You agree to indemnify, 173 - defend, and hold each Contributor harmless for any liability 174 - incurred by, or claims asserted against, such Contributor by reason 175 - of your accepting any such warranty or additional liability. 176 - 177 - END OF TERMS AND CONDITIONS 178 - 179 - APPENDIX: How to apply the Apache License to your work. 180 - 181 - To apply the Apache License to your work, attach the following 182 - boilerplate notice, with the fields enclosed by brackets "[]" 183 - replaced with your own identifying information. (Don't include 184 - the brackets!) The text should be enclosed in the appropriate 185 - comment syntax for the file format. We also recommend that a 186 - file or class name and description of purpose be included on the 187 - same "printed page" as the copyright notice for easier 188 - identification within third-party archives. 189 - 190 - Copyright [yyyy] [name of copyright owner] 191 - 192 - Licensed under the Apache License, Version 2.0 (the "License"); 193 - you may not use this file except in compliance with the License. 194 - You may obtain a copy of the License at 195 - 196 - http://www.apache.org/licenses/LICENSE-2.0 197 - 198 - Unless required by applicable law or agreed to in writing, software 199 - distributed under the License is distributed on an "AS IS" BASIS, 200 - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 - See the License for the specific language governing permissions and 202 - limitations under the License.
osu.Framework/Resources/Fonts/OpenSans/OpenSans-Bold.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-BoldItalic.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-BoldItalic_0.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-Bold_0.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-Regular.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-RegularItalic.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-RegularItalic_0.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/OpenSans/OpenSans-Regular_0.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/Roboto/Roboto-BoldItalic.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/Roboto/Roboto-BoldItalic_0.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/Roboto/Roboto-BoldItalic_1.png

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/Roboto/Roboto-RegularItalic.bin

This is a binary file and will not be displayed.

osu.Framework/Resources/Fonts/Roboto/Roboto-RegularItalic_0.png

This is a binary file and will not be displayed.

+10
osu.Framework/Resources/Shaders/sh_HueSelectorBackground.fs
··· 1 + #include "sh_Utils.h" 2 + 3 + varying mediump vec2 v_TexCoord; 4 + varying mediump vec4 v_TexRect; 5 + 6 + void main(void) 7 + { 8 + float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]); 9 + gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1)); 10 + }
+10
osu.Framework/Resources/Shaders/sh_HueSelectorBackgroundRounded.fs
··· 1 + #include "sh_Utils.h" 2 + #include "sh_Masking.h" 3 + 4 + varying mediump vec2 v_TexCoord; 5 + 6 + void main(void) 7 + { 8 + float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]); 9 + gl_FragColor = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord); 10 + }
+9
osu.Framework/Resources/Shaders/sh_Utils.h
··· 42 42 finalAlpha 43 43 ); 44 44 } 45 + 46 + // http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl 47 + // slightly amended to also handle alpha 48 + vec4 hsv2rgb(vec4 c) 49 + { 50 + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 51 + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 52 + return vec4(c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y), c.w); 53 + }
+12 -8
osu.Framework/Testing/DynamicClassCompiler.cs
··· 45 45 46 46 public void Start() 47 47 { 48 - var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); 49 - 50 48 if (Debugger.IsAttached) 51 49 { 52 50 referenceBuilder = new EmptyTypeReferenceBuilder(); ··· 55 53 return; 56 54 } 57 55 56 + var di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); 57 + var basePath = getSolutionPath(di); 58 + 59 + if (!Directory.Exists(basePath)) 60 + { 61 + referenceBuilder = new EmptyTypeReferenceBuilder(); 62 + 63 + Logger.Log("Dynamic compilation disabled (no solution file found)."); 64 + return; 65 + } 66 + 58 67 #if NET5_0 59 68 referenceBuilder = new RoslynTypeReferenceBuilder(); 60 69 #else ··· 65 74 { 66 75 Logger.Log("Initialising dynamic compilation..."); 67 76 68 - var basePath = getSolutionPath(di); 69 - 70 - if (!Directory.Exists(basePath)) 71 - return; 72 - 73 - await referenceBuilder.Initialise(Directory.GetFiles(getSolutionPath(di), "*.sln").First()).ConfigureAwait(false); 77 + await referenceBuilder.Initialise(Directory.GetFiles(basePath, "*.sln").First()).ConfigureAwait(false); 74 78 75 79 foreach (var dir in Directory.GetDirectories(basePath)) 76 80 {
+29 -2
osu.Framework/Testing/Input/ManualInputManager.cs
··· 4 4 using osu.Framework.Graphics; 5 5 using osu.Framework.Graphics.Containers; 6 6 using osu.Framework.Input; 7 + using osu.Framework.Input.Events; 7 8 using osu.Framework.Input.Handlers; 8 9 using osu.Framework.Input.StateChanges; 9 10 using osu.Framework.Platform; ··· 41 42 42 43 private readonly TestCursorContainer testCursor; 43 44 45 + private readonly LocalPlatformActionContainer platformActionContainer; 46 + 47 + public override bool UseParentInput 48 + { 49 + get => base.UseParentInput; 50 + set 51 + { 52 + base.UseParentInput = value; 53 + platformActionContainer.ShouldHandle = !value; 54 + } 55 + } 56 + 44 57 public ManualInputManager() 45 58 { 46 - UseParentInput = true; 47 59 AddHandler(handler = new ManualInputHandler()); 48 60 49 61 InternalChildren = new Drawable[] 50 62 { 51 - content = new Container { RelativeSizeAxes = Axes.Both }, 63 + platformActionContainer = new LocalPlatformActionContainer().WithChild(content = new Container { RelativeSizeAxes = Axes.Both }), 52 64 testCursor = new TestCursorContainer(), 53 65 }; 66 + 67 + UseParentInput = true; 54 68 } 55 69 56 70 public void Input(IInput input) ··· 132 146 133 147 public void PressTabletAuxiliaryButton(TabletAuxiliaryButton auxiliaryButton) => Input(new TabletAuxiliaryButtonInput(auxiliaryButton, true)); 134 148 public void ReleaseTabletAuxiliaryButton(TabletAuxiliaryButton auxiliaryButton) => Input(new TabletAuxiliaryButtonInput(auxiliaryButton, false)); 149 + 150 + private class LocalPlatformActionContainer : PlatformActionContainer 151 + { 152 + public bool ShouldHandle; 153 + 154 + protected override bool Handle(UIEvent e) 155 + { 156 + if (!ShouldHandle) 157 + return false; 158 + 159 + return base.Handle(e); 160 + } 161 + } 135 162 136 163 private class ManualInputHandler : InputHandler 137 164 {
+1 -1
osu.Framework/Testing/ManualInputManagerTestScene.cs
··· 45 45 InputManager = new ManualInputManager 46 46 { 47 47 UseParentInput = true, 48 - Child = new PlatformActionContainer().WithChild(Content) 48 + Child = Content, 49 49 }, 50 50 new Container 51 51 {
+1 -11
osu.Framework/Testing/TestBrowser.cs
··· 129 129 private readonly BindableDouble audioRateAdjust = new BindableDouble(1); 130 130 131 131 [BackgroundDependencyLoader] 132 - private void load(Storage storage, GameHost host, FrameworkConfigManager frameworkConfig, FontStore fonts, Game game, AudioManager audio) 132 + private void load(Storage storage, GameHost host, FrameworkConfigManager frameworkConfig, FontStore fonts, AudioManager audio) 133 133 { 134 134 interactive = host.Window != null; 135 135 config = new TestBrowserConfig(storage); ··· 137 137 exit = host.Exit; 138 138 139 139 audio.AddAdjustment(AdjustableProperty.Frequency, audioRateAdjust); 140 - 141 - var resources = game.Resources; 142 - 143 - //Roboto 144 - game.AddFont(resources, @"Fonts/Roboto/Roboto-Regular"); 145 - game.AddFont(resources, @"Fonts/Roboto/Roboto-Bold"); 146 - 147 - //RobotoCondensed 148 - game.AddFont(resources, @"Fonts/RobotoCondensed/RobotoCondensed-Regular"); 149 - game.AddFont(resources, @"Fonts/RobotoCondensed/RobotoCondensed-Bold"); 150 140 151 141 showLogOverlay = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay); 152 142
+3 -3
osu.Framework/Threading/AudioThread.cs
··· 18 18 public AudioThread() 19 19 : base(name: "Audio") 20 20 { 21 - OnNewFrame = onNewFrame; 21 + OnNewFrame += onNewFrame; 22 22 23 23 if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux) 24 24 { ··· 89 89 initialised_devices.Add(deviceId); 90 90 } 91 91 92 - protected override void PerformExit() 92 + protected override void OnExit() 93 93 { 94 - base.PerformExit(); 94 + base.OnExit(); 95 95 96 96 lock (managers) 97 97 {
+2 -3
osu.Framework/Threading/DrawThread.cs
··· 46 46 host.Window?.MakeCurrent(); 47 47 } 48 48 49 - protected sealed override void Cleanup() 49 + protected sealed override void OnSuspended() 50 50 { 51 - base.Cleanup(); 52 - 51 + base.OnSuspended(); 53 52 host.Window?.ClearCurrent(); 54 53 } 55 54
+334 -114
osu.Framework/Threading/GameThread.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using System; 5 - using System.Threading; 6 - using osu.Framework.Statistics; 7 - using osu.Framework.Timing; 8 5 using System.Collections.Generic; 9 6 using System.Diagnostics; 10 7 using System.Globalization; 8 + using System.Threading; 11 9 using JetBrains.Annotations; 12 10 using osu.Framework.Bindables; 13 11 using osu.Framework.Development; 12 + using osu.Framework.Platform; 13 + using osu.Framework.Statistics; 14 + using osu.Framework.Timing; 14 15 15 16 namespace osu.Framework.Threading 16 17 { 18 + /// <summary> 19 + /// A conceptual thread used for running game work. May or may not be backed by a native thread. 20 + /// </summary> 17 21 public class GameThread 18 22 { 19 23 internal const double DEFAULT_ACTIVE_HZ = 1000; 20 24 internal const double DEFAULT_INACTIVE_HZ = 60; 21 25 22 - internal PerformanceMonitor Monitor { get; } 26 + /// <summary> 27 + /// The name of this thread. 28 + /// </summary> 29 + public readonly string Name; 30 + 31 + /// <summary> 32 + /// Whether the game is active (in the foreground). 33 + /// </summary> 34 + public readonly IBindable<bool> IsActive = new Bindable<bool>(true); 35 + 36 + /// <summary> 37 + /// The current state of this thread. 38 + /// </summary> 39 + public IBindable<GameThreadState> State => state; 40 + 41 + private readonly Bindable<GameThreadState> state = new Bindable<GameThreadState>(); 42 + 43 + /// <summary> 44 + /// Whether this thread is currently running. 45 + /// </summary> 46 + public bool Running => state.Value == GameThreadState.Running; 47 + 48 + /// <summary> 49 + /// Whether this thread is exited. 50 + /// </summary> 51 + public bool Exited => state.Value == GameThreadState.Exited; 52 + 53 + /// <summary> 54 + /// Whether currently executing on this thread (from the point of invocation). 55 + /// </summary> 56 + public virtual bool IsCurrent => true; 57 + 58 + /// <summary> 59 + /// The thread's clock. Responsible for timekeeping and throttling. 60 + /// </summary> 23 61 public ThrottledFrameClock Clock { get; } 24 62 63 + /// <summary> 64 + /// The current dedicated OS thread for this <see cref="GameThread"/>. 65 + /// A value of <see langword="null"/> does not necessarily mean that this thread is not running; 66 + /// in <see cref="ExecutionMode.SingleThread"/> execution mode <see cref="ThreadRunner"/> drives its <see cref="GameThread"/>s 67 + /// manually and sequentially on the main OS thread of the game process. 68 + /// </summary> 25 69 [CanBeNull] 26 70 public Thread Thread { get; private set; } 27 71 72 + /// <summary> 73 + /// The thread's scheduler. 74 + /// </summary> 28 75 public Scheduler Scheduler { get; } 29 76 30 77 /// <summary> ··· 33 80 /// </summary> 34 81 public EventHandler<UnhandledExceptionEventArgs> UnhandledException; 35 82 36 - protected Action OnNewFrame; 37 - 38 83 /// <summary> 39 - /// Whether the game is active (in the foreground). 84 + /// The culture of this thread. 40 85 /// </summary> 41 - public readonly IBindable<bool> IsActive = new Bindable<bool>(true); 86 + public CultureInfo CurrentCulture 87 + { 88 + get => culture; 89 + set 90 + { 91 + culture = value; 42 92 43 - private double activeHz = DEFAULT_ACTIVE_HZ; 93 + updateCulture(); 94 + } 95 + } 44 96 97 + private CultureInfo culture; 98 + 99 + /// <summary> 100 + /// The refresh rate this thread should run at when active. Only applies when the underlying clock is throttling. 101 + /// </summary> 45 102 public double ActiveHz 46 103 { 47 104 get => activeHz; ··· 52 109 } 53 110 } 54 111 55 - private double inactiveHz = DEFAULT_INACTIVE_HZ; 112 + private double activeHz = DEFAULT_ACTIVE_HZ; 56 113 114 + /// <summary> 115 + /// The refresh rate this thread should run at when inactive. Only applies when the underlying clock is throttling. 116 + /// </summary> 57 117 public double InactiveHz 58 118 { 59 119 get => inactiveHz; ··· 64 124 } 65 125 } 66 126 67 - public static string PrefixedThreadNameFor(string name) => $"{nameof(GameThread)}.{name}"; 68 - 69 - public bool Running => Thread?.IsAlive == true; 70 - 71 - public virtual bool IsCurrent => true; 72 - 73 - private readonly ManualResetEvent initializedEvent = new ManualResetEvent(false); 74 - 75 - internal void Initialize(bool withThrottling) 76 - { 77 - MakeCurrent(); 127 + private double inactiveHz = DEFAULT_INACTIVE_HZ; 78 128 79 - OnInitialize(); 129 + internal PerformanceMonitor Monitor { get; } 80 130 81 - Clock.Throttling = withThrottling; 82 - 83 - Monitor.MakeCurrent(); 131 + internal virtual IEnumerable<StatisticsCounterType> StatisticsCounters => Array.Empty<StatisticsCounterType>(); 84 132 85 - updateCulture(); 133 + /// <summary> 134 + /// The main work which is fired on each frame. 135 + /// </summary> 136 + protected event Action OnNewFrame; 86 137 87 - initializedEvent.Set(); 88 - } 138 + private readonly ManualResetEvent initializedEvent = new ManualResetEvent(false); 89 139 90 - protected virtual void OnInitialize() { } 140 + private readonly object startStopLock = new object(); 91 141 92 - internal virtual IEnumerable<StatisticsCounterType> StatisticsCounters => Array.Empty<StatisticsCounterType>(); 142 + /// <summary> 143 + /// Whether a pause has been requested. 144 + /// </summary> 145 + private volatile bool pauseRequested; 93 146 94 - public readonly string Name; 147 + /// <summary> 148 + /// Whether an exit has been requested. 149 + /// </summary> 150 + private volatile bool exitRequested; 95 151 96 152 internal GameThread(Action onNewFrame = null, string name = "unknown", bool monitorPerformance = true) 97 153 { ··· 106 162 IsActive.BindValueChanged(_ => updateMaximumHz(), true); 107 163 } 108 164 109 - private void createThread() 110 - { 111 - Debug.Assert(Thread == null); 112 - 113 - Thread = new Thread(runWork) 114 - { 115 - Name = PrefixedThreadNameFor(Name), 116 - IsBackground = true, 117 - }; 118 - 119 - updateCulture(); 120 - } 121 - 165 + /// <summary> 166 + /// Block until this thread has entered an initialised state. 167 + /// </summary> 122 168 public void WaitUntilInitialized() 123 169 { 124 170 initializedEvent.WaitOne(); 125 171 } 126 172 127 - private void updateMaximumHz() => Scheduler.Add(() => Clock.MaximumUpdateHz = IsActive.Value ? activeHz : inactiveHz); 173 + /// <summary> 174 + /// Returns a string representation that is prefixed with this thread's identifier. 175 + /// </summary> 176 + /// <param name="name">The content to prefix.</param> 177 + /// <returns>A prefixed string.</returns> 178 + public static string PrefixedThreadNameFor(string name) => $"{nameof(GameThread)}.{name}"; 128 179 129 - private void runWork() 180 + /// <summary> 181 + /// Start this thread. 182 + /// </summary> 183 + /// <remarks> 184 + /// This method blocks until in a running state. 185 + /// </remarks> 186 + public void Start() 130 187 { 131 - try 188 + lock (startStopLock) 132 189 { 133 - Initialize(true); 190 + switch (state.Value) 191 + { 192 + case GameThreadState.Paused: 193 + case GameThreadState.NotStarted: 194 + break; 134 195 135 - while (!exitCompleted && !paused) 196 + default: 197 + throw new InvalidOperationException($"Cannot start when thread is {state.Value}."); 198 + } 199 + 200 + state.Value = GameThreadState.Starting; 201 + PrepareForWork(); 202 + } 203 + 204 + WaitForState(GameThreadState.Running); 205 + Debug.Assert(state.Value == GameThreadState.Running); 206 + } 207 + 208 + /// <summary> 209 + /// Request that this thread is exited. 210 + /// </summary> 211 + /// <remarks> 212 + /// This does not block and will only queue an exit request, which is processed in the main frame loop. 213 + /// </remarks> 214 + /// <exception cref="InvalidOperationException">Thrown when attempting to exit from an invalid state.</exception> 215 + public void Exit() 216 + { 217 + lock (startStopLock) 218 + { 219 + switch (state.Value) 136 220 { 137 - ProcessFrame(); 221 + // technically we could support this, but we don't use this yet and it will add more complexity. 222 + case GameThreadState.Paused: 223 + case GameThreadState.NotStarted: 224 + case GameThreadState.Starting: 225 + throw new InvalidOperationException($"Cannot exit when thread is {state.Value}."); 226 + 227 + case GameThreadState.Exited: 228 + return; 229 + 230 + default: 231 + // actual exit will be done in ProcessFrame. 232 + exitRequested = true; 233 + break; 138 234 } 139 235 } 140 - finally 236 + } 237 + 238 + /// <summary> 239 + /// Prepare this thread for performing work. Must be called when entering a running state. 240 + /// </summary> 241 + /// <param name="withThrottling">Whether this thread's clock should be throttling via thread sleeps.</param> 242 + internal void Initialize(bool withThrottling) 243 + { 244 + lock (startStopLock) 141 245 { 142 - Cleanup(); 246 + Debug.Assert(state.Value != GameThreadState.Running); 247 + Debug.Assert(state.Value != GameThreadState.Exited); 248 + 249 + MakeCurrent(); 250 + 251 + OnInitialize(); 252 + 253 + Clock.Throttling = withThrottling; 254 + 255 + Monitor.MakeCurrent(); 256 + 257 + updateCulture(); 258 + 259 + initializedEvent.Set(); 260 + state.Value = GameThreadState.Running; 143 261 } 144 262 } 145 263 146 264 /// <summary> 147 - /// Run when thread transitions into an active/processing state. 265 + /// Run when thread transitions into an active/processing state, at the beginning of each frame. 148 266 /// </summary> 149 267 internal virtual void MakeCurrent() 150 268 { 151 269 ThreadSafety.ResetAllForCurrentThread(); 152 270 } 153 271 154 - internal void ProcessFrame() 272 + /// <summary> 273 + /// Runs a single frame, updating the execution state if required. 274 + /// </summary> 275 + internal void RunSingleFrame() 155 276 { 156 - try 277 + var newState = processFrame(); 278 + 279 + if (newState.HasValue) 280 + setExitState(newState.Value); 281 + } 282 + 283 + /// <summary> 284 + /// Pause this thread. Must be run from <see cref="ThreadRunner"/> in a safe manner. 285 + /// </summary> 286 + /// <remarks> 287 + /// This method blocks until in a paused state. 288 + /// </remarks> 289 + internal void Pause() 290 + { 291 + lock (startStopLock) 157 292 { 158 - if (exitCompleted) 293 + if (state.Value != GameThreadState.Running) 159 294 return; 160 295 161 - MakeCurrent(); 296 + // actual pause will be done in ProcessFrame. 297 + pauseRequested = true; 298 + } 162 299 163 - if (exitRequested) 164 - { 165 - PerformExit(); 166 - exitCompleted = true; 167 - return; 168 - } 300 + WaitForState(GameThreadState.Paused); 301 + } 169 302 303 + /// <summary> 304 + /// Spin indefinitely until this thread enters a required state. 305 + /// For cases where no native thread is present, this will run <see cref="processFrame"/> until the required state is reached. 306 + /// </summary> 307 + /// <param name="targetState">The state to wait for.</param> 308 + internal void WaitForState(GameThreadState targetState) 309 + { 310 + if (state.Value == targetState) 311 + return; 312 + 313 + if (Thread == null) 314 + { 315 + GameThreadState? newState = null; 316 + 317 + // if the thread is null at this point, we need to assume that this WaitForState call is running on the same native thread as this GameThread has/will be running. 318 + // run frames until the required state is reached. 319 + while (newState != targetState) 320 + newState = processFrame(); 321 + 322 + // note that the only state transition here can be an exiting one. entering a running state can only occur in Initialize(). 323 + setExitState(newState.Value); 324 + } 325 + else 326 + { 327 + while (state.Value != targetState) 328 + Thread.Sleep(1); 329 + } 330 + } 331 + 332 + /// <summary> 333 + /// Prepares this game thread for work. Should block until <see cref="Initialize"/> has been run. 334 + /// </summary> 335 + protected virtual void PrepareForWork() 336 + { 337 + Debug.Assert(Thread == null); 338 + createThread(); 339 + Debug.Assert(Thread != null); 340 + 341 + Thread.Start(); 342 + } 343 + 344 + /// <summary> 345 + /// Called whenever the thread is initialised. Should prepare the thread for performing work. 346 + /// </summary> 347 + protected virtual void OnInitialize() 348 + { 349 + } 350 + 351 + /// <summary> 352 + /// Called when a <see cref="Pause"/> or <see cref="Exit"/> is requested on this <see cref="GameThread"/>. 353 + /// Use this method to release exclusive resources that the thread could have been holding in its current execution mode, 354 + /// like GL contexts or similar. 355 + /// </summary> 356 + protected virtual void OnSuspended() 357 + { 358 + } 359 + 360 + /// <summary> 361 + /// Called when the thread is exited. Should clean up any thread-specific resources. 362 + /// </summary> 363 + protected virtual void OnExit() 364 + { 365 + } 366 + 367 + private void updateMaximumHz() 368 + { 369 + Scheduler.Add(() => Clock.MaximumUpdateHz = IsActive.Value ? activeHz : inactiveHz); 370 + } 371 + 372 + /// <summary> 373 + /// Create the native backing thread to run work. 374 + /// </summary> 375 + /// <remarks> 376 + /// This does not start the thread, but guarantees <see cref="Thread"/> is non-null. 377 + /// </remarks> 378 + private void createThread() 379 + { 380 + Debug.Assert(Thread == null); 381 + Debug.Assert(!Running); 382 + 383 + Thread = new Thread(runWork) 384 + { 385 + Name = PrefixedThreadNameFor(Name), 386 + IsBackground = true, 387 + }; 388 + 389 + void runWork() 390 + { 391 + Initialize(true); 392 + 393 + while (Running) 394 + RunSingleFrame(); 395 + } 396 + } 397 + 398 + /// <summary> 399 + /// Process a single frame of this thread's work. 400 + /// </summary> 401 + /// <returns>A potential execution state change.</returns> 402 + private GameThreadState? processFrame() 403 + { 404 + if (state.Value != GameThreadState.Running) 405 + // host could be in a suspended state. the input thread will still make calls to ProcessFrame so we can't throw. 406 + return null; 407 + 408 + MakeCurrent(); 409 + 410 + if (exitRequested) 411 + { 412 + exitRequested = false; 413 + return GameThreadState.Exited; 414 + } 415 + 416 + if (pauseRequested) 417 + { 418 + pauseRequested = false; 419 + return GameThreadState.Paused; 420 + } 421 + 422 + try 423 + { 170 424 Monitor?.NewFrame(); 171 425 172 426 using (Monitor?.BeginCollecting(PerformanceCollectionType.Scheduler)) ··· 188 442 else 189 443 throw; 190 444 } 191 - } 192 445 193 - private volatile bool exitRequested; 194 - private volatile bool exitCompleted; 195 - 196 - public bool Exited => exitCompleted; 197 - 198 - private CultureInfo culture; 199 - 200 - public CultureInfo CurrentCulture 201 - { 202 - get => culture; 203 - set 204 - { 205 - culture = value; 206 - 207 - updateCulture(); 208 - } 446 + return null; 209 447 } 210 448 211 449 private void updateCulture() ··· 216 454 Thread.CurrentUICulture = culture; 217 455 } 218 456 219 - private bool paused; 220 - 221 - public void Pause() 457 + private void setExitState(GameThreadState exitState) 222 458 { 223 - if (Thread != null) 224 - { 225 - paused = true; 226 - while (Running) 227 - Thread.Sleep(1); 228 - } 229 - else 459 + lock (startStopLock) 230 460 { 231 - Cleanup(); 232 - } 233 - } 461 + Debug.Assert(state.Value == GameThreadState.Running); 462 + Debug.Assert(exitState == GameThreadState.Exited || exitState == GameThreadState.Paused); 234 463 235 - protected virtual void Cleanup() 236 - { 237 - Thread = null; 238 - } 464 + Thread = null; 465 + OnSuspended(); 239 466 240 - public void Exit() => exitRequested = true; 467 + switch (exitState) 468 + { 469 + case GameThreadState.Exited: 470 + Monitor?.Dispose(); 471 + initializedEvent?.Dispose(); 241 472 242 - public virtual void Start() 243 - { 244 - paused = false; 473 + OnExit(); 474 + break; 475 + } 245 476 246 - if (Thread == null) 247 - { 248 - createThread(); 249 - Debug.Assert(Thread != null); 477 + state.Value = exitState; 250 478 } 251 - 252 - Thread.Start(); 253 479 } 254 480 255 - protected virtual void PerformExit() 256 - { 257 - Monitor?.Dispose(); 258 - initializedEvent?.Dispose(); 259 - } 260 - 261 - public class GameThreadScheduler : Scheduler 481 + private class GameThreadScheduler : Scheduler 262 482 { 263 483 public GameThreadScheduler(GameThread thread) 264 484 : base(() => thread.IsCurrent, thread.Clock)
+35
osu.Framework/Threading/GameThreadState.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using osu.Framework.Platform; 5 + 6 + namespace osu.Framework.Threading 7 + { 8 + public enum GameThreadState 9 + { 10 + /// <summary> 11 + /// This thread has not yet been started. 12 + /// </summary> 13 + NotStarted, 14 + 15 + /// <summary> 16 + /// This thread is preparing to run. 17 + /// </summary> 18 + Starting, 19 + 20 + /// <summary> 21 + /// This thread is running. 22 + /// </summary> 23 + Running, 24 + 25 + /// <summary> 26 + /// This thread is paused to be moved to a different native thread. This occurs when <see cref="ExecutionMode"/> changes. 27 + /// </summary> 28 + Paused, 29 + 30 + /// <summary> 31 + /// This thread has permanently exited. 32 + /// </summary> 33 + Exited 34 + } 35 + }
+7 -5
osu.Framework/Threading/InputThread.cs
··· 23 23 StatisticsCounterType.TabletEvents, 24 24 }; 25 25 26 + protected override void PrepareForWork() 27 + { 28 + // Intentionally inhibiting the base implementation which spawns a native thread. 29 + // Therefore, we need to run Initialize inline. 30 + Initialize(true); 31 + } 32 + 26 33 public override bool IsCurrent => ThreadSafety.IsInputThread; 27 34 28 35 internal sealed override void MakeCurrent() ··· 30 37 base.MakeCurrent(); 31 38 32 39 ThreadSafety.IsInputThread = true; 33 - } 34 - 35 - public override void Start() 36 - { 37 - // InputThread does not get started. it is run manually by GameHost. 38 40 } 39 41 } 40 42 }
+25
osu.Framework/Utils/Validation.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using System; 5 + using System.Diagnostics.CodeAnalysis; 4 6 using osuTK; 5 7 using osu.Framework.Graphics; 8 + 9 + #nullable enable 6 10 7 11 namespace osu.Framework.Utils 8 12 { ··· 23 27 /// <param name="toCheck">The <see cref="MarginPadding"/> to check.</param> 24 28 /// <returns>False if either component of <paramref name="toCheck"/> are Infinity or NaN, true otherwise. </returns> 25 29 public static bool IsFinite(MarginPadding toCheck) => float.IsFinite(toCheck.Top) && float.IsFinite(toCheck.Bottom) && float.IsFinite(toCheck.Left) && float.IsFinite(toCheck.Right); 30 + 31 + /// <summary> 32 + /// Attempts to parse <paramref name="uriString"/> as an absolute or relative <see cref="Uri"/> in a platform-agnostic manner. 33 + /// </summary> 34 + /// <remarks> 35 + /// This method is a workaround for inconsistencies across .NET and mono runtimes; 36 + /// on mono runtimes paths starting with <c>/</c> are considered absolute as per POSIX, 37 + /// and on .NET such paths are considered to be relative. 38 + /// This method uses the .NET behaviour. 39 + /// For more info, see <a href="https://www.mono-project.com/docs/faq/known-issues/urikind-relativeorabsolute/">Mono documentation</a>. 40 + /// </remarks> 41 + /// <param name="uriString">The string representation of the URI to parse.</param> 42 + /// <param name="result">The resultant parsed URI, if parsing succeeded.</param> 43 + /// <returns><see langword="true"/> if parsing succeeded; <see langword="false"/> otherwise.</returns> 44 + public static bool TryParseUri(string uriString, [NotNullWhen(true)] out Uri? result) 45 + { 46 + #pragma warning disable RS0030 // Bypassing banned API check, as it'll actually be used properly here 47 + UriKind kind = uriString.StartsWith('/') ? UriKind.Relative : UriKind.RelativeOrAbsolute; 48 + #pragma warning restore RS0030 49 + return Uri.TryCreate(uriString, kind, out result); 50 + } 26 51 } 27 52 }