mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' into skin-fonts
This commit is contained in:
@ -31,12 +31,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// </summary>
|
||||
private const float equilateral_triangle_ratio = 0.866f;
|
||||
|
||||
/// <summary>
|
||||
/// How many screen-space pixels are smoothed over.
|
||||
/// Same behavior as Sprite's EdgeSmoothness.
|
||||
/// </summary>
|
||||
private const float edge_smoothness = 1;
|
||||
|
||||
private Color4 colourLight = Color4.White;
|
||||
|
||||
public Color4 ColourLight
|
||||
@ -83,6 +77,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
set => triangleScale.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s
|
||||
/// shape is drawn to the screen.
|
||||
/// </summary>
|
||||
public bool Masking { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether we should drop-off alpha values of triangles more quickly to improve
|
||||
/// the visual appearance of fading. This defaults to on as it is generally more
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
private void load(IRenderer renderer, ShaderManager shaders)
|
||||
{
|
||||
texture = renderer.WhitePixel;
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -252,14 +252,18 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private class TrianglesDrawNode : DrawNode
|
||||
{
|
||||
private float fill = 1f;
|
||||
|
||||
protected new Triangles Source => (Triangles)base.Source;
|
||||
|
||||
private IShader shader;
|
||||
private Texture texture;
|
||||
private bool masking;
|
||||
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
private Vector2 size;
|
||||
private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
|
||||
|
||||
private Vector2 size;
|
||||
private IVertexBatch<TexturedVertex2D> vertexBatch;
|
||||
|
||||
public TrianglesDrawNode(Triangles source)
|
||||
@ -274,6 +278,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
shader = Source.shader;
|
||||
texture = Source.texture;
|
||||
size = Source.DrawSize;
|
||||
masking = Source.Masking;
|
||||
|
||||
parts.Clear();
|
||||
parts.AddRange(Source.parts);
|
||||
@ -290,34 +295,52 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
}
|
||||
|
||||
shader.Bind();
|
||||
|
||||
Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy;
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
|
||||
|
||||
foreach (TriangleParticle particle in parts)
|
||||
{
|
||||
var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio);
|
||||
Vector2 relativeSize = Vector2.Divide(triangleSize * particle.Scale, size);
|
||||
|
||||
var triangle = new Triangle(
|
||||
Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(particle.Position * size + offset, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(particle.Position * size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix)
|
||||
Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
|
||||
|
||||
Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
|
||||
var drawQuad = new Quad(
|
||||
Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.TopRight * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.BottomLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
ColourInfo colourInfo = DrawColourInfo.Colour;
|
||||
colourInfo.ApplyChild(particle.Colour);
|
||||
|
||||
renderer.DrawTriangle(
|
||||
texture,
|
||||
triangle,
|
||||
colourInfo,
|
||||
null,
|
||||
vertexBatch.AddAction,
|
||||
Vector2.Divide(localInflationAmount, new Vector2(2 * offset.X, offset.Y)));
|
||||
RectangleF textureCoords = new RectangleF(
|
||||
triangleQuad.TopLeft.X - topLeft.X,
|
||||
triangleQuad.TopLeft.Y - topLeft.Y,
|
||||
triangleQuad.Width,
|
||||
triangleQuad.Height
|
||||
) / relativeSize;
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private static Quad clampToDrawable(Vector2 topLeft, Vector2 size)
|
||||
{
|
||||
float leftClamped = Math.Clamp(topLeft.X, 0f, 1f);
|
||||
float topClamped = Math.Clamp(topLeft.Y, 0f, 1f);
|
||||
|
||||
return new Quad(
|
||||
leftClamped,
|
||||
topClamped,
|
||||
Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped,
|
||||
Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -11,9 +11,7 @@ using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
@ -23,28 +21,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
private const float triangle_size = 100;
|
||||
private const float base_velocity = 50;
|
||||
private const int texture_height = 128;
|
||||
|
||||
/// <summary>
|
||||
/// sqrt(3) / 2
|
||||
/// </summary>
|
||||
private const float equilateral_triangle_ratio = 0.866f;
|
||||
|
||||
private readonly Bindable<Color4> colourTop = new Bindable<Color4>(Color4.White);
|
||||
private readonly Bindable<Color4> colourBottom = new Bindable<Color4>(Color4.Black);
|
||||
|
||||
public Color4 ColourTop
|
||||
{
|
||||
get => colourTop.Value;
|
||||
set => colourTop.Value = value;
|
||||
}
|
||||
|
||||
public Color4 ColourBottom
|
||||
{
|
||||
get => colourBottom.Value;
|
||||
set => colourBottom.Value = value;
|
||||
}
|
||||
|
||||
public float Thickness { get; set; } = 0.02f; // No need for invalidation since it's happening in Update()
|
||||
|
||||
/// <summary>
|
||||
@ -70,9 +52,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
|
||||
[Resolved]
|
||||
private IRenderer renderer { get; set; } = null!;
|
||||
|
||||
private Random? stableRandom;
|
||||
|
||||
private IShader shader = null!;
|
||||
@ -89,42 +68,19 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders)
|
||||
private void load(ShaderManager shaders, IRenderer renderer)
|
||||
{
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
|
||||
texture = renderer.WhitePixel;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
colourTop.BindValueChanged(_ => updateTexture());
|
||||
colourBottom.BindValueChanged(_ => updateTexture(), true);
|
||||
|
||||
spawnRatio.BindValueChanged(_ => Reset(), true);
|
||||
}
|
||||
|
||||
private void updateTexture()
|
||||
{
|
||||
var image = new Image<Rgba32>(texture_height, 1);
|
||||
|
||||
texture = renderer.CreateTexture(1, texture_height, true);
|
||||
|
||||
for (int i = 0; i < texture_height; i++)
|
||||
{
|
||||
float ratio = (float)i / texture_height;
|
||||
|
||||
image[i, 0] = new Rgba32(
|
||||
colourBottom.Value.R * ratio + colourTop.Value.R * (1f - ratio),
|
||||
colourBottom.Value.G * ratio + colourTop.Value.G * (1f - ratio),
|
||||
colourBottom.Value.B * ratio + colourTop.Value.B * (1f - ratio)
|
||||
);
|
||||
}
|
||||
|
||||
texture.SetData(new TextureUpload(image));
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -227,6 +183,9 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
private Texture texture = null!;
|
||||
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
|
||||
private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
|
||||
|
||||
private Vector2 size;
|
||||
private float thickness;
|
||||
private float texelSize;
|
||||
@ -246,7 +205,15 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
texture = Source.texture;
|
||||
size = Source.DrawSize;
|
||||
thickness = Source.Thickness;
|
||||
texelSize = Math.Max(1.5f / Source.ScreenSpaceDrawQuad.Size.X, 1.5f / Source.ScreenSpaceDrawQuad.Size.Y);
|
||||
|
||||
Quad triangleQuad = new Quad(
|
||||
Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(new Vector2(triangle_size, 0f), DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(new Vector2(0f, triangleSize.Y), DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleSize, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
texelSize = 1.5f / triangleQuad.Height;
|
||||
|
||||
parts.Clear();
|
||||
parts.AddRange(Source.parts);
|
||||
@ -256,7 +223,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
if (Source.AimCount == 0)
|
||||
if (Source.AimCount == 0 || thickness == 0)
|
||||
return;
|
||||
|
||||
if (vertexBatch == null || vertexBatch.Size != Source.AimCount)
|
||||
@ -269,35 +236,42 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref thickness);
|
||||
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
|
||||
float relativeHeight = triangleSize.Y / size.Y;
|
||||
float relativeWidth = triangleSize.X / size.X;
|
||||
|
||||
foreach (TriangleParticle particle in parts)
|
||||
{
|
||||
var offset = triangle_size * new Vector2(0.5f, equilateral_triangle_ratio);
|
||||
|
||||
Vector2 topLeft = particle.Position * size + new Vector2(-offset.X, 0f);
|
||||
Vector2 topRight = particle.Position * size + new Vector2(offset.X, 0);
|
||||
Vector2 bottomLeft = particle.Position * size + new Vector2(-offset.X, offset.Y);
|
||||
Vector2 bottomRight = particle.Position * size + new Vector2(offset.X, offset.Y);
|
||||
Vector2 topLeft = particle.Position - new Vector2(relativeWidth * 0.5f, 0f);
|
||||
Vector2 topRight = topLeft + new Vector2(relativeWidth, 0f);
|
||||
Vector2 bottomLeft = topLeft + new Vector2(0f, relativeHeight);
|
||||
Vector2 bottomRight = bottomLeft + new Vector2(relativeWidth, 0f);
|
||||
|
||||
var drawQuad = new Quad(
|
||||
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)
|
||||
Vector2Extensions.Transform(topLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(topRight * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomRight * size, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
var tRect = new Quad(
|
||||
topLeft.X / size.X,
|
||||
topLeft.Y / size.Y * texture_height,
|
||||
(topRight.X - topLeft.X) / size.X,
|
||||
(bottomRight.Y - topRight.Y) / size.Y * texture_height
|
||||
).AABBFloat;
|
||||
ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, new Quad(topLeft, topRight, bottomLeft, bottomRight));
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour, tRect, vertexBatch.AddAction, textureCoords: tRect);
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, vertexAction: vertexBatch.AddAction);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private static ColourInfo triangleColourInfo(ColourInfo source, Quad quad)
|
||||
{
|
||||
return new ColourInfo
|
||||
{
|
||||
TopLeft = source.Interpolate(quad.TopLeft),
|
||||
TopRight = source.Interpolate(quad.TopRight),
|
||||
BottomLeft = source.Interpolate(quad.BottomLeft),
|
||||
BottomRight = source.Interpolate(quad.BottomRight)
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -36,8 +36,7 @@ namespace osu.Game.Graphics.Containers
|
||||
/// <param name="easing">The easing type of the initial transform.</param>
|
||||
public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None)
|
||||
{
|
||||
if (logo == null)
|
||||
throw new ArgumentNullException(nameof(logo));
|
||||
ArgumentNullException.ThrowIfNull(logo);
|
||||
|
||||
if (logo.IsTracking && Logo == null)
|
||||
throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s");
|
||||
|
@ -0,0 +1,34 @@
|
||||
// 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 Markdig;
|
||||
using Markdig.Extensions.GenericAttributes;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A variant of <see cref="Markdig.Extensions.GenericAttributes.GenericAttributesExtension"/>
|
||||
/// which only handles generic attributes in the current markdown <see cref="Block"/> and ignores inline generic attributes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For rationale, see implementation of <see cref="Setup(Markdig.MarkdownPipelineBuilder)"/>.
|
||||
/// </remarks>
|
||||
public class BlockAttributeExtension : IMarkdownExtension
|
||||
{
|
||||
private readonly GenericAttributesExtension genericAttributesExtension = new GenericAttributesExtension();
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
genericAttributesExtension.Setup(pipeline);
|
||||
|
||||
// GenericAttributesExtension registers a GenericAttributesParser in pipeline.InlineParsers.
|
||||
// this conflicts with the CustomContainerExtension, leading to some custom containers (e.g. flags) not displaying.
|
||||
// as a workaround, remove the inline parser here before it can do damage.
|
||||
pipeline.InlineParsers.RemoveAll(parser => parser is GenericAttributesParser);
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => genericAttributesExtension.Setup(pipeline, renderer);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// 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 Markdig;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Extensions
|
||||
{
|
||||
public static class OsuMarkdownExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses the block attributes extension.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline.</returns>
|
||||
public static MarkdownPipelineBuilder UseBlockAttributes(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<BlockAttributeExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// 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 Markdig.Extensions.Footnotes;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Containers.Markdown.Footnotes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
|
||||
{
|
||||
public partial class OsuMarkdownFootnote : MarkdownFootnote
|
||||
{
|
||||
public OsuMarkdownFootnote(Footnote footnote)
|
||||
: base(footnote)
|
||||
{
|
||||
}
|
||||
|
||||
public override SpriteText CreateOrderMarker(int order) => CreateSpriteText().With(marker =>
|
||||
{
|
||||
marker.Text = LocalisableString.Format("{0}.", order);
|
||||
});
|
||||
|
||||
public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(textFlow =>
|
||||
{
|
||||
textFlow.Margin = new MarginPadding { Left = 30 };
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
|
||||
{
|
||||
public partial class OsuMarkdownFootnoteBacklink : OsuHoverContainer
|
||||
{
|
||||
private readonly FootnoteLink backlink;
|
||||
|
||||
private SpriteIcon spriteIcon = null!;
|
||||
|
||||
[Resolved]
|
||||
private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
|
||||
|
||||
protected override IEnumerable<Drawable> EffectTargets => spriteIcon.Yield();
|
||||
|
||||
public OsuMarkdownFootnoteBacklink(FootnoteLink backlink)
|
||||
{
|
||||
this.backlink = backlink;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider colourProvider, OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
|
||||
{
|
||||
float fontSize = parentTextComponent.CreateSpriteText().Font.Size;
|
||||
Size = new Vector2(fontSize);
|
||||
|
||||
IdleColour = colourProvider.Light2;
|
||||
HoverColour = colourProvider.Light1;
|
||||
|
||||
Add(spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Size = new Vector2(fontSize / 2),
|
||||
Icon = FontAwesome.Solid.ArrowUp,
|
||||
});
|
||||
|
||||
if (scrollContainer != null)
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
var footnoteLink = markdownContainer.ChildrenOfType<OsuMarkdownFootnoteLink>().Single(footnoteLink => footnoteLink.FootnoteLink.Index == backlink.Index);
|
||||
scrollContainer.ScrollIntoView(footnoteLink);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
|
||||
{
|
||||
public partial class OsuMarkdownFootnoteLink : OsuHoverContainer, IHasCustomTooltip
|
||||
{
|
||||
public readonly FootnoteLink FootnoteLink;
|
||||
|
||||
private SpriteText spriteText = null!;
|
||||
|
||||
[Resolved]
|
||||
private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuMarkdownContainer markdownContainer { get; set; } = null!;
|
||||
|
||||
protected override IEnumerable<Drawable> EffectTargets => spriteText.Yield();
|
||||
|
||||
public OsuMarkdownFootnoteLink(FootnoteLink footnoteLink)
|
||||
{
|
||||
FootnoteLink = footnoteLink;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
|
||||
{
|
||||
IdleColour = colourProvider.Light2;
|
||||
HoverColour = colourProvider.Light1;
|
||||
|
||||
spriteText = parentTextComponent.CreateSpriteText();
|
||||
|
||||
Add(spriteText.With(t =>
|
||||
{
|
||||
float baseSize = t.Font.Size;
|
||||
t.Font = t.Font.With(size: baseSize * 0.58f);
|
||||
t.Margin = new MarginPadding { Bottom = 0.33f * baseSize };
|
||||
t.Text = LocalisableString.Format("[{0}]", FootnoteLink.Index);
|
||||
}));
|
||||
|
||||
if (scrollContainer != null)
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
var footnote = markdownContainer.ChildrenOfType<OsuMarkdownFootnote>().Single(footnote => footnote.Footnote.Label == FootnoteLink.Footnote.Label);
|
||||
scrollContainer.ScrollIntoView(footnote);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public object TooltipContent
|
||||
{
|
||||
get
|
||||
{
|
||||
var span = FootnoteLink.Footnote.LastChild.Span;
|
||||
return markdownContainer.Text.Substring(span.Start, span.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public ITooltip GetCustomTooltip() => new OsuMarkdownFootnoteTooltip(colourProvider);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
// 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 Markdig.Extensions.Footnotes;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
|
||||
{
|
||||
public partial class OsuMarkdownFootnoteTooltip : CompositeDrawable, ITooltip
|
||||
{
|
||||
private readonly FootnoteMarkdownContainer markdownContainer;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider;
|
||||
|
||||
public OsuMarkdownFootnoteTooltip(OverlayColourProvider colourProvider)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
|
||||
Masking = true;
|
||||
Width = 200;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
CornerRadius = 4;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background6
|
||||
},
|
||||
markdownContainer = new FootnoteMarkdownContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
DocumentMargin = new MarginPadding(),
|
||||
DocumentPadding = new MarginPadding { Horizontal = 10, Vertical = 5 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
public void SetContent(object content) => markdownContainer.SetContent((string)content);
|
||||
|
||||
private partial class FootnoteMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
private string? lastFootnote;
|
||||
|
||||
public void SetContent(string footnote)
|
||||
{
|
||||
if (footnote == lastFootnote)
|
||||
return;
|
||||
|
||||
lastFootnote = Text = footnote;
|
||||
}
|
||||
|
||||
public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
|
||||
}
|
||||
|
||||
private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
|
||||
{
|
||||
protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink)
|
||||
{
|
||||
// we don't want footnote backlinks to show up in tooltips.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,41 +4,25 @@
|
||||
#nullable disable
|
||||
|
||||
using Markdig;
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Syntax;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Containers.Markdown.Footnotes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers.Markdown.Footnotes;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown
|
||||
{
|
||||
[Cached]
|
||||
public partial class OsuMarkdownContainer : MarkdownContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows this markdown container to parse and link footnotes.
|
||||
/// </summary>
|
||||
/// <seealso cref="FootnoteExtension"/>
|
||||
protected virtual bool Footnotes => false;
|
||||
|
||||
/// <summary>
|
||||
/// Allows this markdown container to make URL text clickable.
|
||||
/// </summary>
|
||||
/// <seealso cref="AutoLinkExtension"/>
|
||||
protected virtual bool Autolinks => false;
|
||||
|
||||
/// <summary>
|
||||
/// Allows this markdown container to parse custom containers (used for flags and infoboxes).
|
||||
/// </summary>
|
||||
/// <seealso cref="CustomContainerExtension"/>
|
||||
protected virtual bool CustomContainers => false;
|
||||
|
||||
public OsuMarkdownContainer()
|
||||
{
|
||||
LineSpacing = 21;
|
||||
@ -99,25 +83,17 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
return new OsuMarkdownUnorderedListItem(level);
|
||||
}
|
||||
|
||||
// reference: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
|
||||
protected override MarkdownPipeline CreateBuilder()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoIdentifiers()
|
||||
.UsePipeTables()
|
||||
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
|
||||
.UseYamlFrontMatter();
|
||||
protected override MarkdownFootnoteGroup CreateFootnoteGroup(FootnoteGroup footnoteGroup) => base.CreateFootnoteGroup(footnoteGroup).With(g => g.Spacing = new Vector2(5));
|
||||
|
||||
if (Footnotes)
|
||||
pipeline = pipeline.UseFootnotes();
|
||||
protected override MarkdownFootnote CreateFootnote(Footnote footnote) => new OsuMarkdownFootnote(footnote);
|
||||
|
||||
if (Autolinks)
|
||||
pipeline = pipeline.UseAutoLinks();
|
||||
protected sealed override MarkdownPipeline CreateBuilder()
|
||||
=> Options.BuildPipeline();
|
||||
|
||||
if (CustomContainers)
|
||||
pipeline.UseCustomContainers();
|
||||
|
||||
return pipeline.Build();
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a <see cref="OsuMarkdownContainerOptions"/> instance which is used to determine
|
||||
/// which CommonMark/Markdig extensions should be enabled for this <see cref="OsuMarkdownContainer"/>.
|
||||
/// </summary>
|
||||
protected virtual OsuMarkdownContainerOptions Options => new OsuMarkdownContainerOptions();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
// 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 Markdig;
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using osu.Game.Graphics.Containers.Markdown.Extensions;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown
|
||||
{
|
||||
/// <summary>
|
||||
/// Groups options of customising the set of available extensions to <see cref="OsuMarkdownContainer"/> instances.
|
||||
/// </summary>
|
||||
public class OsuMarkdownContainerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows the <see cref="OsuMarkdownContainer"/> to parse and link footnotes.
|
||||
/// </summary>
|
||||
/// <seealso cref="FootnoteExtension"/>
|
||||
public bool Footnotes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows the <see cref="OsuMarkdownContainer"/> container to make URL text clickable.
|
||||
/// </summary>
|
||||
/// <seealso cref="AutoLinkExtension"/>
|
||||
public bool Autolinks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows the <see cref="OsuMarkdownContainer"/> to parse custom containers (used for flags and infoboxes).
|
||||
/// </summary>
|
||||
/// <seealso cref="CustomContainerExtension"/>
|
||||
public bool CustomContainers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows the <see cref="OsuMarkdownContainer"/> to parse custom attributes in block elements (used e.g. for custom anchor names in the wiki).
|
||||
/// </summary>
|
||||
/// <seealso cref="BlockAttributeExtension"/>
|
||||
public bool BlockAttributes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a prepared <see cref="MarkdownPipeline"/> according to the options specified by the current <see cref="OsuMarkdownContainerOptions"/> instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Compare: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
|
||||
/// </remarks>
|
||||
public MarkdownPipeline BuildPipeline()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoIdentifiers()
|
||||
.UsePipeTables()
|
||||
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
|
||||
.UseYamlFrontMatter();
|
||||
|
||||
if (Footnotes)
|
||||
pipeline = pipeline.UseFootnotes();
|
||||
|
||||
if (Autolinks)
|
||||
pipeline = pipeline.UseAutoLinks();
|
||||
|
||||
if (CustomContainers)
|
||||
pipeline = pipeline.UseCustomContainers();
|
||||
|
||||
if (BlockAttributes)
|
||||
pipeline = pipeline.UseBlockAttributes();
|
||||
|
||||
return pipeline.Build();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers.Markdown.Footnotes;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
@ -36,6 +38,10 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
Text = codeInline.Content
|
||||
});
|
||||
|
||||
protected override void AddFootnoteLink(FootnoteLink footnoteLink) => AddDrawable(new OsuMarkdownFootnoteLink(footnoteLink));
|
||||
|
||||
protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink));
|
||||
|
||||
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
|
||||
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@ -18,6 +18,12 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
|
||||
&& Content.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
|
||||
@ -38,11 +44,11 @@ namespace osu.Game.Graphics.Containers
|
||||
content.AutoSizeAxes = AutoSizeAxes;
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
content,
|
||||
CreateHoverSounds(sampleSet)
|
||||
};
|
||||
AddInternal(content);
|
||||
Add(CreateHoverSounds(sampleSet));
|
||||
}
|
||||
|
||||
protected override void ClearInternal(bool disposeChildren = true) =>
|
||||
throw new InvalidOperationException($"Clearing {nameof(InternalChildren)} will cause critical failure. Use {nameof(Clear)} instead.");
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
|
||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||
|
||||
protected override bool BlockScrollInput => false;
|
||||
|
||||
protected override bool BlockNonPositionalInput => true;
|
||||
|
||||
/// <summary>
|
||||
@ -90,6 +88,15 @@ namespace osu.Game.Graphics.Containers
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
// allow for controlling volume when alt is held.
|
||||
// mostly for compatibility with osu-stable.
|
||||
if (e.AltPressed) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
|
@ -240,7 +240,9 @@ namespace osu.Game.Graphics.Containers
|
||||
headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
|
||||
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
|
||||
|
||||
float smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
|
||||
var flowChildren = scrollContentContainer.FlowingChildren.OfType<T>();
|
||||
|
||||
float smallestSectionHeight = flowChildren.Any() ? flowChildren.Min(d => d.Height) : 0;
|
||||
|
||||
// scroll offset is our fixed header height if we have it plus 10% of content height
|
||||
// plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
|
||||
@ -249,7 +251,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
|
||||
|
||||
var presentChildren = Children.Where(c => c.IsPresent);
|
||||
var presentChildren = flowChildren.Where(c => c.IsPresent);
|
||||
|
||||
if (lastClickedSection != null)
|
||||
SelectedSection.Value = lastClickedSection;
|
||||
|
@ -234,7 +234,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
SampleChannel channel = tapSample.GetChannel();
|
||||
|
||||
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
|
||||
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
|
||||
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
|
||||
channel.Volume.Value = baseFrequency;
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
@ -69,8 +70,8 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
DateTimeOffset localDate = date.ToLocalTime();
|
||||
|
||||
dateText.Text = $"{localDate:d MMMM yyyy} ";
|
||||
timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
|
||||
dateText.Text = LocalisableString.Interpolate($"{localDate:d MMMM yyyy} ");
|
||||
timeText.Text = LocalisableString.Interpolate($"{localDate:HH:mm:ss \"UTC\"z}");
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@ -22,38 +23,8 @@ namespace osu.Game.Graphics
|
||||
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the colour for a <see cref="DifficultyRating"/>.
|
||||
/// Retrieves the colour for a given point in the star range.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sourced from the @diff-{rating} variables in https://github.com/ppy/osu-web/blob/71fbab8936d79a7929d13854f5e854b4f383b236/resources/assets/less/variables.less.
|
||||
/// </remarks>
|
||||
public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false)
|
||||
{
|
||||
switch (difficulty)
|
||||
{
|
||||
case DifficultyRating.Easy:
|
||||
return Color4Extensions.FromHex("4ebfff");
|
||||
|
||||
case DifficultyRating.Normal:
|
||||
return Color4Extensions.FromHex("66ff91");
|
||||
|
||||
case DifficultyRating.Hard:
|
||||
return Color4Extensions.FromHex("f7e85d");
|
||||
|
||||
case DifficultyRating.Insane:
|
||||
return Color4Extensions.FromHex("ff7e68");
|
||||
|
||||
case DifficultyRating.Expert:
|
||||
return Color4Extensions.FromHex("fe3c71");
|
||||
|
||||
case DifficultyRating.ExpertPlus:
|
||||
return Color4Extensions.FromHex("6662dd");
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(difficulty));
|
||||
}
|
||||
}
|
||||
|
||||
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
|
||||
{
|
||||
(0.1f, Color4Extensions.FromHex("aaaaaa")),
|
||||
@ -217,6 +188,41 @@ namespace osu.Game.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves colour for a <see cref="RankingTier"/>.
|
||||
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
|
||||
/// </summary>
|
||||
public ColourInfo ForRankingTier(RankingTier tier)
|
||||
{
|
||||
switch (tier)
|
||||
{
|
||||
default:
|
||||
case RankingTier.Iron:
|
||||
return Color4Extensions.FromHex(@"BAB3AB");
|
||||
|
||||
case RankingTier.Bronze:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"B88F7A"), Color4Extensions.FromHex(@"855C47"));
|
||||
|
||||
case RankingTier.Silver:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"E0E0EB"), Color4Extensions.FromHex(@"A3A3C2"));
|
||||
|
||||
case RankingTier.Gold:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"F0E4A8"), Color4Extensions.FromHex(@"E0C952"));
|
||||
|
||||
case RankingTier.Platinum:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"A8F0EF"), Color4Extensions.FromHex(@"52E0DF"));
|
||||
|
||||
case RankingTier.Rhodium:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"D9F8D3"), Color4Extensions.FromHex(@"A0CF96"));
|
||||
|
||||
case RankingTier.Radiant:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"97DCFF"), Color4Extensions.FromHex(@"ED82FF"));
|
||||
|
||||
case RankingTier.Lustrous:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"FFE600"), Color4Extensions.FromHex(@"ED82FF"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a foreground text colour that is supposed to contrast well with
|
||||
/// the supplied <paramref name="backgroundColour"/>.
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
@ -117,11 +118,11 @@ namespace osu.Game.Graphics
|
||||
|
||||
host.GetClipboard()?.SetImage(image);
|
||||
|
||||
string filename = getFilename();
|
||||
(string filename, var stream) = getWritableStream();
|
||||
|
||||
if (filename == null) return;
|
||||
|
||||
using (var stream = storage.CreateFileSafely(filename))
|
||||
using (stream)
|
||||
{
|
||||
switch (screenshotFormat.Value)
|
||||
{
|
||||
@ -142,7 +143,7 @@ namespace osu.Game.Graphics
|
||||
|
||||
notificationOverlay.Post(new SimpleNotification
|
||||
{
|
||||
Text = $"{filename} saved!",
|
||||
Text = $"Screenshot {filename} saved!",
|
||||
Activated = () =>
|
||||
{
|
||||
storage.PresentFileExternally(filename);
|
||||
@ -152,23 +153,28 @@ namespace osu.Game.Graphics
|
||||
}
|
||||
});
|
||||
|
||||
private string getFilename()
|
||||
private static readonly object filename_reservation_lock = new object();
|
||||
|
||||
private (string filename, Stream stream) getWritableStream()
|
||||
{
|
||||
var dt = DateTime.Now;
|
||||
string fileExt = screenshotFormat.ToString().ToLowerInvariant();
|
||||
|
||||
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
|
||||
if (!storage.Exists(withoutIndex))
|
||||
return withoutIndex;
|
||||
|
||||
for (ulong i = 1; i < ulong.MaxValue; i++)
|
||||
lock (filename_reservation_lock)
|
||||
{
|
||||
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
|
||||
if (!storage.Exists(indexedName))
|
||||
return indexedName;
|
||||
}
|
||||
var dt = DateTime.Now;
|
||||
string fileExt = screenshotFormat.ToString().ToLowerInvariant();
|
||||
|
||||
return null;
|
||||
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
|
||||
if (!storage.Exists(withoutIndex))
|
||||
return (withoutIndex, storage.GetStream(withoutIndex, FileAccess.Write, FileMode.Create));
|
||||
|
||||
for (ulong i = 1; i < ulong.MaxValue; i++)
|
||||
{
|
||||
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
|
||||
if (!storage.Exists(indexedName))
|
||||
return (indexedName, storage.GetStream(indexedName, FileAccess.Write, FileMode.Create));
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +52,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public readonly SpriteIcon Chevron;
|
||||
|
||||
//don't allow clicking between transitions and don't make the chevron clickable
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceivePositionalInputAt(screenSpacePos);
|
||||
//don't allow clicking between transitions
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && base.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public override bool HandleNonPositionalInput => State == Visibility.Visible;
|
||||
public override bool HandlePositionalInput => State == Visibility.Visible;
|
||||
@ -95,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
Text.Font = Text.Font.With(size: 18);
|
||||
Text.Margin = new MarginPadding { Vertical = 8 };
|
||||
Padding = new MarginPadding { Right = padding + ChevronSize };
|
||||
Margin = new MarginPadding { Right = padding + ChevronSize };
|
||||
Add(Chevron = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
|
@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (Link != null)
|
||||
{
|
||||
items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
|
||||
items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
|
||||
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Up:
|
||||
|
@ -114,8 +114,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
current.UnbindBindings();
|
||||
current.BindTo(value);
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -20,16 +19,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <summary>
|
||||
/// A button with added default sound effects.
|
||||
/// </summary>
|
||||
public partial class OsuButton : Button
|
||||
public abstract partial class OsuButton : Button
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => SpriteText?.Text ?? default;
|
||||
set
|
||||
{
|
||||
if (SpriteText != null)
|
||||
SpriteText.Text = value;
|
||||
}
|
||||
get => SpriteText.Text;
|
||||
set => SpriteText.Text = value;
|
||||
}
|
||||
|
||||
private Color4? backgroundColour;
|
||||
@ -66,13 +61,19 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
|
||||
&& Content.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
protected Box Hover;
|
||||
protected Box Background;
|
||||
protected SpriteText SpriteText;
|
||||
|
||||
private readonly Box flashLayer;
|
||||
|
||||
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
|
||||
protected OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
|
||||
{
|
||||
Height = 40;
|
||||
|
||||
@ -115,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
});
|
||||
|
||||
if (hoverSounds.HasValue)
|
||||
AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
|
||||
Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public OsuEnumDropdown()
|
||||
{
|
||||
Items = (T[])Enum.GetValues(typeof(T));
|
||||
Items = Enum.GetValues<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
@ -13,12 +11,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public readonly MenuItemType Type;
|
||||
|
||||
public OsuMenuItem(string text, MenuItemType type = MenuItemType.Standard)
|
||||
public OsuMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard)
|
||||
: this(text, type, null)
|
||||
{
|
||||
}
|
||||
|
||||
public OsuMenuItem(LocalisableString text, MenuItemType type, Action action)
|
||||
public OsuMenuItem(LocalisableString text, MenuItemType type, Action? action)
|
||||
: base(text, action)
|
||||
{
|
||||
Type = type;
|
||||
|
@ -250,13 +250,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
BorderThickness = 3;
|
||||
if (Masking)
|
||||
BorderThickness = 3;
|
||||
|
||||
base.OnFocus(e);
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
BorderThickness = 0;
|
||||
if (Masking)
|
||||
BorderThickness = 0;
|
||||
|
||||
base.OnFocusLost(e);
|
||||
}
|
||||
@ -277,7 +280,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
var samples = sampleMap[feedbackSampleType];
|
||||
|
||||
if (samples == null || samples.Length == 0)
|
||||
if (samples.Length == 0)
|
||||
return null;
|
||||
|
||||
return samples[RNG.Next(0, samples.Length)]?.GetChannel();
|
||||
|
212
osu.Game/Graphics/UserInterface/RangeSlider.cs
Normal file
212
osu.Game/Graphics/UserInterface/RangeSlider.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// 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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class RangeSlider : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The lower limiting value
|
||||
/// </summary>
|
||||
public Bindable<double> LowerBound
|
||||
{
|
||||
get => lowerBound.Current;
|
||||
set => lowerBound.Current = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The upper limiting value
|
||||
/// </summary>
|
||||
public Bindable<double> UpperBound
|
||||
{
|
||||
get => upperBound.Current;
|
||||
set => upperBound.Current = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text that describes this RangeSlider's functionality
|
||||
/// </summary>
|
||||
public string Label
|
||||
{
|
||||
set => label.Text = value;
|
||||
}
|
||||
|
||||
public float NubWidth
|
||||
{
|
||||
set => lowerBound.NubWidth = upperBound.NubWidth = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum difference between the lower bound and higher bound
|
||||
/// </summary>
|
||||
public float MinRange
|
||||
{
|
||||
set => minRange = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lower bound display for when it is set to its default value
|
||||
/// </summary>
|
||||
public string DefaultStringLowerBound
|
||||
{
|
||||
set => lowerBound.DefaultString = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// upper bound display for when it is set to its default value
|
||||
/// </summary>
|
||||
public string DefaultStringUpperBound
|
||||
{
|
||||
set => upperBound.DefaultString = value;
|
||||
}
|
||||
|
||||
public LocalisableString DefaultTooltipLowerBound
|
||||
{
|
||||
set => lowerBound.DefaultTooltip = value;
|
||||
}
|
||||
|
||||
public LocalisableString DefaultTooltipUpperBound
|
||||
{
|
||||
set => upperBound.DefaultTooltip = value;
|
||||
}
|
||||
|
||||
public string TooltipSuffix
|
||||
{
|
||||
set => upperBound.TooltipSuffix = lowerBound.TooltipSuffix = value;
|
||||
}
|
||||
|
||||
private float minRange = 0.1f;
|
||||
|
||||
private readonly OsuSpriteText label;
|
||||
|
||||
private readonly LowerBoundSlider lowerBound;
|
||||
private readonly UpperBoundSlider upperBound;
|
||||
|
||||
public RangeSlider()
|
||||
{
|
||||
const float vertical_offset = 13;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
label = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14),
|
||||
},
|
||||
upperBound = new UpperBoundSlider
|
||||
{
|
||||
KeyboardStep = 0.1f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Y = vertical_offset,
|
||||
},
|
||||
lowerBound = new LowerBoundSlider
|
||||
{
|
||||
KeyboardStep = 0.1f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Y = vertical_offset,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
lowerBound.Current.ValueChanged += min => upperBound.Current.Value = Math.Max(min.NewValue + minRange, upperBound.Current.Value);
|
||||
upperBound.Current.ValueChanged += max => lowerBound.Current.Value = Math.Min(max.NewValue - minRange, lowerBound.Current.Value);
|
||||
}
|
||||
|
||||
private partial class LowerBoundSlider : BoundSlider
|
||||
{
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LeftBox.Height = 6; // hide any colour bleeding from overlap
|
||||
|
||||
AccentColour = BackgroundColour;
|
||||
BackgroundColour = Color4.Transparent;
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
&& screenSpacePos.X <= Nub.ScreenSpaceDrawQuad.TopRight.X;
|
||||
}
|
||||
|
||||
private partial class UpperBoundSlider : BoundSlider
|
||||
{
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
RightBox.Height = 6; // just to match the left bar height really
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
&& screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X;
|
||||
}
|
||||
|
||||
protected partial class BoundSlider : OsuSliderBar<double>
|
||||
{
|
||||
public string? DefaultString;
|
||||
public LocalisableString? DefaultTooltip;
|
||||
public string? TooltipSuffix;
|
||||
public float NubWidth { get; set; } = Nub.HEIGHT;
|
||||
|
||||
public override LocalisableString TooltipText =>
|
||||
(Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}");
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
base.OnHover(e);
|
||||
return true; // Make sure only one nub shows hover effect at once.
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Nub.Width = NubWidth;
|
||||
RangePadding = Nub.Width / 2;
|
||||
|
||||
OsuSpriteText currentDisplay;
|
||||
|
||||
Nub.Add(currentDisplay = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -0.5f,
|
||||
Colour = Color4.White,
|
||||
Font = OsuFont.Torus.With(size: 10),
|
||||
});
|
||||
|
||||
Current.BindValueChanged(current =>
|
||||
{
|
||||
currentDisplay.Text = (current.NewValue != Current.Default ? current.NewValue.ToString("N1") : DefaultString) ?? current.NewValue.ToString("N1");
|
||||
}, true);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? colourProvider)
|
||||
{
|
||||
if (colourProvider == null) return;
|
||||
|
||||
AccentColour = colourProvider.Background2;
|
||||
Nub.AccentColour = colourProvider.Background2;
|
||||
Nub.GlowingAccentColour = colourProvider.Background1;
|
||||
Nub.GlowColour = colourProvider.Background2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
354
osu.Game/Graphics/UserInterface/SegmentedGraph.cs
Normal file
354
osu.Game/Graphics/UserInterface/SegmentedGraph.cs
Normal file
@ -0,0 +1,354 @@
|
||||
// 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;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class SegmentedGraph<T> : Drawable
|
||||
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
|
||||
{
|
||||
private bool graphNeedsUpdate;
|
||||
|
||||
private T[]? values;
|
||||
private int[] tiers = Array.Empty<int>();
|
||||
private readonly SegmentManager segments;
|
||||
|
||||
private int tierCount;
|
||||
|
||||
public SegmentedGraph(int tierCount = 1)
|
||||
{
|
||||
this.tierCount = tierCount;
|
||||
tierColours = new[]
|
||||
{
|
||||
new Colour4(0, 0, 0, 0)
|
||||
};
|
||||
segments = new SegmentManager(tierCount);
|
||||
}
|
||||
|
||||
public T[] Values
|
||||
{
|
||||
get => values ?? Array.Empty<T>();
|
||||
set
|
||||
{
|
||||
if (value == values) return;
|
||||
|
||||
values = value;
|
||||
graphNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<Colour4> tierColours;
|
||||
|
||||
public IReadOnlyList<Colour4> TierColours
|
||||
{
|
||||
get => tierColours;
|
||||
set
|
||||
{
|
||||
tierCount = value.Count;
|
||||
tierColours = value;
|
||||
|
||||
graphNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Texture texture = null!;
|
||||
private IShader shader = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IRenderer renderer, ShaderManager shaders)
|
||||
{
|
||||
texture = renderer.WhitePixel;
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (graphNeedsUpdate)
|
||||
{
|
||||
recalculateTiers(values);
|
||||
recalculateSegments();
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
graphNeedsUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateTiers(T[]? arr)
|
||||
{
|
||||
if (arr == null || arr.Length == 0)
|
||||
{
|
||||
tiers = Array.Empty<int>();
|
||||
return;
|
||||
}
|
||||
|
||||
float[] floatValues = arr.Select(v => Convert.ToSingle(v)).ToArray();
|
||||
|
||||
// Shift values to eliminate negative ones
|
||||
float min = floatValues.Min();
|
||||
|
||||
if (min < 0)
|
||||
{
|
||||
for (int i = 0; i < floatValues.Length; i++)
|
||||
floatValues[i] += Math.Abs(min);
|
||||
}
|
||||
|
||||
// Normalize values
|
||||
float max = floatValues.Max();
|
||||
|
||||
for (int i = 0; i < floatValues.Length; i++)
|
||||
floatValues[i] /= max;
|
||||
|
||||
// Deduce tiers from values
|
||||
tiers = floatValues.Select(v => (int)Math.Floor(v * tierCount)).ToArray();
|
||||
}
|
||||
|
||||
private void recalculateSegments()
|
||||
{
|
||||
segments.Clear();
|
||||
|
||||
if (tiers.Length == 0)
|
||||
{
|
||||
segments.Add(0, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tiers.Length; i++)
|
||||
{
|
||||
for (int tier = 0; tier < tierCount; tier++)
|
||||
{
|
||||
if (tier < 0)
|
||||
continue;
|
||||
|
||||
// One tier covers itself and all tiers above it.
|
||||
// By layering multiple transparent boxes, higher tiers will be brighter.
|
||||
// If using opaque colors, higher tiers will be on front, covering lower tiers.
|
||||
if (tiers[i] >= tier)
|
||||
{
|
||||
if (!segments.IsTierStarted(tier))
|
||||
segments.StartSegment(tier, i * 1f / tiers.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (segments.IsTierStarted(tier))
|
||||
segments.EndSegment(tier, i * 1f / tiers.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segments.EndAllPendingSegments();
|
||||
segments.Sort();
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
|
||||
|
||||
protected struct SegmentInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The tier this segment is at.
|
||||
/// </summary>
|
||||
public int Tier;
|
||||
|
||||
/// <summary>
|
||||
/// The progress at which this segment starts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value is a normalized float (from 0 to 1).
|
||||
/// </remarks>
|
||||
public float Start;
|
||||
|
||||
/// <summary>
|
||||
/// The progress at which this segment ends.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value is a normalized float (from 0 to 1).
|
||||
/// </remarks>
|
||||
public float End;
|
||||
|
||||
/// <summary>
|
||||
/// The length of this segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value is a normalized float (from 0 to 1).
|
||||
/// </remarks>
|
||||
public float Length => End - Start;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({Tier}, {Start * 100}%, {End * 100}%)";
|
||||
}
|
||||
}
|
||||
|
||||
private class SegmentedGraphDrawNode : DrawNode
|
||||
{
|
||||
public new SegmentedGraph<T> Source => (SegmentedGraph<T>)base.Source;
|
||||
|
||||
private Texture texture = null!;
|
||||
private IShader shader = null!;
|
||||
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
|
||||
private Vector2 drawSize;
|
||||
private readonly List<Colour4> tierColours = new List<Colour4>();
|
||||
|
||||
public SegmentedGraphDrawNode(SegmentedGraph<T> source)
|
||||
: base(source)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
texture = Source.texture;
|
||||
shader = Source.shader;
|
||||
drawSize = Source.DrawSize;
|
||||
|
||||
segments.Clear();
|
||||
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
|
||||
|
||||
tierColours.Clear();
|
||||
tierColours.AddRange(Source.tierColours);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
shader.Bind();
|
||||
|
||||
foreach (SegmentInfo segment in segments)
|
||||
{
|
||||
Vector2 topLeft = new Vector2(segment.Start * drawSize.X, 0);
|
||||
Vector2 topRight = new Vector2(segment.End * drawSize.X, 0);
|
||||
Vector2 bottomLeft = new Vector2(segment.Start * drawSize.X, drawSize.Y);
|
||||
Vector2 bottomRight = new Vector2(segment.End * drawSize.X, drawSize.Y);
|
||||
|
||||
renderer.DrawQuad(
|
||||
texture,
|
||||
new Quad(
|
||||
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
|
||||
getSegmentColour(segment));
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private ColourInfo getSegmentColour(SegmentInfo segment)
|
||||
{
|
||||
var segmentColour = new ColourInfo
|
||||
{
|
||||
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
|
||||
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
|
||||
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
|
||||
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
|
||||
};
|
||||
|
||||
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
|
||||
segmentColour.ApplyChild(tierColour);
|
||||
|
||||
return segmentColour;
|
||||
}
|
||||
}
|
||||
|
||||
protected class SegmentManager : IEnumerable<SegmentInfo>
|
||||
{
|
||||
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
|
||||
|
||||
private readonly SegmentInfo?[] pendingSegments;
|
||||
|
||||
public SegmentManager(int tierCount)
|
||||
{
|
||||
pendingSegments = new SegmentInfo?[tierCount];
|
||||
}
|
||||
|
||||
public void StartSegment(int tier, float start)
|
||||
{
|
||||
if (pendingSegments[tier] != null)
|
||||
throw new InvalidOperationException($"Another {nameof(SegmentInfo)} of tier {tier.ToString()} has already been started.");
|
||||
|
||||
pendingSegments[tier] = new SegmentInfo
|
||||
{
|
||||
Tier = tier,
|
||||
Start = Math.Clamp(start, 0, 1)
|
||||
};
|
||||
}
|
||||
|
||||
public void EndSegment(int tier, float end)
|
||||
{
|
||||
SegmentInfo? pendingSegment = pendingSegments[tier];
|
||||
if (pendingSegment == null)
|
||||
throw new InvalidOperationException($"Cannot end {nameof(SegmentInfo)} of tier {tier.ToString()} that has not been started.");
|
||||
|
||||
SegmentInfo segment = pendingSegment.Value;
|
||||
segment.End = Math.Clamp(end, 0, 1);
|
||||
segments.Add(segment);
|
||||
pendingSegments[tier] = null;
|
||||
}
|
||||
|
||||
public void EndAllPendingSegments()
|
||||
{
|
||||
foreach (SegmentInfo? pendingSegment in pendingSegments)
|
||||
{
|
||||
if (pendingSegment == null)
|
||||
continue;
|
||||
|
||||
SegmentInfo finalizedSegment = pendingSegment.Value;
|
||||
finalizedSegment.End = 1;
|
||||
segments.Add(finalizedSegment);
|
||||
}
|
||||
}
|
||||
|
||||
public void Sort() =>
|
||||
segments.Sort((a, b) =>
|
||||
a.Tier != b.Tier
|
||||
? a.Tier.CompareTo(b.Tier)
|
||||
: a.Start.CompareTo(b.Start));
|
||||
|
||||
public void Add(SegmentInfo segment) => segments.Add(segment);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
segments.Clear();
|
||||
|
||||
for (int i = 0; i < pendingSegments.Length; i++)
|
||||
pendingSegments[i] = null;
|
||||
}
|
||||
|
||||
public int Count => segments.Count;
|
||||
|
||||
public void Add(int tier, float start, float end)
|
||||
{
|
||||
SegmentInfo segment = new SegmentInfo
|
||||
{
|
||||
Tier = tier,
|
||||
Start = Math.Clamp(start, 0, 1),
|
||||
End = Math.Clamp(end, 0, 1)
|
||||
};
|
||||
|
||||
if (segment.Start > segment.End)
|
||||
throw new InvalidOperationException("Segment start cannot be after segment end.");
|
||||
|
||||
Add(segment);
|
||||
}
|
||||
|
||||
public bool IsTierStarted(int tier) => tier >= 0 && pendingSegments[tier].HasValue;
|
||||
|
||||
public IEnumerator<SegmentInfo> GetEnumerator() => segments.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@ -25,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
/// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param>
|
||||
protected StatefulMenuItem(string text, Func<object, object> changeStateFunc, MenuItemType type = MenuItemType.Standard)
|
||||
protected StatefulMenuItem(LocalisableString text, Func<object, object> changeStateFunc, MenuItemType type = MenuItemType.Standard)
|
||||
: this(text, changeStateFunc, type, null)
|
||||
{
|
||||
}
|
||||
@ -37,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
/// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param>
|
||||
/// <param name="action">A delegate to be invoked when this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
protected StatefulMenuItem(string text, Func<object, object> changeStateFunc, MenuItemType type, Action<object> action)
|
||||
protected StatefulMenuItem(LocalisableString text, Func<object, object>? changeStateFunc, MenuItemType type, Action<object>? action)
|
||||
: base(text, type)
|
||||
{
|
||||
Action.Value = () =>
|
||||
@ -69,7 +68,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
/// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param>
|
||||
protected StatefulMenuItem(string text, Func<T, T> changeStateFunc, MenuItemType type = MenuItemType.Standard)
|
||||
protected StatefulMenuItem(LocalisableString text, Func<T, T>? changeStateFunc, MenuItemType type = MenuItemType.Standard)
|
||||
: this(text, changeStateFunc, type, null)
|
||||
{
|
||||
}
|
||||
@ -81,7 +80,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
/// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param>
|
||||
/// <param name="action">A delegate to be invoked when this <see cref="StatefulMenuItem"/> is pressed.</param>
|
||||
protected StatefulMenuItem(string text, Func<T, T> changeStateFunc, MenuItemType type, Action<T> action)
|
||||
protected StatefulMenuItem(LocalisableString text, Func<T, T>? changeStateFunc, MenuItemType type, Action<T>? action)
|
||||
: base(text, o => changeStateFunc?.Invoke((T)o) ?? o, type, o => action?.Invoke((T)o))
|
||||
{
|
||||
base.State.BindValueChanged(state =>
|
||||
|
@ -1,10 +1,9 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@ -18,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="type">The type of action which this <see cref="ToggleMenuItem"/> performs.</param>
|
||||
public ToggleMenuItem(string text, MenuItemType type = MenuItemType.Standard)
|
||||
public ToggleMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard)
|
||||
: this(text, type, null)
|
||||
{
|
||||
}
|
||||
@ -29,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="type">The type of action which this <see cref="ToggleMenuItem"/> performs.</param>
|
||||
/// <param name="action">A delegate to be invoked when this <see cref="ToggleMenuItem"/> is pressed.</param>
|
||||
public ToggleMenuItem(string text, MenuItemType type, Action<bool> action)
|
||||
public ToggleMenuItem(LocalisableString text, MenuItemType type, Action<bool>? action)
|
||||
: base(text, value => !value, type, action)
|
||||
{
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
@ -14,6 +15,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
@ -58,6 +60,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
this.FadeOut(fade_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back).
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
@ -68,7 +78,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
if (e.Action == GlobalAction.Back)
|
||||
{
|
||||
Hide();
|
||||
this.HidePopover();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
@ -79,8 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
Triangles.ColourTop = triangleGradientSecondColour.Value;
|
||||
Triangles.ColourBottom = BackgroundColour;
|
||||
Triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
Reference in New Issue
Block a user