// 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 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.Localisation; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Extensions; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public partial class EffectPointVisualisation : CompositeDrawable, IControlPointVisualisation { private readonly EffectControlPoint effect; private Bindable<bool> kiai = null!; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; public EffectPointVisualisation(EffectControlPoint point) { RelativePositionAxes = Axes.Both; RelativeSizeAxes = Axes.Y; effect = point; } [BackgroundDependencyLoader] private void load() { kiai = effect.KiaiModeBindable.GetBoundCopy(); kiai.BindValueChanged(_ => refreshDisplay(), true); } private EffectControlPoint? nextControlPoint; protected override void LoadComplete() { base.LoadComplete(); // Due to the limitations of ControlPointInfo, it's impossible to know via event flow when the next kiai point has changed. // This is due to the fact that an EffectPoint can be added to an existing group. We would need to bind to ItemAdded on *every* // future group to track this. // // I foresee this being a potential performance issue on beatmaps with many control points, so let's limit how often we check // for changes. ControlPointInfo needs a refactor to make this flow better, but it should do for now. Scheduler.AddDelayed(() => { EffectControlPoint? next = null; for (int i = 0; i < beatmap.ControlPointInfo.EffectPoints.Count; i++) { var point = beatmap.ControlPointInfo.EffectPoints[i]; if (point.Time > effect.Time) { next = point; break; } } if (!ReferenceEquals(nextControlPoint, next)) { nextControlPoint = next; refreshDisplay(); } }, 100, true); } private void refreshDisplay() { ClearInternal(); AddInternal(new ControlPointVisualisation(effect)); if (!kiai.Value) return; // handle kiai duration // eventually this will be simpler when we have control points with durations. if (nextControlPoint != null) { RelativeSizeAxes = Axes.Both; Origin = Anchor.TopLeft; Width = (float)(nextControlPoint.Time - effect.Time); AddInternal(new KiaiVisualisation(effect.Time, nextControlPoint.Time) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomLeft, Origin = Anchor.CentreLeft, Height = 0.4f, Depth = float.MaxValue, Colour = colours.Purple1, }); } } private partial class KiaiVisualisation : Circle, IHasTooltip { private readonly double startTime; private readonly double endTime; public KiaiVisualisation(double startTime, double endTime) { this.startTime = startTime; this.endTime = endTime; } public LocalisableString TooltipText => $"{startTime.ToEditorFormattedString()} - {endTime.ToEditorFormattedString()} kiai time"; } // kiai sections display duration, so are required to be visualised. public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint otherEffect && effect.KiaiMode == otherEffect.KiaiMode; } }