// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osuTK; using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Transforms; using osu.Framework.Layout; namespace osu.Framework.Graphics.Containers { /// /// A container that can be used to fluently arrange its children. /// public abstract class FlowContainer : Container where T : Drawable { internal event Action OnLayout; protected FlowContainer() { AddLayout(layout); AddLayout(childLayout); } /// /// The easing that should be used when children are moved to their position in the layout. /// public Easing LayoutEasing { get => AutoSizeEasing; set => AutoSizeEasing = value; } /// /// The time it should take to move a child from its current position to its new layout position. /// public float LayoutDuration { get => AutoSizeDuration * 2; set => AutoSizeDuration = value / 2; } private Vector2 maximumSize; /// /// Optional maximum dimensions for this container. Note that the meaning of this value can change /// depending on the implementation. /// public Vector2 MaximumSize { get => maximumSize; set { if (maximumSize == value) return; maximumSize = value; Invalidate(Invalidation.DrawSize); } } private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); private readonly LayoutValue childLayout = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child); protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || !layout.IsValid; /// /// Invoked when layout should be invalidated. /// protected virtual void InvalidateLayout() => layout.Invalidate(); private readonly Dictionary layoutChildren = new Dictionary(); protected internal override void AddInternal(Drawable drawable) { layoutChildren.Add(drawable, 0f); // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. InvalidateLayout(); base.AddInternal(drawable); } protected internal override bool RemoveInternal(Drawable drawable) { layoutChildren.Remove(drawable); // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. InvalidateLayout(); return base.RemoveInternal(drawable); } protected internal override void ClearInternal(bool disposeChildren = true) { layoutChildren.Clear(); // we have to ensure that the layout gets invalidated since Adding or Removing a child will affect the layout. The base class will not invalidate // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. InvalidateLayout(); base.ClearInternal(disposeChildren); } /// /// Changes the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . /// /// The drawable whose position should be changed, must be a child of this container. /// The new position in the layout the drawable should have. public void SetLayoutPosition(Drawable drawable, float newPosition) { if (!layoutChildren.ContainsKey(drawable)) throw new InvalidOperationException($"Cannot change layout position of drawable which is not contained within this {nameof(FlowContainer)}."); layoutChildren[drawable] = newPosition; InvalidateLayout(); } /// /// Inserts a new drawable at the specified layout position. /// /// The layout position of the new child. /// The drawable to be inserted. public void Insert(int position, T drawable) { Add(drawable); SetLayoutPosition(drawable, position); } /// /// Gets the position of the drawable in the layout. A higher position value means the drawable will be processed later (that is, the drawables with the lowest position appear first, and the drawable with the highest position appear last). /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal and the drawable with the highest position value will be the right-most drawable in a horizontal . /// /// The drawable whose position should be retrieved, must be a child of this container. /// The position of the drawable in the layout. public float GetLayoutPosition(Drawable drawable) { if (!layoutChildren.ContainsKey(drawable)) throw new InvalidOperationException($"Cannot get layout position of drawable which is not contained within this {nameof(FlowContainer)}."); return layoutChildren[drawable]; } protected override bool UpdateChildrenLife() { bool changed = base.UpdateChildrenLife(); if (changed) InvalidateLayout(); return changed; } /// /// Gets the children that appear in the flow of this in the order in which they are processed within the flowing layout. /// public virtual IEnumerable FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => layoutChildren[d]).ThenBy(d => d.ChildID); protected abstract IEnumerable ComputeLayoutPositions(); private void performLayout() { OnLayout?.Invoke(); if (!Children.Any()) return; var positions = ComputeLayoutPositions().ToArray(); int i = 0; foreach (var d in FlowingChildren) { if (i > positions.Length) break; if (d.RelativePositionAxes != Axes.None) throw new InvalidOperationException($"A flow container cannot contain a child with relative positioning (it is {d.RelativePositionAxes})."); var finalPos = positions[i]; var existingTransform = d.Transforms.OfType().FirstOrDefault(); Vector2 currentTargetPos = existingTransform?.EndValue ?? d.Position; if (currentTargetPos != finalPos) { if (LayoutDuration > 0) d.TransformTo(d.PopulateTransform(new FlowTransform { Rewindable = false }, finalPos, LayoutDuration, LayoutEasing)); else { if (existingTransform != null) d.ClearTransforms(false, nameof(FlowTransform)); d.Position = finalPos; } } ++i; } if (i != positions.Length) { throw new InvalidOperationException( $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); } } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); if (!childLayout.IsValid) { layout.Invalidate(); childLayout.Validate(); } if (!layout.IsValid) { performLayout(); layout.Validate(); } } private class FlowTransform : TransformCustom { public FlowTransform() : base(nameof(Position)) { } } } }