1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 21:17:25 +08:00
osu-lazer/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

254 lines
9.3 KiB
C#
Raw Normal View History

// 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.
2018-05-18 12:05:58 +08:00
2018-05-18 16:53:09 +08:00
using System;
using osu.Framework.Allocation;
2018-05-18 12:05:58 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
2018-11-20 15:51:59 +08:00
using osuTK;
2018-05-18 12:05:58 +08:00
2018-11-06 17:28:22 +08:00
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
2018-05-18 12:05:58 +08:00
{
public partial class ZoomableScrollContainer : OsuScrollContainer
2018-05-18 12:05:58 +08:00
{
2018-05-18 16:53:09 +08:00
/// <summary>
/// The time to zoom into/out of a point.
/// All user scroll input will be overwritten during the zoom transform.
/// </summary>
public double ZoomDuration;
/// <summary>
/// The easing with which to transform the zoom.
/// </summary>
public Easing ZoomEasing;
2018-05-18 12:05:58 +08:00
private readonly Container zoomedContent;
protected override Container<Drawable> Content => zoomedContent;
/// <summary>
/// The current zoom level of <see cref="ZoomableScrollContainer"/>.
/// It may differ from <see cref="Zoom"/> during transitions.
/// </summary>
public float CurrentZoom { get; private set; } = 1;
private bool isZoomSetUp;
[Resolved]
private IFrameBasedClock? editorClock { get; set; }
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
private float minZoom;
private float maxZoom;
/// <summary>
/// Creates a <see cref="ZoomableScrollContainer"/> with no zoom range.
/// Functionality will be disabled until zoom is set up via <see cref="SetupZoom"/>.
/// </summary>
protected ZoomableScrollContainer()
2018-05-18 12:05:58 +08:00
: base(Direction.Horizontal)
{
base.Content.Add(zoomedContent = new Container
{
RelativeSizeAxes = Axes.Y,
// We must hide content until SetupZoom is called.
// If not, a child component that relies on its DrawWidth (via RelativeSizeAxes) may see a very incorrect value
// momentarily, as noticed in the TimelineTickDisplay, which would render thousands of ticks incorrectly.
Alpha = 0,
});
AddLayout(zoomedContentWidthCache);
2018-05-18 12:05:58 +08:00
}
/// <summary>
/// Creates a <see cref="ZoomableScrollContainer"/> with a defined zoom range.
2018-05-18 16:53:09 +08:00
/// </summary>
public ZoomableScrollContainer(float minimum, float maximum, float initial)
: this()
2018-05-18 16:53:09 +08:00
{
SetupZoom(initial, minimum, maximum);
2018-05-18 16:53:09 +08:00
}
/// <summary>
/// Sets up the minimum and maximum range of this zoomable scroll container, along with the initial zoom value.
2018-05-18 16:53:09 +08:00
/// </summary>
/// <param name="initial">The initial zoom value, applied immediately.</param>
/// <param name="minimum">The minimum zoom value.</param>
/// <param name="maximum">The maximum zoom value.</param>
protected void SetupZoom(float initial, float minimum, float maximum)
2018-05-18 16:53:09 +08:00
{
if (minimum < 1)
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be >= 1.", nameof(maximum));
2019-02-28 12:31:40 +08:00
if (maximum < 1)
throw new ArgumentException($"{nameof(maximum)} ({maximum}) must be >= 1.", nameof(maximum));
2018-05-18 16:53:09 +08:00
if (minimum > maximum)
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
if (initial < minimum || initial > maximum)
throw new ArgumentException($"{nameof(initial)} ({initial}) must be between {nameof(minimum)} ({minimum}) and {nameof(maximum)} ({maximum})");
minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
zoomedContentWidthCache.Invalidate();
isZoomSetUp = true;
zoomedContent.Show();
2018-05-18 16:53:09 +08:00
}
/// <summary>
/// Gets or sets the content zoom level of this <see cref="ZoomableScrollContainer"/>.
2018-05-18 12:05:58 +08:00
/// </summary>
public float Zoom
2018-05-18 12:05:58 +08:00
{
get => zoomTarget;
set => updateZoom(value);
}
2018-05-18 16:53:09 +08:00
private void updateZoom(float value)
{
if (!isZoomSetUp)
return;
float newZoom = Math.Clamp(value, minZoom, maxZoom);
if (IsLoaded)
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else
CurrentZoom = zoomTarget = newZoom;
2018-05-18 12:05:58 +08:00
}
protected override void UpdateAfterChildren()
2019-05-09 12:53:53 +08:00
{
base.UpdateAfterChildren();
2019-05-09 12:53:53 +08:00
if (!zoomedContentWidthCache.IsValid)
updateZoomedContentWidth();
2019-05-09 12:53:53 +08:00
}
2019-05-09 12:16:56 +08:00
2018-10-02 11:02:47 +08:00
protected override bool OnScroll(ScrollEvent e)
2018-05-18 12:05:58 +08:00
{
2020-11-04 04:49:04 +08:00
if (e.AltPressed)
{
// zoom when holding alt.
2022-07-22 13:44:48 +08:00
AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;
}
// can't handle scroll correctly while playing.
// the editor will handle this case for us.
if (editorClock?.IsRunning == true)
return false;
return base.OnScroll(e);
2018-05-18 12:05:58 +08:00
}
private void updateZoomedContentWidth()
{
zoomedContent.Width = DrawWidth * CurrentZoom;
zoomedContentWidthCache.Validate();
}
2022-07-22 13:44:48 +08:00
public void AdjustZoomRelatively(float change, float? focusPoint = null)
{
if (!isZoomSetUp)
return;
2022-07-22 13:44:48 +08:00
const float zoom_change_sensitivity = 0.02f;
setZoomTarget(zoomTarget + change * (maxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
}
private float zoomTarget = 1;
2019-02-28 12:31:40 +08:00
2022-07-22 13:44:48 +08:00
private void setZoomTarget(float newZoom, float? focusPoint = null)
2018-05-18 12:05:58 +08:00
{
zoomTarget = Math.Clamp(newZoom, minZoom, maxZoom);
focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
OnZoomChanged();
2018-05-18 12:05:58 +08:00
}
private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None)
2018-05-18 12:05:58 +08:00
=> this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing));
/// <summary>
/// Invoked when <see cref="Zoom"/> has changed.
/// </summary>
protected virtual void OnZoomChanged()
{
}
2018-05-18 12:05:58 +08:00
private class TransformZoom : Transform<float, ZoomableScrollContainer>
{
/// <summary>
/// The focus point in absolute coordinates local to the content.
/// </summary>
private readonly float focusPoint;
/// <summary>
/// The size of the content.
/// </summary>
private readonly float contentSize;
/// <summary>
/// The scroll offset at the start of the transform.
/// </summary>
private readonly float scrollOffset;
/// <summary>
/// Transforms <see cref="ZoomableScrollContainer.CurrentZoom"/> to a new value.
2018-05-18 12:05:58 +08:00
/// </summary>
/// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param>
/// <param name="contentSize">The size of the content.</param>
/// <param name="scrollOffset">The scroll offset at the start of the transform.</param>
public TransformZoom(float focusPoint, float contentSize, float scrollOffset)
{
this.focusPoint = focusPoint;
this.contentSize = contentSize;
this.scrollOffset = scrollOffset;
}
public override string TargetMember => nameof(CurrentZoom);
2018-05-18 12:05:58 +08:00
private float valueAt(double time)
{
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
protected override void Apply(ZoomableScrollContainer d, double time)
{
float newZoom = valueAt(time);
float focusOffset = focusPoint - scrollOffset;
float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.CurrentZoom = newZoom;
2019-05-09 12:16:56 +08:00
d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
// TODO: Make sure draw size gets invalidated properly on the framework side, and remove this once it is.
d.Invalidate(Invalidation.DrawSize);
2018-05-18 12:05:58 +08:00
d.ScrollTo(targetOffset, false);
}
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom;
2018-05-18 12:05:58 +08:00
}
}
}