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 @@
-
+