1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 05:12:34 +08:00
Files
osu-lazer/osu.Game/Graphics/UserInterface/LoadingSpinner.cs
T
Dean Herbert c570db6c40 Add ability to search for users (#37225)
This was a private request for roundtable event usage. It’s also a
common feature request, so I decided to spend a bit of time getting this
working well-enough.


https://github.com/user-attachments/assets/acceb57f-2979-43d0-9fc2-33e977bd2dd5


---

### Delay loading spinner / loading layer initial load briefly to avoid
flickering

There's cases in this overlay where loading takes a few milliseconds.
The loading spinner gets annoying. This also happens elsewhere, so this
could be considered a global fix. Separate PR? probably...

### Ingest loading state of dashboard child content to show more correct
loading layer

Each display had their own loading layer implementation, but this is
already too deep (inside the scroll content) and doesn't display great
when for instance, results don't take up the full screen height.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2026-04-07 14:14:49 +02:00

201 lines
7.3 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Backgrounds;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A loading spinner.
/// </summary>
public partial class LoadingSpinner : VisibilityContainer
{
public const float TRANSITION_DURATION = 500;
private readonly SpriteIcon spinner;
protected override bool StartHidden => true;
protected Container MainContents;
private readonly TrianglesV2 triangles;
private readonly Container? trianglesMasking;
private readonly bool withBox;
private const float spin_duration = 900;
/// <summary>
/// Constuct a new loading spinner.
/// </summary>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
/// <param name="inverted">Whether colours should be inverted (black spinner instead of white).</param>
public LoadingSpinner(bool withBox = false, bool inverted = false)
{
this.withBox = withBox;
Size = new Vector2(60);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
if (withBox)
{
Child = MainContents = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 20,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
Colour = inverted ? Color4.White : Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0.7f,
},
triangles = new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
Colour = inverted ? Color4.White : Color4.Black,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
ScaleAdjust = 0.4f,
Velocity = 0.8f,
SpawnRatio = 2
},
spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Custom,
Colour = inverted ? Color4.Black : Color4.White,
Scale = new Vector2(0.6f),
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.CircleNotch
}
}
};
}
else
{
Children = new[]
{
MainContents = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Custom,
Colour = inverted ? Color4.Black : Color4.White,
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.CircleNotch
}
}
},
trianglesMasking = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Masking = true,
CornerRadius = 20,
Children = new Drawable[]
{
triangles = new TrianglesV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.4f,
Colour = ColourInfo.GradientVertical(
inverted ? Color4.Black.Opacity(0) : Color4.White.Opacity(0),
inverted ? Color4.Black : Color4.White),
RelativeSizeAxes = Axes.Both,
ScaleAdjust = 0.4f,
SpawnRatio = 4,
},
}
},
};
}
}
protected override void LoadComplete()
{
base.LoadComplete();
rotate();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// Font awesome icon isn't centered perfectly.
spinner.OriginPosition = spinner.DrawSize * 0.4963333333f;
if (withBox)
{
MainContents.CornerRadius = MainContents.DrawWidth / 4;
triangles.Rotation = -MainContents.Rotation;
}
else
{
Debug.Assert(trianglesMasking != null);
trianglesMasking.CornerRadius = MainContents.DrawWidth / 2;
}
}
protected override void PopIn()
{
if (Alpha < 0.5f)
// reset animation if the user can't see us.
rotate();
MainContents.ScaleTo(1, TRANSITION_DURATION, Easing.OutQuint);
// Very slight delay to avoid spinner flickering briefly during minimal loads.
// Note that we still use fade in here because it is important for input blocking cases (see `LoadingLayer`).
this.FadeTo(0.01f, 50)
.Then()
.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
MainContents.ScaleTo(0.6f, TRANSITION_DURATION, Easing.OutQuint);
this.FadeOut(TRANSITION_DURATION / 2, Easing.OutQuint);
}
private void rotate()
{
spinner.Spin(spin_duration * 3.5f, RotationDirection.Clockwise);
MainContents.RotateTo(0).Then()
.RotateTo(90, spin_duration, Easing.InOutQuart).Then()
.RotateTo(180, spin_duration, Easing.InOutQuart).Then()
.RotateTo(270, spin_duration, Easing.InOutQuart).Then()
.RotateTo(360, spin_duration, Easing.InOutQuart).Then()
.Loop();
}
}
}