// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Framework.Input.Events; using osu.Framework.MathUtils; using osu.Game.Graphics.Containers; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class ZoomableScrollContainer : OsuScrollContainer { /// /// The time to zoom into/out of a point. /// All user scroll input will be overwritten during the zoom transform. /// public double ZoomDuration; /// /// The easing with which to transform the zoom. /// public Easing ZoomEasing; private readonly Container zoomedContent; protected override Container Content => zoomedContent; private float currentZoom = 1; public ZoomableScrollContainer() : base(Direction.Horizontal) { base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); } private int minZoom = 1; /// /// The minimum zoom level allowed. /// public int MinZoom { get => minZoom; set { if (value < 1) throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value)); minZoom = value; if (Zoom < value) Zoom = value; } } private int maxZoom = 60; /// /// The maximum zoom level allowed. /// public int MaxZoom { get => maxZoom; set { if (value < 1) throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value)); maxZoom = value; if (Zoom > value) Zoom = value; } } /// /// Gets or sets the content zoom level of this . /// public float Zoom { get => zoomTarget; set { value = Math.Clamp(value, MinZoom, MaxZoom); if (IsLoaded) setZoomTarget(value, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); else currentZoom = zoomTarget = value; } } protected override void LoadComplete() { base.LoadComplete(); // This width only gets updated on the application of a transform, so this needs to be initialized here. updateZoomedContentWidth(); } protected override bool OnScroll(ScrollEvent e) { if (e.IsPrecise) // for now, we don't support zoom when using a precision scroll device. this needs gesture support. return base.OnScroll(e); setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); return true; } private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom; private float zoomTarget = 1; private void setZoomTarget(float newZoom, float focusPoint) { zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom); transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); } private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); private class TransformZoom : Transform { /// /// The focus point in absolute coordinates local to the content. /// private readonly float focusPoint; /// /// The size of the content. /// private readonly float contentSize; /// /// The scroll offset at the start of the transform. /// private readonly float scrollOffset; /// /// Transforms to a new value. /// /// The focus point in absolute coordinates local to the content. /// The size of the content. /// The scroll offset at the start of the transform. public TransformZoom(float focusPoint, float contentSize, float scrollOffset) { this.focusPoint = focusPoint; this.contentSize = contentSize; this.scrollOffset = scrollOffset; } public override string TargetMember => nameof(currentZoom); 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; 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); d.ScrollTo(targetOffset, false); } protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom; } } }