diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 96c02a508b..056a325dce 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Utils;
@@ -9,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
@@ -25,40 +27,100 @@ namespace osu.Game.Rulesets.Osu.Mods
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
- private Random? rng;
+ private Random random = null!;
public void ApplyToBeatmap(IBeatmap beatmap)
{
- if (!(beatmap is OsuBeatmap osuBeatmap))
+ if (beatmap is not OsuBeatmap osuBeatmap)
return;
Seed.Value ??= RNG.Next();
- rng = new Random((int)Seed.Value);
+ random = new Random((int)Seed.Value);
var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
- float rateOfChangeMultiplier = 0;
+ // Offsets the angles of all hit objects in a "section" by the same amount.
+ float sectionOffset = 0;
- foreach (var positionInfo in positionInfos)
+ // Whether the angles are positive or negative (clockwise or counter-clockwise flow).
+ bool flowDirection = false;
+
+ for (int i = 0; i < positionInfos.Count; i++)
{
- // rateOfChangeMultiplier only changes every 5 iterations in a combo
- // to prevent shaky-line-shaped streams
- if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
- rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
-
- if (positionInfo == positionInfos.First())
+ if (shouldStartNewSection(osuBeatmap, positionInfos, i))
{
- positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
- positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
+ sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
+ flowDirection = !flowDirection;
+ }
+
+ if (i == 0)
+ {
+ positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
+ positionInfos[i].RelativeAngle = (float)(random.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
- positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
+ // Offsets only the angle of the current hit object if a flow change occurs.
+ float flowChangeOffset = 0;
+
+ // Offsets only the angle of the current hit object.
+ float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+
+ if (shouldApplyFlowChange(positionInfos, i))
+ {
+ flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+ flowDirection = !flowDirection;
+ }
+
+ float totalOffset =
+ // sectionOffset and oneTimeOffset should mainly affect patterns with large spacing.
+ (sectionOffset + oneTimeOffset) * positionInfos[i].DistanceFromPrevious +
+ // flowChangeOffset should mainly affect streams.
+ flowChangeOffset * (playfield_diagonal - positionInfos[i].DistanceFromPrevious);
+
+ positionInfos[i].RelativeAngle = getRelativeTargetAngle(positionInfos[i].DistanceFromPrevious, totalOffset, flowDirection);
}
}
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
+
+ /// The target distance between the previous and the current .
+ /// The angle (in rad) by which the target angle should be offset.
+ /// Whether the relative angle should be positive or negative.
+ private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
+ {
+ float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
+ float relativeAngle = (float)Math.PI - angle;
+ return flowDirection ? -relativeAngle : relativeAngle;
+ }
+
+ /// Whether a new section should be started at the current .
+ private bool shouldStartNewSection(OsuBeatmap beatmap, IReadOnlyList positionInfos, int i)
+ {
+ if (i == 0)
+ return true;
+
+ // Exclude new-combo-spam and 1-2-combos.
+ bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
+ positionInfos[i - 1].HitObject.NewCombo;
+ bool previousObjectWasOnDownbeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject, true);
+ bool previousObjectWasOnBeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject);
+
+ return (previousObjectStartedCombo && random.NextDouble() < 0.6f) ||
+ previousObjectWasOnDownbeat ||
+ (previousObjectWasOnBeat && random.NextDouble() < 0.4f);
+ }
+
+ /// Whether a flow change should be applied at the current .
+ private bool shouldApplyFlowChange(IReadOnlyList positionInfos, int i)
+ {
+ // Exclude new-combo-spam and 1-2-combos.
+ bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
+ positionInfos[i - 1].HitObject.NewCombo;
+
+ return previousObjectStartedCombo && random.NextDouble() < 0.6f;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 5e827d4782..3a8b3f67d0 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@@ -186,5 +187,39 @@ namespace osu.Game.Rulesets.Osu.Utils
length * MathF.Sin(angle)
);
}
+
+ /// The beatmap hitObject is a part of.
+ /// The that should be checked.
+ /// If true, this method only returns true if hitObject is on a downbeat.
+ /// If false, it returns true if hitObject is on any beat.
+ /// true if hitObject is on a (down-)beat, false otherwise.
+ public static bool IsHitObjectOnBeat(OsuBeatmap beatmap, OsuHitObject hitObject, bool downbeatsOnly = false)
+ {
+ var timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
+
+ double timeSinceTimingPoint = hitObject.StartTime - timingPoint.Time;
+
+ double beatLength = timingPoint.BeatLength;
+
+ if (downbeatsOnly)
+ beatLength *= timingPoint.TimeSignature.Numerator;
+
+ // Ensure within 1ms of expected location.
+ return Math.Abs(timeSinceTimingPoint + 1) % beatLength < 2;
+ }
+
+ ///
+ /// Generates a random number from a normal distribution using the Box-Muller transform.
+ ///
+ public static float RandomGaussian(Random rng, float mean = 0, float stdDev = 1)
+ {
+ // Generate 2 random numbers in the interval (0,1].
+ // x1 must not be 0 since log(0) = undefined.
+ double x1 = 1 - rng.NextDouble();
+ double x2 = 1 - rng.NextDouble();
+
+ double stdNormal = Math.Sqrt(-2 * Math.Log(x1)) * Math.Sin(2 * Math.PI * x2);
+ return mean + stdDev * (float)stdNormal;
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
index 6bcc45d97e..f95e3b9990 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
@@ -15,12 +15,14 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
+using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Beatmaps
{
@@ -295,5 +297,22 @@ namespace osu.Game.Tests.Visual.Beatmaps
BeatmapCardNormal firstCard() => this.ChildrenOfType().First();
}
+
+ [Test]
+ public void TestPlayButtonByTouchInput()
+ {
+ AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, beatmapSetInfo => new BeatmapCardNormal(beatmapSetInfo)));
+
+ // mimics touch input
+ AddStep("touch play button area on first card", () =>
+ {
+ InputManager.MoveMouseTo(firstCard().ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("first card is playing", () => firstCard().ChildrenOfType().Single().Playing.Value);
+
+ BeatmapCardNormal firstCard() => this.ChildrenOfType().First();
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
index 0af876bfe8..5afda0ad0c 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
};
});
AddStep("enable dim", () => thumbnail.Dimmed.Value = true);
- AddUntilStep("button visible", () => playButton.IsPresent);
+ AddUntilStep("button visible", () => playButton.Alpha == 1);
AddStep("click button", () =>
{
@@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddStep("disable dim", () => thumbnail.Dimmed.Value = false);
AddWaitStep("wait some", 3);
- AddAssert("button still visible", () => playButton.IsPresent);
+ AddAssert("button still visible", () => playButton.Alpha == 1);
// The track plays in real-time, so we need to check for progress in increments to avoid timeout.
AddUntilStep("progress > 0.25", () => thumbnail.ChildrenOfType().Single().Progress.Value > 0.25);
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddUntilStep("progress > 0.75", () => thumbnail.ChildrenOfType().Single().Progress.Value > 0.75);
AddUntilStep("wait for track to end", () => !playButton.Playing.Value);
- AddUntilStep("button hidden", () => !playButton.IsPresent);
+ AddUntilStep("button hidden", () => playButton.Alpha == 0);
}
private void iconIs(IconUsage usage) => AddUntilStep("icon is correct", () => playButton.ChildrenOfType().Any(icon => icon.Icon.Equals(usage)));
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSizePreservingSpriteText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSizePreservingSpriteText.cs
new file mode 100644
index 0000000000..c4568d9aeb
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSizePreservingSpriteText.cs
@@ -0,0 +1,94 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSizePreservingSpriteText : OsuGridTestScene
+ {
+ private readonly List parentContainers = new List();
+ private readonly List childContainers = new List();
+ private readonly OsuSpriteText osuSpriteText = new OsuSpriteText();
+ private readonly SizePreservingSpriteText sizePreservingSpriteText = new SizePreservingSpriteText();
+
+ public TestSceneSizePreservingSpriteText()
+ : base(1, 2)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ UprightAspectMaintainingContainer childContainer;
+ Container parentContainer = new Container
+ {
+ Origin = Anchor.BottomRight,
+ Anchor = Anchor.BottomCentre,
+ AutoSizeAxes = Axes.Both,
+ Rotation = 45,
+ Y = -200,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Red,
+ },
+ childContainer = new UprightAspectMaintainingContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Blue,
+ },
+ }
+ },
+ }
+ };
+
+ Container cellInfo = new Container
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Margin = new MarginPadding
+ {
+ Top = 100,
+ },
+ Child = new OsuSpriteText
+ {
+ Text = (i == 0) ? "OsuSpriteText" : "SizePreservingSpriteText",
+ Font = OsuFont.GetFont(Typeface.Inter, weight: FontWeight.Bold, size: 40),
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ },
+ };
+
+ parentContainers.Add(parentContainer);
+ childContainers.Add(childContainer);
+ Cell(i).Add(cellInfo);
+ Cell(i).Add(parentContainer);
+ }
+
+ childContainers[0].Add(osuSpriteText);
+ childContainers[1].Add(sizePreservingSpriteText);
+ osuSpriteText.Font = sizePreservingSpriteText.Font = OsuFont.GetFont(Typeface.Venera, weight: FontWeight.Bold, size: 20);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ osuSpriteText.Text = sizePreservingSpriteText.Text = DateTime.Now.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUprightAspectMaintainingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUprightAspectMaintainingContainer.cs
new file mode 100644
index 0000000000..67c26829df
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUprightAspectMaintainingContainer.cs
@@ -0,0 +1,244 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneUprightAspectMaintainingContainer : OsuGridTestScene
+ {
+ private const int rows = 3;
+ private const int columns = 4;
+
+ private readonly ScaleMode[] scaleModeValues = { ScaleMode.NoScaling, ScaleMode.Horizontal, ScaleMode.Vertical };
+ private readonly float[] scalingFactorValues = { 1.0f / 3, 1.0f / 2, 1.0f, 1.5f };
+
+ private readonly List> parentContainers = new List>(rows);
+ private readonly List> childContainers = new List>(rows);
+
+ // Preferably should be set to (4 * 2^n)
+ private const int rotation_step_count = 3;
+
+ private readonly List flipStates = new List();
+ private readonly List rotationSteps = new List();
+ private readonly List scaleSteps = new List();
+
+ public TestSceneUprightAspectMaintainingContainer()
+ : base(rows, columns)
+ {
+ for (int i = 0; i < rows; i++)
+ {
+ parentContainers.Add(new List());
+ childContainers.Add(new List());
+
+ for (int j = 0; j < columns; j++)
+ {
+ UprightAspectMaintainingContainer child;
+ Container parent = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Height = 80,
+ Width = 80,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(255, 0, 0, 160),
+ },
+ new OsuSpriteText
+ {
+ Text = "Parent",
+ },
+ child = new UprightAspectMaintainingContainer
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+
+ // These are the parameters being Tested
+ Scaling = scaleModeValues[i],
+ ScalingFactor = scalingFactorValues[j],
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(0, 0, 255, 160),
+ },
+ new OsuSpriteText
+ {
+ Text = "Text",
+ Font = OsuFont.Numeric,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Padding = new MarginPadding
+ {
+ Horizontal = 4,
+ Vertical = 4,
+ }
+ },
+ }
+ }
+ }
+ };
+
+ Container cellInfo = new Container
+ {
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = "Scaling: " + scaleModeValues[i].ToString(),
+ },
+ new OsuSpriteText
+ {
+ Text = "ScalingFactor: " + scalingFactorValues[j].ToString("0.00"),
+ Margin = new MarginPadding
+ {
+ Top = 15,
+ },
+ },
+ },
+ };
+
+ Cell(i * columns + j).Add(cellInfo);
+ Cell(i * columns + j).Add(parent);
+ parentContainers[i].Add(parent);
+ childContainers[i].Add(child);
+ }
+ }
+
+ flipStates.AddRange(new[] { 1, -1 });
+ rotationSteps.AddRange(Enumerable.Range(0, rotation_step_count).Select(x => 360f * ((float)x / rotation_step_count)));
+ scaleSteps.AddRange(new[] { 1, 0.3f, 1.5f });
+ }
+
+ [Test]
+ public void ExplicitlySizedParent()
+ {
+ var parentStates = from xFlip in flipStates
+ from yFlip in flipStates
+ from xScale in scaleSteps
+ from yScale in scaleSteps
+ from rotation in rotationSteps
+ select new { xFlip, yFlip, xScale, yScale, rotation };
+
+ foreach (var state in parentStates)
+ {
+ Vector2 parentScale = new Vector2(state.xFlip * state.xScale, state.yFlip * state.yScale);
+ float parentRotation = state.rotation;
+
+ AddStep("S: (" + parentScale.X.ToString("0.00") + ", " + parentScale.Y.ToString("0.00") + "), R: " + parentRotation.ToString("0.00"), () =>
+ {
+ foreach (List list in parentContainers)
+ {
+ foreach (Container container in list)
+ {
+ container.Scale = parentScale;
+ container.Rotation = parentRotation;
+ }
+ }
+ });
+
+ AddAssert("Check if state is valid", () =>
+ {
+ foreach (int i in Enumerable.Range(0, parentContainers.Count))
+ {
+ foreach (int j in Enumerable.Range(0, parentContainers[i].Count))
+ {
+ if (!uprightAspectMaintainingContainerStateIsValid(parentContainers[i][j], childContainers[i][j]))
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }
+ }
+
+ private bool uprightAspectMaintainingContainerStateIsValid(Container parent, UprightAspectMaintainingContainer child)
+ {
+ Matrix3 parentMatrix = parent.DrawInfo.Matrix;
+ Matrix3 childMatrix = child.DrawInfo.Matrix;
+ Vector3 childScale = childMatrix.ExtractScale();
+ Vector3 parentScale = parentMatrix.ExtractScale();
+
+ // Orientation check
+ if (!(isNearlyZero(MathF.Abs(childMatrix.M21)) && isNearlyZero(MathF.Abs(childMatrix.M12))))
+ return false;
+
+ // flip check
+ if (!(childMatrix.M11 * childMatrix.M22 > 0))
+ return false;
+
+ // Aspect ratio check
+ if (!isNearlyZero(childScale.X - childScale.Y))
+ return false;
+
+ // ScalingMode check
+ switch (child.Scaling)
+ {
+ case ScaleMode.NoScaling:
+ if (!(isNearlyZero(childMatrix.M11 - 1.0f) && isNearlyZero(childMatrix.M22 - 1.0f)))
+ return false;
+
+ break;
+
+ case ScaleMode.Vertical:
+ if (!(checkScaling(child.ScalingFactor, parentScale.Y, childScale.Y)))
+ return false;
+
+ break;
+
+ case ScaleMode.Horizontal:
+ if (!(checkScaling(child.ScalingFactor, parentScale.X, childScale.X)))
+ return false;
+
+ break;
+ }
+
+ return true;
+ }
+
+ private bool checkScaling(float scalingFactor, float parentScale, float childScale)
+ {
+ if (scalingFactor <= 1.0f)
+ {
+ if (!isNearlyZero(1.0f + (parentScale - 1.0f) * scalingFactor - childScale))
+ return false;
+ }
+ else if (scalingFactor > 1.0f)
+ {
+ if (parentScale < 1.0f)
+ {
+ if (!isNearlyZero((parentScale * (1.0f / scalingFactor)) - childScale))
+ return false;
+ }
+ else if (!isNearlyZero(parentScale * scalingFactor - childScale))
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool isNearlyZero(float f, float epsilon = Precision.FLOAT_EPSILON)
+ {
+ return f < epsilon;
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs
index 8ab632a757..c5436182a4 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs
@@ -41,6 +41,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
Anchor = Origin = Anchor.Centre;
+ // needed for touch input to work when card is not hovered/expanded
+ AlwaysPresent = true;
+
Children = new Drawable[]
{
icon = new SpriteIcon
diff --git a/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs b/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs
new file mode 100644
index 0000000000..300b5bd4b4
--- /dev/null
+++ b/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs
@@ -0,0 +1,119 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Layout;
+using osuTK;
+
+namespace osu.Game.Graphics.Containers
+{
+ ///
+ /// A container that reverts any rotation (and optionally scale) applied by its direct parent.
+ ///
+ public class UprightAspectMaintainingContainer : Container
+ {
+ ///
+ /// Controls how much this container scales compared to its parent (default is 1.0f).
+ ///
+ public float ScalingFactor { get; set; } = 1;
+
+ ///
+ /// Controls the scaling of this container.
+ ///
+ public ScaleMode Scaling { get; set; } = ScaleMode.Vertical;
+
+ private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawInfo, InvalidationSource.Parent);
+
+ public UprightAspectMaintainingContainer()
+ {
+ AddLayout(layout);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!layout.IsValid)
+ {
+ keepUprightAndUnstretched();
+ layout.Validate();
+ }
+ }
+
+ ///
+ /// Keeps the drawable upright and unstretched preventing it from being rotated, sheared, scaled or flipped with its Parent.
+ ///
+ private void keepUprightAndUnstretched()
+ {
+ // Decomposes the inverse of the parent DrawInfo.Matrix into rotation, shear and scale.
+ var parentMatrix = Parent.DrawInfo.Matrix;
+
+ // Remove Translation.>
+ parentMatrix.M31 = 0.0f;
+ parentMatrix.M32 = 0.0f;
+
+ Matrix3 reversedParent = parentMatrix.Inverted();
+
+ // Extract the rotation.
+ float angle = MathF.Atan2(reversedParent.M12, reversedParent.M11);
+ Rotation = MathHelper.RadiansToDegrees(angle);
+
+ // Remove rotation from the C matrix so that it only contains shear and scale.
+ Matrix3 m = Matrix3.CreateRotationZ(-angle);
+ reversedParent *= m;
+
+ // Extract shear.
+ float alpha = reversedParent.M21 / reversedParent.M22;
+ Shear = new Vector2(-alpha, 0);
+
+ // Etract scale.
+ float sx = reversedParent.M11;
+ float sy = reversedParent.M22;
+
+ Vector3 parentScale = parentMatrix.ExtractScale();
+
+ float usedScale = 1.0f;
+
+ switch (Scaling)
+ {
+ case ScaleMode.Horizontal:
+ usedScale = parentScale.X;
+ break;
+
+ case ScaleMode.Vertical:
+ usedScale = parentScale.Y;
+ break;
+ }
+
+ if (Scaling != ScaleMode.NoScaling)
+ {
+ if (ScalingFactor < 1.0f)
+ usedScale = 1.0f + (usedScale - 1.0f) * ScalingFactor;
+ if (ScalingFactor > 1.0f)
+ usedScale = (usedScale < 1.0f) ? usedScale * (1.0f / ScalingFactor) : usedScale * ScalingFactor;
+ }
+
+ Scale = new Vector2(sx * usedScale, sy * usedScale);
+ }
+ }
+
+ public enum ScaleMode
+ {
+ ///
+ /// Prevent this container from scaling.
+ ///
+ NoScaling,
+
+ ///
+ /// Scale uniformly (maintaining aspect ratio) based on the vertical scale of the parent.
+ ///
+ Vertical,
+
+ ///
+ /// Scale uniformly (maintaining aspect ratio) based on the horizontal scale of the parent.
+ ///
+ Horizontal,
+ }
+}
diff --git a/osu.Game/Graphics/Sprites/SizePreservingSpriteText.cs b/osu.Game/Graphics/Sprites/SizePreservingSpriteText.cs
new file mode 100644
index 0000000000..baffe106ed
--- /dev/null
+++ b/osu.Game/Graphics/Sprites/SizePreservingSpriteText.cs
@@ -0,0 +1,108 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Graphics.Sprites
+{
+ ///
+ /// A wrapped version of which will expand in size based on text content, but never shrink back down.
+ ///
+ public class SizePreservingSpriteText : CompositeDrawable
+ {
+ private readonly OsuSpriteText text = new OsuSpriteText();
+
+ private Vector2 maximumSize;
+
+ public SizePreservingSpriteText(Vector2? minimumSize = null)
+ {
+ text.Origin = Anchor.Centre;
+ text.Anchor = Anchor.Centre;
+
+ AddInternal(text);
+ maximumSize = minimumSize ?? Vector2.Zero;
+ }
+
+ protected override void Update()
+ {
+ Width = maximumSize.X = MathF.Max(maximumSize.X, text.Width);
+ Height = maximumSize.Y = MathF.Max(maximumSize.Y, text.Height);
+ }
+
+ public new Axes AutoSizeAxes
+ {
+ get => Axes.None;
+ set => throw new InvalidOperationException("You can't set AutoSizeAxes of this container");
+ }
+
+ ///
+ /// Gets or sets the text to be displayed.
+ ///
+ public LocalisableString Text
+ {
+ get => text.Text;
+ set => text.Text = value;
+ }
+
+ ///
+ /// Contains information on the font used to display the text.
+ ///
+ public FontUsage Font
+ {
+ get => text.Font;
+ set => text.Font = value;
+ }
+
+ ///
+ /// True if a shadow should be displayed around the text.
+ ///
+ public bool Shadow
+ {
+ get => text.Shadow;
+ set => text.Shadow = value;
+ }
+
+ ///
+ /// The colour of the shadow displayed around the text. A shadow will only be displayed if the property is set to true.
+ ///
+ public Color4 ShadowColour
+ {
+ get => text.ShadowColour;
+ set => text.ShadowColour = value;
+ }
+
+ ///
+ /// The offset of the shadow displayed around the text. A shadow will only be displayed if the property is set to true.
+ ///
+ public Vector2 ShadowOffset
+ {
+ get => text.ShadowOffset;
+ set => text.ShadowOffset = value;
+ }
+
+ ///
+ /// True if the 's vertical size should be equal to (the full height) or precisely the size of used characters.
+ /// Set to false to allow better centering of individual characters/numerals/etc.
+ ///
+ public bool UseFullGlyphHeight
+ {
+ get => text.UseFullGlyphHeight;
+ set => text.UseFullGlyphHeight = value;
+ }
+
+ public override bool IsPresent => text.IsPresent;
+
+ public override string ToString() => text.ToString();
+
+ public float LineBaseHeight => text.LineBaseHeight;
+
+ public IEnumerable FilterTerms => text.FilterTerms;
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
index 659984682e..30b420441c 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
@@ -16,7 +16,6 @@ namespace osu.Game.Screens.Play.HUD
{
public class DefaultSongProgress : SongProgress
{
- private const float info_height = 20;
private const float bottom_bar_height = 5;
private const float graph_height = SquareGraph.Column.WIDTH * 6;
private const float handle_height = 18;
@@ -65,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
- Height = info_height,
},
graph = new SongProgressGraph
{
@@ -178,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void Update()
{
base.Update();
- Height = bottom_bar_height + graph_height + handle_size.Y + info_height - graph.Y;
+ Height = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
}
private void updateBarVisibility()
diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
index 96a4c5f2bc..d0eb8f8ca1 100644
--- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System;
@@ -14,9 +15,9 @@ namespace osu.Game.Screens.Play.HUD
{
public class SongProgressInfo : Container
{
- private OsuSpriteText timeCurrent;
- private OsuSpriteText timeLeft;
- private OsuSpriteText progress;
+ private SizePreservingSpriteText timeCurrent;
+ private SizePreservingSpriteText timeLeft;
+ private SizePreservingSpriteText progress;
private double startTime;
private double endTime;
@@ -46,36 +47,71 @@ namespace osu.Game.Screens.Play.HUD
if (clock != null)
gameplayClock = clock;
+ AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
- timeCurrent = new OsuSpriteText
+ new Container
{
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Colour = colours.BlueLighter,
- Font = OsuFont.Numeric,
- Margin = new MarginPadding
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ Child = new UprightAspectMaintainingContainer
{
- Left = margin,
- },
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Scaling = ScaleMode.Vertical,
+ ScalingFactor = 0.5f,
+ Child = timeCurrent = new SizePreservingSpriteText
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Colour = colours.BlueLighter,
+ Font = OsuFont.Numeric,
+ }
+ }
},
- progress = new OsuSpriteText
+ new Container
{
- Origin = Anchor.BottomCentre,
- Anchor = Anchor.BottomCentre,
- Colour = colours.BlueLighter,
- Font = OsuFont.Numeric,
- },
- timeLeft = new OsuSpriteText
- {
- Origin = Anchor.BottomRight,
- Anchor = Anchor.BottomRight,
- Colour = colours.BlueLighter,
- Font = OsuFont.Numeric,
- Margin = new MarginPadding
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Child = new UprightAspectMaintainingContainer
{
- Right = margin,
- },
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Scaling = ScaleMode.Vertical,
+ ScalingFactor = 0.5f,
+ Child = progress = new SizePreservingSpriteText
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Colour = colours.BlueLighter,
+ Font = OsuFont.Numeric,
+ }
+ }
+ },
+ new Container
+ {
+ Origin = Anchor.CentreRight,
+ Anchor = Anchor.CentreRight,
+ AutoSizeAxes = Axes.Both,
+ Child = new UprightAspectMaintainingContainer
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Scaling = ScaleMode.Vertical,
+ ScalingFactor = 0.5f,
+ Child = timeLeft = new SizePreservingSpriteText
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Colour = colours.BlueLighter,
+ Font = OsuFont.Numeric,
+ }
+ }
}
};
}