Skip to content

Commit

Permalink
Merge pull request #2273 from andy840119/add-test-case-for-the-intera…
Browse files Browse the repository at this point in the history
…ctable-lyric

Add test case for the interactable lyric.
  • Loading branch information
andy840119 authored Aug 17, 2024
2 parents 9cfd5fd + 7f77f65 commit 5a1657c
Show file tree
Hide file tree
Showing 6 changed files with 452 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
// Copyright (c) andy840119 <[email protected]>. Licensed under the GPL Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Karaoke.Beatmaps;
using osu.Game.Rulesets.Karaoke.Configuration;
using osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;
using osu.Game.Rulesets.Karaoke.Objects;
using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;
using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;
using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;
using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;
using osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;
using osu.Game.Rulesets.Karaoke.Tests.Helper;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;

namespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Content;

public partial class TestSceneInteractableLyric : EditorClockTestScene
{
private const int border = 36;

private static readonly Lyric lyric = new()
{
Text = "カラオケ",
TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { "[0,start]:1000#^ka", "[1,start]:2000#ra", "[2,start]:3000#o", "[3,start]:4000#ke", "[3,end]:5000" }),
RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { "[0]:か", "[1]:ら", "[2]:お", "[3]:け" }),
};

[Resolved]
private OsuColour colour { get; set; } = null!;

[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new(new KaraokeBeatmap
{
BeatmapInfo =
{
Ruleset = new KaraokeRuleset().RulesetInfo,
},
});

public TestSceneInteractableLyric()
{
editorBeatmap.Add(lyric);
editorBeatmap.SelectedHitObjects.Add(lyric);
}

[Test]
public void TestGridLayer()
{
AddToggleStep("Add/remove the grid layer", value =>
{
if (value)
{
addLoader(new LayerLoader<GridLayer>
{
OnLoad = layer =>
{
layer.Spacing = 10;
},
});
}
else
{
removeLoader<GridLayer>();
}
});

AddSliderStep("Change the spacing of the grid", 0, 100, 10, value =>
{
changeLayerProperty<GridLayer>(layer =>
{
layer.Spacing = value;
});
});
}

[Test]
public void TestEditLyricLayer()
{
AddToggleStep("Add/remove the edit lyric layer", value =>
{
if (value)
{
addLoader<EditLyricLayer>();
}
else
{
removeLoader<EditLyricLayer>();
}
});
}

[Test]
public void TestTimeTagLayer()
{
AddToggleStep("Add/remove the time-tag layer", value =>
{
if (value)
{
addLoader<TimeTagLayer>();
}
else
{
removeLoader<TimeTagLayer>();
}
});
}

[Test]
public void TestCaretLayer()
{
AddToggleStep("Add/remove the caret layer", value =>
{
if (value)
{
addLoader<CaretLayer>();
}
else
{
removeLoader<CaretLayer>();
}
});

AddStep("View mode", () => switchMode(LyricEditorMode.View));
AddStep("Typing mode", () => switchMode(LyricEditorMode.EditText, TextEditStep.Typing));
AddStep("Cutting mode", () => switchMode(LyricEditorMode.EditText, TextEditStep.Split));
AddStep("Edit ruby mode", () => switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create));
AddStep("Edit time-tag mode", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Create));
AddStep("Record time-tag mode", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Recording));
}

[Test]
public void TestBlueprintLayer()
{
AddToggleStep("Add/remove the blueprint layer", value =>
{
if (value)
{
addLoader<BlueprintLayer>();
}
else
{
removeLoader<BlueprintLayer>();
}
});

AddStep("Ruby blueprint", () =>
{
switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create);
switchEditRubyModeState(RubyTagEditMode.Modify);
});
AddStep("Time-tag adjust blueprint", () =>
{
switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Adjust);
});
}

[BackgroundDependencyLoader]
private void load()
{
Dependencies.CacheAs<ILyricsProvider>(new LyricsProvider().With(Add));
Dependencies.CacheAs<ILyricsChangeHandler>(new LyricsChangeHandler().With(Add));
Dependencies.CacheAs<ILyricTextChangeHandler>(new LyricTextChangeHandler().With(Add));
Dependencies.CacheAs<ILyricRubyTagsChangeHandler>(new LyricRubyTagsChangeHandler().With(Add));
Dependencies.CacheAs<ILyricTimeTagsChangeHandler>(new LyricTimeTagsChangeHandler().With(Add));
Dependencies.Cache(new KaraokeRulesetLyricEditorConfigManager());
}

#region Testing tools

private void switchMode(LyricEditorMode mode, Enum? step = null)
{
var editorState = this.ChildrenOfType<ILyricEditorState>().First();
editorState.SwitchMode(mode);

if (step != null)
{
editorState.SwitchEditStep(step);
}
}

private void switchEditRubyModeState(RubyTagEditMode mode)
{
var editRubyModeState = this.ChildrenOfType<IEditRubyModeState>().First();
editRubyModeState.BindableRubyTagEditMode.Value = mode;
}

private readonly List<LayerLoader> loaders = new()
{
// note: lyric layer should always in the loader and never be removed.
new LayerLoader<LyricLayer>
{
OnLoad = layer =>
{
layer.LyricPosition = new Vector2(border);
},
},
};

private LayerLoader? getLoader<TLayer>()
=> loaders.FirstOrDefault(l => l.GetType().GenericTypeArguments.First() == typeof(TLayer));

private void addLoader<TLayer>(bool reloadView = true) where TLayer : Layer
{
addLoader(new LayerLoader<TLayer>(), reloadView);
}

private void addLoader<TLayer>(LayerLoader<TLayer> instance, bool reloadView = true) where TLayer : Layer
{
if (getLoader<TLayer>() != null)
{
return;
}

loaders.Add(instance);

if (reloadView)
{
updateInteractableLyric();
}
}

private void removeLoader<TLoader>(bool reloadView = true)
{
var loader = getLoader<TLoader>();

if (loader != null)
{
loaders.Remove(loader);
}

if (reloadView)
{
updateInteractableLyric();
}
}

private void changeLayerProperty<TLayer>(Action<TLayer> action, bool reloadView = true) where TLayer : Layer
{
if (getLoader<TLayer>() == null)
{
return;
}

removeLoader<TLayer>(false);
addLoader(new LayerLoader<TLayer>
{
OnLoad = action,
}, false);

if (reloadView)
{
updateInteractableLyric();
}
}

private void updateInteractableLyric()
{
RemoveAll(x => x is PopoverContainer, true);
Add(new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scale = new Vector2(2),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colour.BlueDark,
},
new MockLyricEditorState
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(48),
Child = createInteractableLyric(loaders.ToArray()),
},
},
});
}

private static InteractableLyric createInteractableLyric(LayerLoader[] loaders)
{
return new InteractableLyric(lyric)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
TextSizeChanged = (self, size) =>
{
self.Width = size.X + border * 2;
self.Height = size.Y + border * 2;
},
Loaders = reorderLoaders(loaders),
};
}

private static LayerLoader[] reorderLoaders(LayerLoader[] loaders)
{
// follow how LyricEditor to change the sort from input loaders
return loaders.OrderBy(x =>
{
var type = x.GetType().GenericTypeArguments.First();
return type.Name switch
{
nameof(GridLayer) => 0,
nameof(LyricLayer) => 1,
nameof(EditLyricLayer) => 2,
nameof(TimeTagLayer) => 3,
nameof(CaretLayer) => 4,
nameof(BlueprintLayer) => 5,
_ => throw new InvalidOperationException(),
};
}).ToArray();
}

#endregion

/// <summary>
/// Follow <see cref="LyricEditor"/> to create the component with necessary DI.
/// </summary>
[Cached(typeof(ILyricEditorState))]
private partial class MockLyricEditorState : Container, ILyricEditorState
{
private readonly Bindable<LyricEditorMode> bindableMode = new();
private readonly Bindable<EditorModeWithEditStep> bindableModeWithEditStep = new();

public IBindable<LyricEditorMode> BindableMode => bindableMode;

public IBindable<EditorModeWithEditStep> BindableModeWithEditStep => bindableModeWithEditStep;
public LyricEditorMode Mode => LyricEditorMode.View;

[Cached]
private readonly LyricEditorColourProvider colourProvider = new();

[Cached(typeof(ILyricCaretState))]
private readonly LyricCaretState lyricCaretState = new();

[Cached(typeof(IEditRubyModeState))]
private readonly EditRubyModeState editRubyModeState = new();

[Cached(typeof(IEditTimeTagModeState))]
private readonly EditTimeTagModeState editTimeTagModeState = new();

/// <summary>
/// Add the DI into children here for prevent child is removed if call Children = [...] outside.
/// </summary>
protected override void LoadComplete()
{
base.LoadComplete();

// global state
AddInternal(lyricCaretState);

// state for target mode only.
AddInternal(editRubyModeState);
AddInternal(editTimeTagModeState);
}

public void SwitchMode(LyricEditorMode mode)
=> bindableMode.Value = mode;

public void SwitchEditStep<TEditStep>(TEditStep editStep) where TEditStep : Enum
{
bindableModeWithEditStep.Value = new EditorModeWithEditStep
{
Mode = bindableMode.Value,
EditStep = editStep,
Default = false,
};
}

public void NavigateToFix(LyricEditorMode mode)
=> bindableMode.Value = mode;
}
}
Loading

0 comments on commit 5a1657c

Please sign in to comment.