diff --git a/fluXis/Graphics/Containers/SeekContainer.cs b/fluXis/Graphics/Containers/SeekContainer.cs new file mode 100644 index 000000000..b4e059307 --- /dev/null +++ b/fluXis/Graphics/Containers/SeekContainer.cs @@ -0,0 +1,79 @@ +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Input; + +namespace fluXis.Graphics.Containers; + +public partial class SeekContainer : Container +{ + public Action OnSeek { get; set; } + public Func IsPlaying { get; set; } + + public bool AlwaysDebounce { get; set; } = false; // debounce regardless of playing state + + public float HorizontalOffset { get; set; } = 0; + public double DebounceTime { get; set; } = Styling.SEEK_DEBOUNCE; + + public Bindable Progress { get; private set; } = new(0); + + private double? lastSeekTime; + + protected override bool OnClick(ClickEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + seekToMousePosition(e.MousePosition); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + seekToMousePosition(e.MousePosition); + return true; + } + + protected override void OnDrag(DragEvent e) + { + float x = Math.Clamp(e.MousePosition.X - HorizontalOffset, 0, DrawWidth); + float progress = DrawWidth > 0 ? x / DrawWidth : 0; + + if (!float.IsFinite(progress) || float.IsNaN(progress)) + progress = 0; + + Progress.Value = progress; + + bool shouldDebounce = IsPlaying != null && IsPlaying() && DebounceTime > 0 || AlwaysDebounce; + + if (shouldDebounce && lastSeekTime != null && Time.Current - lastSeekTime < DebounceTime) + return; + + OnSeek?.Invoke(progress); + lastSeekTime = Time.Current; + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + seekToMousePosition(e.MousePosition); + lastSeekTime = null; + } + + private void seekToMousePosition(Vector2 position) + { + float x = Math.Clamp(position.X - HorizontalOffset, 0, DrawWidth); + float progress = DrawWidth > 0 ? x / DrawWidth : 0; + + if (!float.IsFinite(progress) || float.IsNaN(progress)) + progress = 0; + + Progress.Value = progress; + OnSeek?.Invoke(progress); + } +} diff --git a/fluXis/Graphics/Styling.cs b/fluXis/Graphics/Styling.cs index 274a9ea10..0baa087df 100644 --- a/fluXis/Graphics/Styling.cs +++ b/fluXis/Graphics/Styling.cs @@ -13,6 +13,8 @@ public static class Styling public const float TRANSITION_FADE = 300; public const float TRANSITION_ENTER_DELAY = 100; + public const float SEEK_DEBOUNCE = 50; + public static EdgeEffectParameters ShadowSmall => createShadow(5, 2); public static EdgeEffectParameters ShadowSmallNoOffset => createShadow(5, 0); diff --git a/fluXis/Overlay/Music/MusicPlayer.cs b/fluXis/Overlay/Music/MusicPlayer.cs index 5b3f93a12..f6a2a8eaf 100644 --- a/fluXis/Overlay/Music/MusicPlayer.cs +++ b/fluXis/Overlay/Music/MusicPlayer.cs @@ -20,6 +20,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; @@ -79,7 +80,7 @@ public partial class MusicPlayer : OverlayContainer, IKeyBindingHandler true; @@ -269,13 +270,12 @@ private void load() } } }, - progress = new Box + progress = new Progress(globalClock) { - Size = new Vector2(0, 4), - RelativeSizeAxes = Axes.X, - Colour = Theme.Text, Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 1f } } } @@ -299,8 +299,6 @@ protected override void Update() globalBackground.Alpha = screens.Alpha = dim.Alpha = animationProgress >= 1f ? 0 : 1; backgrounds.Alpha = video.Alpha >= 1f ? 0 : 1; - progress.Width = (float)((globalClock.CurrentTrack?.CurrentTime ?? 0) / (globalClock.CurrentTrack?.Length ?? 1000)); - pausePlay.IconSprite.Icon = globalClock.IsRunning ? Phosphor.Bold.Pause : Phosphor.Bold.Play; fullscreenToggle.IconSprite.Icon = fullscreen ? Phosphor.Bold.ArrowsInSimple : Phosphor.Bold.ArrowsOutSimple; @@ -351,8 +349,6 @@ protected override void Update() Top = Interpolation.ValueAt(animationProgress, 12, 20, 0, 1), Left = Interpolation.ValueAt(animationProgress, cover_small / 2f - 196 / 2f, 0, 0, 1) }; - - progress.Height = Interpolation.ValueAt(animationProgress, 4, 8, 0, 1); } protected override void Dispose(bool isDisposing) @@ -486,4 +482,95 @@ protected override bool OnMouseMove(MouseMoveEvent e) showMetadata(); return true; } + + private partial class Progress : Container + { + private const float collapsed_height = 6; + private const float expanded_height = 10; + + private readonly GlobalClock clock; + + private Box fill; + private Box background; + private SeekContainer seekContainer; + + public Progress(GlobalClock clock) + { + this.clock = clock; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + RelativeSizeAxes = Axes.X; + Width = 1f; + Height = collapsed_height; + + Child = seekContainer = new SeekContainer + { + RelativeSizeAxes = Axes.Both, + IsPlaying = () => clock.IsRunning, + AlwaysDebounce = true, + OnSeek = onSeek, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White, + Alpha = 0.1f + }, + fill = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = Theme.Text, + } + } + }; + } + + protected override void Update() + { + base.Update(); + + if (seekContainer.IsDragged) + { + fill.Width = seekContainer.Progress.Value; + } + else + { + var width = clock.CurrentTime / clock.CurrentTrack.Length; + if (!double.IsFinite(width) || double.IsNaN(width)) width = 0; + + fill.Width = (float)width; + } + } + + protected override bool OnHover(HoverEvent e) + { + this.ResizeHeightTo(expanded_height, 200, Easing.OutQuad); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + this.ResizeHeightTo(collapsed_height, 300, Easing.OutQuad); + } + + public void FadeColour(ColourInfo colour, double duration = 0, Easing easing = Easing.None) + { + fill.FadeColour(colour, duration, easing); + } + + private void onSeek(float progress) + { + if (clock.CurrentTrack == null) return; + double targetTime = progress * clock.CurrentTrack.Length; + clock.Seek(targetTime); + } + } } diff --git a/fluXis/Screens/Edit/UI/BottomBar/Timeline/EditorTimeline.cs b/fluXis/Screens/Edit/UI/BottomBar/Timeline/EditorTimeline.cs index ce3d8f00a..048a30e1a 100644 --- a/fluXis/Screens/Edit/UI/BottomBar/Timeline/EditorTimeline.cs +++ b/fluXis/Screens/Edit/UI/BottomBar/Timeline/EditorTimeline.cs @@ -1,12 +1,11 @@ using System; +using fluXis.Graphics; +using fluXis.Graphics.Containers; using fluXis.Graphics.UserInterface.Color; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osuTK; -using osuTK.Input; namespace fluXis.Screens.Edit.UI.BottomBar.Timeline; @@ -19,10 +18,7 @@ public partial class EditorTimeline : Container private EditorMap map { get; set; } private TimelineIndicator indicator; - - private const double debounce = 50; - - private double? lastSeekTime; + private SeekContainer seekContainer; [BackgroundDependencyLoader] private void load() @@ -31,17 +27,27 @@ private void load() Children = new Drawable[] { - new Circle + seekContainer = new SeekContainer { - Colour = Theme.Text, - RelativeSizeAxes = Axes.X, - Height = 5, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new TimelineTagContainer() { Offset = 10 }, - new TimelineDensity(), - indicator = new TimelineIndicator() + RelativeSizeAxes = Axes.Both, + HorizontalOffset = 0, + IsPlaying = () => clock.IsRunning, + OnSeek = onSeek, + Children = new Drawable[] + { + new Circle + { + Colour = Theme.Text, + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new TimelineTagContainer() { Offset = 10 }, + new TimelineDensity(), + indicator = new TimelineIndicator() + } + } }; } @@ -49,7 +55,11 @@ protected override void Update() { base.Update(); - if (!IsDragged) + if (seekContainer.IsDragged) + { + indicator.X = seekContainer.Progress.Value; + } + else { var x = clock.CurrentTime / clock.TrackLength; if (!double.IsFinite(x) || double.IsNaN(x)) x = 0; @@ -58,47 +68,9 @@ protected override void Update() } } - protected override bool OnClick(ClickEvent e) - { - if (e.Button != MouseButton.Left) - return false; - - seekToMousePosition(e.MousePosition, instant: true); - return true; - } - - protected override bool OnDragStart(DragStartEvent e) + private void onSeek(float progress) { - if (e.Button != MouseButton.Left) - return false; - - seekToMousePosition(e.MousePosition, instant: false); - return true; - } - - protected override void OnDrag(DragEvent e) => seekToMousePosition(e.MousePosition, instant: false); - - protected override void OnDragEnd(DragEndEvent e) - { - base.OnDragEnd(e); - seekToMousePosition(e.MousePosition, instant: true); - } - - private void seekToMousePosition(Vector2 position, bool instant) - { - // why is there a 20px offset?? - float x = Math.Clamp(position.X - 20, 0, DrawWidth); - float progress = x / DrawWidth; double targetTime = progress * clock.TrackLength; - - var indicatorPos = targetTime / clock.TrackLength; - if (!double.IsFinite(indicatorPos) || double.IsNaN(indicatorPos)) indicatorPos = 0; - indicator.X = (float)indicatorPos; - - if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < debounce) - return; - clock.SeekSmoothly(targetTime); - lastSeekTime = instant ? null : Time.Current; } }