diff --git a/osu.Android.props b/osu.Android.props index b24493665e..6744590f0d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -63,6 +63,6 @@ - + diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index ddf708d0f1..2d940479f3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs new file mode 100644 index 0000000000..62b5ecfd58 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -0,0 +1,92 @@ +// 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 osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModSpinIn : Mod, IApplicableToDrawableHitObjects, IReadFromConfig + { + public override string Name => "Spin In"; + public override string Acronym => "SI"; + public override IconUsage Icon => FontAwesome.Solid.Undo; + public override ModType Type => ModType.Fun; + public override string Description => "Circles spin in. No approach circles."; + public override double ScoreMultiplier => 1; + + // todo: this mod should be able to be compatible with hidden with a bit of further implementation. + public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden) }; + + private const int rotate_offset = 360; + private const float rotate_starting_width = 2; + + private Bindable increaseFirstObjectVisibility = new Bindable(); + + public void ReadFromConfig(OsuConfigManager config) + { + increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) + { + switch (drawable) + { + case DrawableSpinner _: + continue; + + default: + drawable.ApplyCustomUpdateState += applyZoomState; + break; + } + } + } + + private void applyZoomState(DrawableHitObject drawable, ArmedState state) + { + var h = (OsuHitObject)drawable.HitObject; + + switch (drawable) + { + case DrawableHitCircle circle: + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + { + circle.ApproachCircle.Hide(); + + circle.RotateTo(rotate_offset).Then().RotateTo(0, h.TimePreempt, Easing.InOutSine); + circle.ScaleTo(new Vector2(rotate_starting_width, 0)).Then().ScaleTo(1, h.TimePreempt, Easing.InOutSine); + + // bypass fade in. + if (state == ArmedState.Idle) + circle.FadeIn(); + } + + break; + + case DrawableSlider slider: + using (slider.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + { + slider.ScaleTo(0).Then().ScaleTo(1, h.TimePreempt, Easing.InOutSine); + + // bypass fade in. + if (state == ArmedState.Idle) + slider.FadeIn(); + } + + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs index ad6a15718a..e926ade41b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs @@ -1,6 +1,7 @@ // 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 osu.Framework.Bindables; @@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods private Bindable increaseFirstObjectVisibility = new Bindable(); + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; + public void ReadFromConfig(OsuConfigManager config) { increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); @@ -64,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSlider _: case DrawableHitCircle _: { - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); break; } @@ -75,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableHitCircle circle: // we don't want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) circle.ApproachCircle.Hide(); break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index aacf3ee08d..a2a23e9ff7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Anchor = Anchor.Centre, Alpha = 0.5f, } - }, restrictSize: false); + }, confineMode: ConfineMode.NoScaling); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 1e2c0ae59f..2e6e7e03ac 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.ChevronRight - }, restrictSize: false) + }) }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index f5f92dd05d..c8062b67b5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { - Size = new Vector2(16) * sliderTick.Scale; + Size = new Vector2(16 * sliderTick.Scale); Origin = Anchor.Centre; InternalChildren = new Drawable[] @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Colour = AccentColour.Value, Alpha = 0.3f, } - }, restrictSize: false) + }) }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index 84034d3ee9..e8dc63abca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, - }, restrictSize: false) + }, confineMode: ConfineMode.NoScaling) { Text = @"1" } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 97c7c9cec5..6bc19ee3b5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -1,6 +1,7 @@ // 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.Containers; using osu.Framework.Graphics.Lines; @@ -75,22 +76,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected SliderBody() { - InternalChild = path = new SliderPath(); + RecyclePath(); } /// /// Initialises a new , releasing all resources retained by the old one. /// - public void RecyclePath() + public virtual void RecyclePath() { InternalChild = path = new SliderPath { - Position = path.Position, - PathRadius = path.PathRadius, - AccentColour = path.AccentColour, - BorderColour = path.BorderColour, - BorderSize = path.BorderSize, - Vertices = path.Vertices + Position = path?.Position ?? Vector2.Zero, + PathRadius = path?.PathRadius ?? 10, + AccentColour = path?.AccentColour ?? Color4.White, + BorderColour = path?.BorderColour ?? Color4.White, + BorderSize = path?.BorderSize ?? DEFAULT_BORDER_SIZE, + Vertices = path?.Vertices ?? Array.Empty() }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index 73b184bffe..a3d3893c8b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -78,9 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces slider.Path.GetPathToProgress(CurrentCurve, 0, 1); SetVertices(CurrentCurve); - // The body is sized to the full path size to avoid excessive autosize computations + // Force the body to be the final path size to avoid excessive autosize computations + Path.AutoSizeAxes = Axes.Both; Size = Path.Size; + updatePathSize(); + snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); @@ -93,6 +97,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces setRange(lastSnakedStart, lastSnakedEnd); } + public override void RecyclePath() + { + base.RecyclePath(); + updatePathSize(); + } + + private void updatePathSize() + { + // Force the path to its final size to avoid excessive framebuffer resizes + Path.AutoSizeAxes = Axes.None; + Path.Size = Size; + } + private void setRange(double p0, double p1) { if (p0 > p1) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8df0f77629..7f45fbe1dd 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -134,6 +134,7 @@ namespace osu.Game.Rulesets.Osu { new OsuModTransform(), new OsuModWiggle(), + new OsuModSpinIn(), new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new ModWindUp(), new ModWindDown()), }; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 27546fa424..b3492a2b31 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }, }, } - }, restrictSize: false) + }) { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs deleted file mode 100644 index c7a0df6e9f..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs +++ /dev/null @@ -1,145 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Textures; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Skinning; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public class TestSceneSkinReloadable : OsuTestScene - { - [Test] - public void TestInitialLoad() - { - var secondarySource = new SecondarySource(); - SkinConsumer consumer = null; - - AddStep("setup layout", () => - { - Child = new SkinSourceContainer - { - RelativeSizeAxes = Axes.Both, - Child = new LocalSkinOverrideContainer(secondarySource) - { - RelativeSizeAxes = Axes.Both, - Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true) - } - }; - }); - - AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); - AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); - } - - [Test] - public void TestOverride() - { - var secondarySource = new SecondarySource(); - - SkinConsumer consumer = null; - Container target = null; - - AddStep("setup layout", () => - { - Child = new SkinSourceContainer - { - RelativeSizeAxes = Axes.Both, - Child = target = new LocalSkinOverrideContainer(secondarySource) - { - RelativeSizeAxes = Axes.Both, - } - }; - }); - - AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); - AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); - AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); - } - - private class NamedBox : Container - { - public NamedBox(string name) - { - Children = new Drawable[] - { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Font = OsuFont.Default.With(size: 40), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = name - } - }; - } - } - - private class SkinConsumer : SkinnableDrawable - { - public new Drawable Drawable => base.Drawable; - public int SkinChangedCount { get; private set; } - - public SkinConsumer(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) - { - } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - SkinChangedCount++; - } - } - - private class BaseSourceBox : NamedBox - { - public BaseSourceBox() - : base("Base Source") - { - } - } - - private class SecondarySourceBox : NamedBox - { - public SecondarySourceBox() - : base("Secondary Source") - { - } - } - - private class SecondarySource : ISkin - { - public Drawable GetDrawableComponent(string componentName) => new SecondarySourceBox(); - - public Texture GetTexture(string componentName) => throw new NotImplementedException(); - - public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); - - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); - } - - private class SkinSourceContainer : Container, ISkin - { - public Drawable GetDrawableComponent(string componentName) => new BaseSourceBox(); - - public Texture GetTexture(string componentName) => throw new NotImplementedException(); - - public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); - - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs new file mode 100644 index 0000000000..0b5978e3eb --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -0,0 +1,283 @@ +// 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.Globalization; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableDrawable : OsuTestScene + { + [Test] + public void TestConfineScaleDown() + { + FillFlowContainer fill = null; + + AddStep("setup layout larger source", () => + { + Child = new LocalSkinOverrideContainer(new SizedSource(50)) + { + RelativeSizeAxes = Axes.Both, + Child = fill = new FillFlowContainer + { + Size = new Vector2(30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10), + Children = new[] + { + new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) + } + }, + }; + }); + + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddStep("adjust scale", () => fill.Scale = new Vector2(2)); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + } + + [Test] + public void TestConfineScaleUp() + { + FillFlowContainer fill = null; + + AddStep("setup layout larger source", () => + { + Child = new LocalSkinOverrideContainer(new SizedSource(30)) + { + RelativeSizeAxes = Axes.Both, + Child = fill = new FillFlowContainer + { + Size = new Vector2(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10), + Children = new[] + { + new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) + } + }, + }; + }); + + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddStep("adjust scale", () => fill.Scale = new Vector2(2)); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + } + + [Test] + public void TestInitialLoad() + { + var secondarySource = new SecondarySource(); + SkinConsumer consumer = null; + + AddStep("setup layout", () => + { + Child = new SkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = new LocalSkinOverrideContainer(secondarySource) + { + RelativeSizeAxes = Axes.Both, + Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true) + } + }; + }); + + AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); + AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); + } + + [Test] + public void TestOverride() + { + var secondarySource = new SecondarySource(); + + SkinConsumer consumer = null; + Container target = null; + + AddStep("setup layout", () => + { + Child = new SkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = target = new LocalSkinOverrideContainer(secondarySource) + { + RelativeSizeAxes = Axes.Both, + } + }; + }); + + AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); + AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); + AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); + } + + private class ExposedSkinnableDrawable : SkinnableDrawable + { + public new Drawable Drawable => base.Drawable; + + public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, defaultImplementation, allowFallback, confineMode) + { + } + } + + private class DefaultBox : DrawWidthBox + { + public DefaultBox() + { + RelativeSizeAxes = Axes.Both; + } + } + + private class DrawWidthBox : Container + { + private readonly OsuSpriteText text; + + public DrawWidthBox() + { + Children = new Drawable[] + { + new Box + { + Colour = Color4.Gray, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + text.Text = DrawWidth.ToString(CultureInfo.InvariantCulture); + } + } + + private class NamedBox : Container + { + public NamedBox(string name) + { + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Font = OsuFont.Default.With(size: 40), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = name + } + }; + } + } + + private class SkinConsumer : SkinnableDrawable + { + public new Drawable Drawable => base.Drawable; + public int SkinChangedCount { get; private set; } + + public SkinConsumer(string name, Func defaultImplementation, Func allowFallback = null) + : base(name, defaultImplementation, allowFallback) + { + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + SkinChangedCount++; + } + } + + private class BaseSourceBox : NamedBox + { + public BaseSourceBox() + : base("Base Source") + { + } + } + + private class SecondarySourceBox : NamedBox + { + public SecondarySourceBox() + : base("Secondary Source") + { + } + } + + private class SizedSource : ISkin + { + private readonly float size; + + public SizedSource(float size) + { + this.size = size; + } + + public Drawable GetDrawableComponent(string componentName) => + componentName == "available" + ? new DrawWidthBox + { + Colour = Color4.Yellow, + Size = new Vector2(size) + } + : null; + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + + private class SecondarySource : ISkin + { + public Drawable GetDrawableComponent(string componentName) => new SecondarySourceBox(); + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + + private class SkinSourceContainer : Container, ISkin + { + public Drawable GetDrawableComponent(string componentName) => new BaseSourceBox(); + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 4d3992ce13..9196513a55 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; - private readonly Channel channel1 = new Channel(new User()) { Name = "test1" }; + private readonly Channel channel1 = new Channel(new User()) { Name = "test really long username" }; private readonly Channel channel2 = new Channel(new User()) { Name = "test2" }; [SetUp] diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 46d4cfa98c..d32c0d6156 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Screens.MapPool mapFlows = new FillFlowContainer> { Y = 100, - Spacing = new Vector2(10, 20), - Padding = new MarginPadding(50), + Spacing = new Vector2(10, 10), + Padding = new MarginPadding(25), Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, }, @@ -218,7 +218,7 @@ namespace osu.Game.Tournament.Screens.MapPool { mapFlows.Add(currentFlow = new FillFlowContainer { - Spacing = new Vector2(10, 20), + Spacing = new Vector2(10, 5), Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 65efcaa949..166ba5111c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -386,7 +386,7 @@ namespace osu.Game.Beatmaps beatmap.OnlineBeatmapID = res.OnlineBeatmapID; }; - req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap}", e); }; + req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); }; // intentionally blocking to limit web request concurrency req.Perform(api); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ed65bdc069..efb76deff8 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -253,7 +253,7 @@ namespace osu.Game.Database using (Stream s = reader.GetStream(file)) s.CopyTo(hashable); - return hashable.ComputeSHA2Hash(); + return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : null; } /// diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 757a9a349c..714e953816 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -76,7 +76,12 @@ namespace osu.Game.Graphics.UserInterface { Masking = true, RelativeSizeAxes = Axes.Both, - Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathRadius = 1 } + Child = path = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 1 + } }); } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index 8134cfb42d..d158186899 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface Direction = FillDirection.Horizontal, Children = new Drawable[] { - text = new OsuSpriteText { Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) }, + text = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) }, icon = new SpriteIcon { Size = new Vector2(14), @@ -84,7 +84,11 @@ namespace osu.Game.Graphics.UserInterface } }; - Current.ValueChanged += selected => { icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; }; + Current.ValueChanged += selected => + { + icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; + text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium); + }; } [BackgroundDependencyLoader] diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 41b67f343a..ceaf0c3d5e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -589,7 +589,7 @@ namespace osu.Game { int recentLogCount = 0; - const double debounce = 5000; + const double debounce = 60000; Logger.NewEntry += entry => { diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index 7386bffb1a..d5d9a6c2ce 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -13,6 +13,8 @@ namespace osu.Game.Overlays.Chat.Tabs public override bool IsSwitchable => false; + protected override bool IsBoldWhenActive => false; + public ChannelSelectorTabItem() : base(new ChannelSelectorTabChannel()) { @@ -22,7 +24,7 @@ namespace osu.Game.Overlays.Chat.Tabs Icon.Alpha = 0; Text.Font = Text.Font.With(size: 45); - TextBold.Font = Text.Font.With(size: 45); + Text.Truncate = false; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs index 7f820e4ff7..266e68f17e 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays.Chat.Tabs { @@ -28,7 +29,6 @@ namespace osu.Game.Overlays.Chat.Tabs public override bool IsRemovable => !Pinned; protected readonly SpriteText Text; - protected readonly SpriteText TextBold; protected readonly ClickableContainer CloseButton; private readonly Box box; private readonly Box highlightBox; @@ -87,20 +87,17 @@ namespace osu.Game.Overlays.Chat.Tabs }, Text = new OsuSpriteText { - Margin = new MarginPadding(5), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Text = value.ToString(), - Font = OsuFont.GetFont(size: 18) - }, - TextBold = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(5), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Text = value.ToString(), - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 18), + Padding = new MarginPadding(5) + { + Left = LeftTextPadding, + Right = RightTextPadding, + }, + RelativeSizeAxes = Axes.X, + Truncate = true, }, CloseButton = new TabCloseButton { @@ -118,10 +115,16 @@ namespace osu.Game.Overlays.Chat.Tabs }; } + protected virtual float LeftTextPadding => 5; + + protected virtual float RightTextPadding => IsRemovable ? 40 : 5; + protected virtual IconUsage DisplayIcon => FontAwesome.Solid.Hashtag; protected virtual bool ShowCloseOnHover => true; + protected virtual bool IsBoldWhenActive => true; + protected override bool OnHover(HoverEvent e) { if (IsRemovable && ShowCloseOnHover) @@ -138,6 +141,19 @@ namespace osu.Game.Overlays.Chat.Tabs updateState(); } + protected override bool OnMouseUp(MouseUpEvent e) + { + switch (e.Button) + { + case MouseButton.Middle: + CloseButton.Click(); + return true; + + default: + return false; + } + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -189,8 +205,7 @@ namespace osu.Game.Overlays.Chat.Tabs box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint); highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - Text.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - TextBold.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + if (IsBoldWhenActive) Text.Font = Text.Font.With(weight: FontWeight.Bold); } protected virtual void FadeInactive() @@ -202,8 +217,7 @@ namespace osu.Game.Overlays.Chat.Tabs box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - Text.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - TextBold.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.Medium); } protected override void OnActivated() => updateState(); diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 9e87bae864..1413b8fe78 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -62,11 +62,10 @@ namespace osu.Game.Overlays.Chat.Tabs }); avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); - - Text.X = ChatOverlay.TAB_AREA_HEIGHT; - TextBold.X = ChatOverlay.TAB_AREA_HEIGHT; } + protected override float LeftTextPadding => base.LeftTextPadding + ChatOverlay.TAB_AREA_HEIGHT; + protected override bool ShowCloseOnHover => false; protected override void FadeActive() diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 2150726a42..61c2644c6f 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Judgements Font = OsuFont.Numeric.With(size: 12), Colour = judgementColour(Result.Type), Scale = new Vector2(0.85f, 1), - }, restrictSize: false) + }) }; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1d9d885527..181ae37a8b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -147,12 +147,6 @@ namespace osu.Game.Rulesets.Objects.Drawables if (State.Value == newState && !force) return; - // apply any custom state overrides - ApplyCustomUpdateState?.Invoke(this, newState); - - if (newState == ArmedState.Hit) - PlaySamples(); - if (UseTransformStateManagement) { double transformTime = HitObject.StartTime - InitialLifetimeOffset; @@ -177,6 +171,12 @@ namespace osu.Game.Rulesets.Objects.Drawables state.Value = newState; UpdateState(newState); + + // apply any custom state overrides + ApplyCustomUpdateState?.Invoke(this, newState); + + if (newState == ArmedState.Hit) + PlaySamples(); } /// diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 9ffd620e55..90806bab6e 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Multi public override bool OnExiting(IScreen next) { - if (!(screenStack.CurrentScreen is LoungeSubScreen)) + if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) { screenStack.Exit(); return true; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index d390787090..2b401778a6 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -19,11 +19,11 @@ namespace osu.Game.Screens.Play private const float remaining_time_container_max_size = 0.3f; private const int vertical_margin = 25; - private List breaks; - private readonly Container fadeContainer; - public List Breaks + private IReadOnlyList breaks; + + public IReadOnlyList Breaks { get => breaks; set diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 16354534f4..cf88b697d9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -26,6 +26,9 @@ namespace osu.Game.Screens.Select { public class BeatmapCarousel : OsuScrollContainer { + private const float bleed_top = FilterControl.HEIGHT; + private const float bleed_bottom = Footer.HEIGHT; + /// /// Triggered when the loaded change and are completely loaded. /// @@ -81,7 +84,8 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); scrollPositionCache.Invalidate(); - Schedule(() => + // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run. + SchedulerAfterChildren.Add(() => { BeatmapSetsChanged?.Invoke(); BeatmapSetsLoaded = true; @@ -129,19 +133,16 @@ namespace osu.Game.Screens.Select loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); } - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - Schedule(() => - { - var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); + var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); - if (existingSet == null) - return; + if (existingSet == null) + return; - root.RemoveChild(existingSet); - itemsCache.Invalidate(); - }); - } + root.RemoveChild(existingSet); + itemsCache.Invalidate(); + }); public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { @@ -338,6 +339,25 @@ namespace osu.Game.Screens.Select public bool AllowSelection = true; + /// + /// Half the height of the visible content. + /// + /// This is different from the height of , since + /// the beatmap carousel bleeds into the and the + /// + /// + private float visibleHalfHeight => (DrawHeight + bleed_bottom + bleed_top) / 2; + + /// + /// The position of the lower visible bound with respect to the current scroll position. + /// + private float visibleBottomBound => Current + DrawHeight + bleed_bottom; + + /// + /// The position of the upper visible bound with respect to the current scroll position. + /// + private float visibleUpperBound => Current - bleed_top; + public void FlushPendingFilterOperations() { if (PendingFilter?.Completed == false) @@ -414,6 +434,8 @@ namespace osu.Game.Screens.Select return true; } + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos); + protected override void Update() { base.Update(); @@ -424,17 +446,15 @@ namespace osu.Game.Screens.Select if (!scrollPositionCache.IsValid) updateScrollPosition(); - float drawHeight = DrawHeight; - // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y < Current - p.DrawHeight || p.Y > Current + drawHeight || !p.IsPresent); + scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); // Find index range of all items that should be on-screen Trace.Assert(Items.Count == yPositions.Count); - int firstIndex = yPositions.BinarySearch(Current - DrawableCarouselItem.MAX_HEIGHT); + int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = yPositions.BinarySearch(Current + drawHeight); + int lastIndex = yPositions.BinarySearch(visibleBottomBound); if (lastIndex < 0) lastIndex = ~lastIndex; int notVisibleCount = 0; @@ -486,9 +506,8 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). - float halfHeight = drawHeight / 2; foreach (DrawableCarouselItem p in scrollableContent.Children) - updateItem(p, halfHeight); + updateItem(p); } protected override void Dispose(bool isDisposing) @@ -542,7 +561,7 @@ namespace osu.Game.Screens.Select yPositions.Clear(); - float currentY = DrawHeight / 2; + float currentY = visibleHalfHeight; DrawableCarouselBeatmapSet lastSet = null; scrollTarget = null; @@ -575,7 +594,6 @@ namespace osu.Game.Screens.Select float? setY = null; if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override. - // ReSharper disable once PossibleNullReferenceException (resharper broken?) setY = lastSet.Y + lastSet.DrawHeight + 5; if (d.IsLoaded) @@ -596,7 +614,7 @@ namespace osu.Game.Screens.Select currentY += d.DrawHeight + 5; } - currentY += DrawHeight / 2; + currentY += visibleHalfHeight; scrollableContent.Height = currentY; if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) @@ -637,18 +655,15 @@ namespace osu.Game.Screens.Select /// the current scroll position. /// /// The item to be updated. - /// Half the draw height of the carousel container. - private void updateItem(DrawableCarouselItem p, float halfHeight) + private void updateItem(DrawableCarouselItem p) { - var height = p.IsPresent ? p.DrawHeight : 0; - - float itemDrawY = p.Position.Y - Current + height / 2; - float dist = Math.Abs(1f - itemDrawY / halfHeight); + float itemDrawY = p.Position.Y - visibleUpperBound + p.DrawHeight / 2; + float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); // Setting the origin position serves as an additive position on top of potential // local transformation we may want to apply (e.g. when a item gets selected, we // may want to smoothly transform it leftwards.) - p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); + p.OriginPosition = new Vector2(-offsetX(dist, visibleHalfHeight), 0); // We are applying a multiplicative alpha (which is internally done by nesting an // additional container and setting that container's alpha) such that we can diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index f66cd2b29a..7f82d3cc12 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -25,22 +25,6 @@ namespace osu.Game.Screens.Select private Bindable selectedTab; - private void invokeOnFilter() - { - OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colour, OsuConfigManager config) - { - modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight; - - selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab); - - tabs.Current.BindTo(selectedTab); - tabs.Current.TriggerChange(); - } - public BeatmapDetailAreaTabControl() { Height = HEIGHT; @@ -66,12 +50,31 @@ namespace osu.Game.Screens.Select Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Text = @"Mods", + Alpha = 0, }, }; tabs.Current.ValueChanged += _ => invokeOnFilter(); modsCheckbox.Current.ValueChanged += _ => invokeOnFilter(); } + + [BackgroundDependencyLoader] + private void load(OsuColour colour, OsuConfigManager config) + { + modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight; + + selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab); + + tabs.Current.BindTo(selectedTab); + tabs.Current.TriggerChange(); + } + + private void invokeOnFilter() + { + OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value); + + modsCheckbox.FadeTo(tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 1, 200, Easing.OutQuint); + } } public enum BeatmapDetailTab diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 57fe15fd99..84e8e90f54 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select.Filter; using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -22,6 +21,8 @@ namespace osu.Game.Screens.Select { public class FilterControl : Container { + public const float HEIGHT = 100; + public Action FilterChanged; private readonly OsuTabControl sortTabs; @@ -187,11 +188,5 @@ namespace osu.Game.Screens.Select } private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); - - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnMouseMove(MouseMoveEvent e) => true; - - protected override bool OnClick(ClickEvent e) => true; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 20dbcf2693..7dd934f91a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Select Size = new Vector2(wedged_container_size.X, 1), Padding = new MarginPadding { - Bottom = 50, + Bottom = Footer.HEIGHT, Top = wedged_container_size.Y + left_area_padding, Left = left_area_padding, Right = left_area_padding * 2, @@ -147,20 +147,29 @@ namespace osu.Game.Screens.Select Width = 0.5f, Children = new Drawable[] { - Carousel = new BeatmapCarousel + new Container { - Masking = false, RelativeSizeAxes = Axes.Both, - Size = new Vector2(1 - wedged_container_size.X, 1), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - SelectionChanged = updateSelectedBeatmap, - BeatmapSetsChanged = carouselBeatmapsLoaded, + Padding = new MarginPadding + { + Top = FilterControl.HEIGHT, + Bottom = Footer.HEIGHT + }, + Child = Carousel = new BeatmapCarousel + { + Masking = false, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1 - wedged_container_size.X, 1), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + SelectionChanged = updateSelectedBeatmap, + BeatmapSetsChanged = carouselBeatmapsLoaded, + }, }, FilterControl = new FilterControl { RelativeSizeAxes = Axes.X, - Height = 100, + Height = FilterControl.HEIGHT, FilterChanged = c => Carousel.Filter(c), Background = { Width = 2 }, Exit = () => diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 995cb15136..eb0508b568 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Caching; using osu.Framework.Graphics; using osuTK; @@ -9,8 +10,8 @@ namespace osu.Game.Skinning { public class SkinnableDrawable : SkinnableDrawable { - public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, defaultImplementation, allowFallback, confineMode) { } } @@ -29,7 +30,7 @@ namespace osu.Game.Skinning private readonly string componentName; - private readonly bool restrictSize; + private readonly ConfineMode confineMode; /// /// Create a new skinnable drawable. @@ -37,28 +38,32 @@ namespace osu.Game.Skinning /// The namespace-complete resource name for this skinnable element. /// A function to create the default skin implementation of this element. /// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present. - /// Whether a user-skin drawable should be limited to the size of our parent. - public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : this(name, allowFallback, restrictSize) + /// How (if at all) the should be resize to fit within our own bounds. + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : this(name, allowFallback, confineMode) { createDefault = defaultImplementation; } - protected SkinnableDrawable(string name, Func allowFallback = null, bool restrictSize = true) + protected SkinnableDrawable(string name, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) : base(allowFallback) { componentName = name; - this.restrictSize = restrictSize; + this.confineMode = confineMode; RelativeSizeAxes = Axes.Both; } private readonly Func createDefault; + private readonly Cached scaling = new Cached(); + + private bool isDefault; + protected virtual T CreateDefault(string name) => createDefault(name); /// - /// Whether to apply size restrictions (specified via ) to the default implementation. + /// Whether to apply size restrictions (specified via ) to the default implementation. /// protected virtual bool ApplySizeRestrictionsToDefault => false; @@ -66,7 +71,7 @@ namespace osu.Game.Skinning { Drawable = skin.GetDrawableComponent(componentName); - bool isDefault = false; + isDefault = false; if (Drawable == null && allowFallback) { @@ -76,21 +81,57 @@ namespace osu.Game.Skinning if (Drawable != null) { - if (restrictSize && (!isDefault || ApplySizeRestrictionsToDefault)) - { - Drawable.RelativeSizeAxes = Axes.Both; - Drawable.Size = Vector2.One; - Drawable.Scale = Vector2.One; - Drawable.FillMode = FillMode.Fit; - } - + scaling.Invalidate(); Drawable.Origin = Anchor.Centre; Drawable.Anchor = Anchor.Centre; - InternalChild = Drawable; } else ClearInternal(); } + + protected override void Update() + { + base.Update(); + + if (!scaling.IsValid) + { + try + { + if (Drawable == null || (isDefault && !ApplySizeRestrictionsToDefault)) return; + + switch (confineMode) + { + case ConfineMode.NoScaling: + return; + + case ConfineMode.ScaleDownToFit: + if (Drawable.DrawSize.X <= DrawSize.X && Drawable.DrawSize.Y <= DrawSize.Y) + return; + + break; + } + + Drawable.RelativeSizeAxes = Axes.Both; + Drawable.Size = Vector2.One; + Drawable.Scale = Vector2.One; + Drawable.FillMode = FillMode.Fit; + } + finally + { + scaling.Validate(); + } + } + } + } + + public enum ConfineMode + { + /// + /// Don't apply any scaling. This allows the user element to be of any size, exceeding specified bounds. + /// + NoScaling, + ScaleDownToFit, + ScaleToFit, } } diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index ceb1ed0f70..1716ec71de 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -18,8 +18,8 @@ namespace osu.Game.Skinning [Resolved] private TextureStore textures { get; set; } - public SkinnableSprite(string name, Func allowFallback = null, bool restrictSize = true) - : base(name, allowFallback, restrictSize) + public SkinnableSprite(string name, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, allowFallback, confineMode) { } diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs index 36e646d743..d12a6f74f7 100644 --- a/osu.Game/Skinning/SkinnableSpriteText.cs +++ b/osu.Game/Skinning/SkinnableSpriteText.cs @@ -8,8 +8,8 @@ namespace osu.Game.Skinning { public class SkinnableSpriteText : SkinnableDrawable, IHasText { - public SkinnableSpriteText(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) + public SkinnableSpriteText(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, defaultImplementation, allowFallback, confineMode) { } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c05cc6f9dd..0b2baa982b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -15,7 +15,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3b18039600..55d1afa645 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -105,8 +105,8 @@ - - + + diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs index 058e246ed8..9ef21e014c 100644 --- a/osu.iOS/AppDelegate.cs +++ b/osu.iOS/AppDelegate.cs @@ -1,15 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using Foundation; using osu.Framework.iOS; using osu.Game; +using UIKit; namespace osu.iOS { [Register("AppDelegate")] public class AppDelegate : GameAppDelegate { - protected override Framework.Game CreateGame() => new OsuGameIOS(); + private OsuGameIOS game; + + protected override Framework.Game CreateGame() => game = new OsuGameIOS(); + + public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation) + { + Task.Run(() => game.Import(url.Path)); + return true; + } } } diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index d7992353cf..0775d1522d 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -40,5 +40,70 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UTExportedTypeDeclarations + + + UTTypeConformsTo + + + + UTTypeIdentifier + sh.ppy.osu.items + UTTypeTagSpecification + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osr + UTTypeTagSpecification + + public.filename-extension + osr + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osk + UTTypeTagSpecification + + public.filename-extension + osk + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osz + UTTypeTagSpecification + + public.filename-extension + osz + + + + CFBundleDocumentTypes + + + LSHandlerRank + Owner + CFBundleTypeName + Supported osu! files + LSItemContentTypes + + sh.ppy.osu.items + + +