diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 22ba972390..7ff463361a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -38,8 +38,8 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestButtonShowsOnCustomisableMod() { createModSelect(); + openModSelect(); - AddStep("open", () => modSelect.Show()); AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value); AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded); AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod)); @@ -58,19 +58,21 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mods still active", () => SelectedMods.Value.Count == 1); - AddStep("open", () => modSelect.Show()); + openModSelect(); AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); } [Test] - public void TestCustomisationOpensOnModSelect() + public void TestCustomisationMenuVisibility() { createModSelect(); + openModSelect(); - AddStep("open", () => modSelect.Show()); AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0); AddStep("select mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1); + AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); + AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0); } private void createModSelect() @@ -86,6 +88,12 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + private void openModSelect() + { + AddStep("open", () => modSelect.Show()); + AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded); + } + private class TestModSelectOverlay : ModSelectOverlay { public new Container ModSettingsContainer => base.ModSettingsContainer; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 05c344b199..5dc483b61c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack()); + track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack(1000)); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); @@ -48,7 +48,7 @@ namespace osu.Game.Beatmaps total_count.Value++; } - protected virtual Track GetVirtualTrack() + protected virtual Track GetVirtualTrack(double emptyLength = 0) { const double excess_length = 1000; @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps switch (lastObject) { case null: - length = excess_length; + length = emptyLength; break; case IHasEndTime endTime: diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index be9aefa359..f36079682e 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -59,9 +59,9 @@ namespace osu.Game.Graphics.Containers Track track = null; IBeatmap beatmap = null; - double currentTrackTime; - TimingControlPoint timingPoint; - EffectControlPoint effectPoint; + double currentTrackTime = 0; + TimingControlPoint timingPoint = null; + EffectControlPoint effectPoint = null; if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { @@ -69,24 +69,18 @@ namespace osu.Game.Graphics.Containers beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && track.IsRunning) + if (track != null && beatmap != null && track.IsRunning && track.Length > 0) { - currentTrackTime = track.Length > 0 ? track.CurrentTime + EarlyActivationMilliseconds : Clock.CurrentTime; + currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); - - if (timingPoint.BeatLength == 0) - { - IsBeatSyncedWithTrack = false; - return; - } - - IsBeatSyncedWithTrack = true; } - else + + IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0; + + if (timingPoint == null || !IsBeatSyncedWithTrack) { - IsBeatSyncedWithTrack = false; currentTrackTime = Clock.CurrentTime; timingPoint = defaultTiming; effectPoint = defaultEffect; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index f5b7bc3073..4abbf8db57 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -2,13 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osuTK; namespace osu.Game.Graphics.UserInterface { @@ -59,5 +64,91 @@ namespace osu.Game.Graphics.UserInterface } protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }; + + protected override Caret CreateCaret() => new OsuCaret + { + CaretWidth = CaretWidth, + SelectionColour = SelectionColour, + }; + + private class OsuCaret : Caret + { + private const float caret_move_time = 60; + + private readonly CaretBeatSyncedContainer beatSync; + + public OsuCaret() + { + RelativeSizeAxes = Axes.Y; + Size = new Vector2(1, 0.9f); + + Colour = Color4.Transparent; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Masking = true; + CornerRadius = 1; + InternalChild = beatSync = new CaretBeatSyncedContainer + { + RelativeSizeAxes = Axes.Both, + }; + } + + public override void Hide() => this.FadeOut(200); + + public float CaretWidth { get; set; } + + public Color4 SelectionColour { get; set; } + + public override void DisplayAt(Vector2 position, float? selectionWidth) + { + beatSync.HasSelection = selectionWidth != null; + + if (selectionWidth != null) + { + this.MoveTo(new Vector2(position.X, position.Y), 60, Easing.Out); + this.ResizeWidthTo(selectionWidth.Value + CaretWidth / 2, caret_move_time, Easing.Out); + this.FadeColour(SelectionColour, 200, Easing.Out); + } + else + { + this.MoveTo(new Vector2(position.X - CaretWidth / 2, position.Y), 60, Easing.Out); + this.ResizeWidthTo(CaretWidth, caret_move_time, Easing.Out); + this.FadeColour(Color4.White, 200, Easing.Out); + } + } + + private class CaretBeatSyncedContainer : BeatSyncedContainer + { + private bool hasSelection; + + public bool HasSelection + { + set + { + hasSelection = value; + if (value) + + this.FadeTo(0.5f, 200, Easing.Out); + } + } + + public CaretBeatSyncedContainer() + { + MinimumBeatLength = 300; + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + if (!hasSelection) + this.FadeTo(0.7f).FadeTo(0.4f, timingPoint.BeatLength, Easing.InOutSine); + } + } + } } }