diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6583d36179..5905850e62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: # Attempt to upload results even if test fails. # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always - name: Upload Test Results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: osu-framework-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.os.configuration}} diff --git a/osu.Framework.Android/osu.Framework.Android.csproj b/osu.Framework.Android/osu.Framework.Android.csproj index 968c6888a7..54e6baf66a 100644 --- a/osu.Framework.Android/osu.Framework.Android.csproj +++ b/osu.Framework.Android/osu.Framework.Android.csproj @@ -18,7 +18,7 @@ - + diff --git a/osu.Framework.Tests/Graphics/RendererTest.cs b/osu.Framework.Tests/Graphics/RendererTest.cs index 1f77e8e1cb..fd84554e2c 100644 --- a/osu.Framework.Tests/Graphics/RendererTest.cs +++ b/osu.Framework.Tests/Graphics/RendererTest.cs @@ -26,5 +26,28 @@ public void TestWhitePixelReuseUpdatesTextureWrapping() Assert.That(renderer.CurrentWrapModeS, Is.EqualTo(WrapMode.None)); Assert.That(renderer.CurrentWrapModeS, Is.EqualTo(WrapMode.None)); } + + [Test] + public void TestTextureAtlasReuseUpdatesTextureWrapping() + { + DummyRenderer renderer = new DummyRenderer(); + + TextureAtlas atlas = new TextureAtlas(renderer, 1024, 1024); + + Texture textureWrapNone = atlas.Add(100, 100, WrapMode.None, WrapMode.None)!; + Texture textureWrapClamp = atlas.Add(100, 100, WrapMode.ClampToEdge, WrapMode.ClampToEdge)!; + + renderer.BindTexture(textureWrapNone, 0, null, null); + Assert.That(renderer.CurrentWrapModeS, Is.EqualTo(WrapMode.None)); + Assert.That(renderer.CurrentWrapModeT, Is.EqualTo(WrapMode.None)); + + renderer.BindTexture(textureWrapClamp, 0, null, null); + Assert.That(renderer.CurrentWrapModeS, Is.EqualTo(WrapMode.ClampToEdge)); + Assert.That(renderer.CurrentWrapModeT, Is.EqualTo(WrapMode.ClampToEdge)); + + renderer.BindTexture(textureWrapNone, 0, null, null); + Assert.That(renderer.CurrentWrapModeS, Is.EqualTo(WrapMode.None)); + Assert.That(renderer.CurrentWrapModeT, Is.EqualTo(WrapMode.None)); + } } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneSliderBar.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneSliderBar.cs index d41832481c..9cf6f699c3 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneSliderBar.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneSliderBar.cs @@ -59,6 +59,7 @@ public TestSceneSliderBar() Size = new Vector2(200, 50), BackgroundColour = Color4.White, SelectionColour = Color4.Pink, + FocusColour = Color4.OrangeRed, KeyboardStep = 1, Current = sliderBarValue }, @@ -72,6 +73,7 @@ public TestSceneSliderBar() RangePadding = 20, BackgroundColour = Color4.White, SelectionColour = Color4.Pink, + FocusColour = Color4.OrangeRed, KeyboardStep = 1, Current = sliderBarValue }, @@ -85,6 +87,7 @@ public TestSceneSliderBar() Size = new Vector2(200, 10), BackgroundColour = Color4.White, SelectionColour = Color4.Pink, + FocusColour = Color4.OrangeRed, KeyboardStep = 1, Current = sliderBarValue }, @@ -97,6 +100,7 @@ public TestSceneSliderBar() Size = new Vector2(200, 10), BackgroundColour = Color4.White, SelectionColour = Color4.Pink, + FocusColour = Color4.OrangeRed, KeyboardStep = 1, Current = sliderBarValue }, @@ -109,6 +113,8 @@ public TestSceneSliderBar() { sliderBar.Current.Disabled = false; sliderBar.Current.Value = 0; + sliderBar.GetContainingFocusManager()!.ChangeFocus(null); + sliderBarWithNub.GetContainingFocusManager()!.ChangeFocus(null); }); [Test] @@ -122,6 +128,7 @@ public void TestVerticalDragHasNoEffect() () => { InputManager.MoveMouseTo(sliderBar.ToScreenSpace(sliderBar.DrawSize * new Vector2(0.75f, 1f))); }); AddStep("Release Click", () => { InputManager.ReleaseButton(MouseButton.Left); }); checkValue(0); + AddAssert("Slider has no focus", () => !sliderBar.HasFocus); } [Test] @@ -136,6 +143,7 @@ public void TestDragOutReleaseInHasNoEffect() AddStep("Drag Up", () => { InputManager.MoveMouseTo(sliderBar.ToScreenSpace(sliderBar.DrawSize * new Vector2(0.25f, 0.5f))); }); AddStep("Release Click", () => { InputManager.ReleaseButton(MouseButton.Left); }); checkValue(0); + AddAssert("Slider has focus", () => sliderBar.HasFocus); } [Test] @@ -160,6 +168,23 @@ public void TestKeyboardInput() InputManager.ReleaseKey(Key.Right); }); checkValue(1); + + AddStep("Click slider", () => InputManager.Click(MouseButton.Left)); + checkValue(-5); + + AddAssert("Slider has focus", () => sliderBar.HasFocus); + + AddStep("move mouse outside", () => + { + InputManager.MoveMouseTo(sliderBar.ToScreenSpace(sliderBar.DrawSize * new Vector2(2f, 0.5f))); + }); + + AddStep("Press right arrow key", () => + { + InputManager.PressKey(Key.Right); + InputManager.ReleaseKey(Key.Right); + }); + checkValue(-4); } [TestCase(false)] @@ -246,6 +271,7 @@ public void TestAbsoluteDrag() () => { InputManager.MoveMouseTo(sliderBarWithNub.ToScreenSpace(sliderBarWithNub.DrawSize * new Vector2(0.4f, 1f))); }); AddStep("Release Click", () => { InputManager.ReleaseButton(MouseButton.Left); }); checkValue(-2); + AddAssert("Slider has focus", () => sliderBarWithNub.HasFocus); } [Test] @@ -259,6 +285,7 @@ public void TestRelativeDrag() () => { InputManager.MoveMouseTo(sliderBarWithNub.ToScreenSpace(sliderBarWithNub.DrawSize * new Vector2(0.75f, 1f))); }); AddStep("Release Click", () => { InputManager.ReleaseButton(MouseButton.Left); }); checkValue(3); + AddAssert("Slider has focus", () => sliderBarWithNub.HasFocus); } [Test] diff --git a/osu.Framework/FrameworkEnvironment.cs b/osu.Framework/FrameworkEnvironment.cs index 5b4b5ee8ee..f5c4369bbc 100644 --- a/osu.Framework/FrameworkEnvironment.cs +++ b/osu.Framework/FrameworkEnvironment.cs @@ -53,7 +53,7 @@ static FrameworkEnvironment() if (DebugUtils.IsDebugBuild) AllowInsecureRequests = parseBool(Environment.GetEnvironmentVariable("OSU_INSECURE_REQUESTS")) ?? false; - UseSDL3 = RuntimeInfo.IsMobile || (parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? false); + UseSDL3 = RuntimeInfo.IsMobile || (parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? true); } private static bool? parseBool(string? value) diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 8066d379ed..01a8a1f39a 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -818,7 +818,10 @@ public bool BindTexture(Texture texture, int unit, WrapMode? wrapModeS, WrapMode public bool BindTexture(INativeTexture texture, int unit = 0, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) { if (lastActiveTextureUnit == unit && lastBoundTexture[unit] == texture) + { + setWrapMode(wrapModeS, wrapModeT); return true; + } FlushCurrentBatch(FlushBatchSource.BindTexture); diff --git a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs index 17b932cee9..9b6697d13d 100644 --- a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs @@ -4,6 +4,7 @@ using System.Numerics; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using Vector2 = osuTK.Vector2; namespace osu.Framework.Graphics.UserInterface @@ -23,6 +24,18 @@ public Color4 SelectionColour set => SelectionBox.Colour = value; } + private Color4 focusColour = FrameworkColour.YellowGreen; + + public Color4 FocusColour + { + get => focusColour; + set + { + focusColour = value; + updateFocus(); + } + } + protected readonly Box SelectionBox; protected readonly Box Box; @@ -37,10 +50,37 @@ public BasicSliderBar() }, SelectionBox = new Box { - RelativeSizeAxes = Axes.Both, Colour = FrameworkColour.Yellow, + RelativeSizeAxes = Axes.Both, } }; + + Masking = true; + } + + protected override void OnFocus(FocusEvent e) + { + updateFocus(); + base.OnFocus(e); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + updateFocus(); + base.OnFocusLost(e); + } + + private void updateFocus() + { + if (HasFocus) + { + BorderThickness = 3; + BorderColour = FocusColour; + } + else + { + BorderThickness = 0; + } } protected override void UpdateValue(float value) diff --git a/osu.Framework/Graphics/UserInterface/SliderBar.cs b/osu.Framework/Graphics/UserInterface/SliderBar.cs index d1232baea4..20fe6a18b7 100644 --- a/osu.Framework/Graphics/UserInterface/SliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/SliderBar.cs @@ -166,18 +166,21 @@ protected override bool OnDragStart(DragStartEvent e) return false; } + GetContainingFocusManager()?.ChangeFocus(this); handleMouseInput(e); return true; } protected override void OnDragEnd(DragEndEvent e) => Commit(); + public override bool AcceptsFocus => true; + protected override bool OnKeyDown(KeyDownEvent e) { if (currentNumberInstantaneous.Disabled) return false; - if (!IsHovered) + if (!IsHovered && !HasFocus) return false; float step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(currentNumberInstantaneous.MaxValue) - Convert.ToSingle(currentNumberInstantaneous.MinValue)) / 20; diff --git a/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs b/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs index c66615f740..2362c7c73e 100644 --- a/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs +++ b/osu.Framework/Graphics/Visualisation/DrawVisualiser.cs @@ -207,6 +207,8 @@ protected override void Update() overlay.Target = Searching ? cursorTarget : inputManager.HoveredDrawables.OfType().FirstOrDefault()?.Target; } + private static readonly Dictionary is_type_valid_target_cache = new Dictionary(); + private void updateCursorTarget() { Drawable drawableTarget = null; @@ -268,30 +270,32 @@ void findTarget(Drawable drawable) if (!validForTarget(drawable)) return; - // Special case for full-screen overlays that act as input receptors, but don't display anything - if (!hasCustomDrawNode(drawable)) - return; - drawableTarget = drawable; } } // Valid if the drawable contains the mouse position and the position wouldn't be masked by the parent bool validForTarget(Drawable drawable) - => drawable.ScreenSpaceDrawQuad.Contains(inputManager.CurrentState.Mouse.Position) - && maskingQuad?.Contains(inputManager.CurrentState.Mouse.Position) != false; - } + { + if (!drawable.ScreenSpaceDrawQuad.Contains(inputManager.CurrentState.Mouse.Position) + || maskingQuad?.Contains(inputManager.CurrentState.Mouse.Position) == false) + { + return false; + } - private static readonly Dictionary has_custom_drawnode_cache = new Dictionary(); + Type type = drawable.GetType(); - private bool hasCustomDrawNode(Drawable drawable) - { - var type = drawable.GetType(); + if (is_type_valid_target_cache.TryGetValue(type, out bool valid)) + return valid; - if (has_custom_drawnode_cache.TryGetValue(type, out bool existing)) - return existing; + // Exclude "overlay" objects (Component/etc) that don't draw anything and don't override CreateDrawNode(). + valid = type.GetMethod(nameof(CreateDrawNode), BindingFlags.Instance | BindingFlags.NonPublic)?.DeclaringType != typeof(Drawable); - return has_custom_drawnode_cache[type] = type.GetMethod(nameof(CreateDrawNode), BindingFlags.Instance | BindingFlags.NonPublic)?.DeclaringType != typeof(Drawable); + // Exclude objects that specify they should be hidden anyway. + valid &= !type.GetCustomAttributes(true).Any(); + + return is_type_valid_target_cache[type] = valid; + } } public bool Searching { get; private set; } diff --git a/osu.Framework/Graphics/Visualisation/DrawVisualiserHiddenAttribute.cs b/osu.Framework/Graphics/Visualisation/DrawVisualiserHiddenAttribute.cs new file mode 100644 index 0000000000..c5c62d683c --- /dev/null +++ b/osu.Framework/Graphics/Visualisation/DrawVisualiserHiddenAttribute.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Graphics.Visualisation +{ + /// + /// Indicates that instances of this type or any subtype should not be valid targets for the draw visualiser. + /// + [AttributeUsage(AttributeTargets.Class)] + public class DrawVisualiserHiddenAttribute : Attribute; +} diff --git a/osu.Framework/IO/Network/WebRequest.cs b/osu.Framework/IO/Network/WebRequest.cs index 4fff194c84..e3d5a91b48 100644 --- a/osu.Framework/IO/Network/WebRequest.cs +++ b/osu.Framework/IO/Network/WebRequest.cs @@ -119,7 +119,7 @@ public class WebRequest : IDisposable /// /// FILE parameters. /// - private readonly IDictionary files = new Dictionary(); + private readonly List files = new List(); /// /// The request headers. @@ -349,9 +349,9 @@ private async Task internalPerform(CancellationToken cancellationToken = default foreach (var p in files) { - var byteContent = new ByteArrayContent(p.Value); + var byteContent = new ByteArrayContent(p.Content); byteContent.Headers.Add("Content-Type", "application/octet-stream"); - formData.Add(byteContent, p.Key, p.Key); + formData.Add(byteContent, p.ParamName, p.Filename); } postContent = await formData.ReadAsStreamAsync(linkedToken.Token).ConfigureAwait(false); @@ -662,17 +662,21 @@ public void AddRaw(Stream stream) } /// - /// Add a new FILE parameter to this request. Replaces any existing file with the same name. + /// Add a new FILE parameter to this request. /// This may not be used in conjunction with . GET requests may not contain files. /// - /// The name of the file. This becomes the name of the file in a multi-part form POST content. + /// The name of the form parameter of the request that the file relates to. /// The file data. - public void AddFile(string name, byte[] data) + /// + /// The filename of the file to be sent to be reported to the server in the Content-Disposition header. + /// blob is used by default if omitted, to mirror browser behaviour. + /// + public void AddFile(string paramName, byte[] data, string filename = "blob") { - ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(paramName); ArgumentNullException.ThrowIfNull(data); - files[name] = data; + files.Add(new FormFile(paramName, data, filename)); } /// @@ -931,5 +935,7 @@ protected override void Dispose(bool disposing) baseStream.Dispose(); } } + + private record struct FormFile(string ParamName, byte[] Content, string Filename); } } diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 0133948419..f9b558c41b 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -40,7 +40,7 @@ - +