A game framework written with osu! in mind.
at master 229 lines 9.7 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using osuTK; 5using System; 6using System.Collections.Generic; 7using System.Linq; 8using osu.Framework.Graphics.Transforms; 9using osu.Framework.Layout; 10 11namespace osu.Framework.Graphics.Containers 12{ 13 /// <summary> 14 /// A container that can be used to fluently arrange its children. 15 /// </summary> 16 public abstract class FlowContainer<T> : Container<T> 17 where T : Drawable 18 { 19 internal event Action OnLayout; 20 21 protected FlowContainer() 22 { 23 AddLayout(layout); 24 AddLayout(childLayout); 25 } 26 27 /// <summary> 28 /// The easing that should be used when children are moved to their position in the layout. 29 /// </summary> 30 public Easing LayoutEasing 31 { 32 get => AutoSizeEasing; 33 set => AutoSizeEasing = value; 34 } 35 36 /// <summary> 37 /// The time it should take to move a child from its current position to its new layout position. 38 /// </summary> 39 public float LayoutDuration 40 { 41 get => AutoSizeDuration * 2; 42 set => AutoSizeDuration = value / 2; 43 } 44 45 private Vector2 maximumSize; 46 47 /// <summary> 48 /// Optional maximum dimensions for this container. Note that the meaning of this value can change 49 /// depending on the implementation. 50 /// </summary> 51 public Vector2 MaximumSize 52 { 53 get => maximumSize; 54 set 55 { 56 if (maximumSize == value) return; 57 58 maximumSize = value; 59 Invalidate(Invalidation.DrawSize); 60 } 61 } 62 63 private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); 64 private readonly LayoutValue childLayout = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child); 65 66 protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || !layout.IsValid; 67 68 /// <summary> 69 /// Invoked when layout should be invalidated. 70 /// </summary> 71 protected virtual void InvalidateLayout() => layout.Invalidate(); 72 73 private readonly Dictionary<Drawable, float> layoutChildren = new Dictionary<Drawable, float>(); 74 75 protected internal override void AddInternal(Drawable drawable) 76 { 77 layoutChildren.Add(drawable, 0f); 78 // 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 79 // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. 80 InvalidateLayout(); 81 base.AddInternal(drawable); 82 } 83 84 protected internal override bool RemoveInternal(Drawable drawable) 85 { 86 layoutChildren.Remove(drawable); 87 // 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 88 // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. 89 InvalidateLayout(); 90 return base.RemoveInternal(drawable); 91 } 92 93 protected internal override void ClearInternal(bool disposeChildren = true) 94 { 95 layoutChildren.Clear(); 96 // 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 97 // if we are set to AutoSizeAxes.None, but even in that situation, the layout can and often does change when children are added/removed. 98 InvalidateLayout(); 99 base.ClearInternal(disposeChildren); 100 } 101 102 /// <summary> 103 /// 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). 104 /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal <see cref="FillFlowContainer{T}"/> and the drawable with the highest position value will be the right-most drawable in a horizontal <see cref="FillFlowContainer{T}"/>. 105 /// </summary> 106 /// <param name="drawable">The drawable whose position should be changed, must be a child of this container.</param> 107 /// <param name="newPosition">The new position in the layout the drawable should have.</param> 108 public void SetLayoutPosition(Drawable drawable, float newPosition) 109 { 110 if (!layoutChildren.ContainsKey(drawable)) 111 throw new InvalidOperationException($"Cannot change layout position of drawable which is not contained within this {nameof(FlowContainer<T>)}."); 112 113 layoutChildren[drawable] = newPosition; 114 InvalidateLayout(); 115 } 116 117 /// <summary> 118 /// Inserts a new drawable at the specified layout position. 119 /// </summary> 120 /// <param name="position">The layout position of the new child.</param> 121 /// <param name="drawable">The drawable to be inserted.</param> 122 public void Insert(int position, T drawable) 123 { 124 Add(drawable); 125 SetLayoutPosition(drawable, position); 126 } 127 128 /// <summary> 129 /// 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). 130 /// For example, the drawable with the lowest position value will be the left-most drawable in a horizontal <see cref="FillFlowContainer{T}"/> and the drawable with the highest position value will be the right-most drawable in a horizontal <see cref="FillFlowContainer{T}"/>. 131 /// </summary> 132 /// <param name="drawable">The drawable whose position should be retrieved, must be a child of this container.</param> 133 /// <returns>The position of the drawable in the layout.</returns> 134 public float GetLayoutPosition(Drawable drawable) 135 { 136 if (!layoutChildren.ContainsKey(drawable)) 137 throw new InvalidOperationException($"Cannot get layout position of drawable which is not contained within this {nameof(FlowContainer<T>)}."); 138 139 return layoutChildren[drawable]; 140 } 141 142 protected override bool UpdateChildrenLife() 143 { 144 bool changed = base.UpdateChildrenLife(); 145 146 if (changed) 147 InvalidateLayout(); 148 149 return changed; 150 } 151 152 /// <summary> 153 /// Gets the children that appear in the flow of this <see cref="FlowContainer{T}"/> in the order in which they are processed within the flowing layout. 154 /// </summary> 155 public virtual IEnumerable<Drawable> FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => layoutChildren[d]).ThenBy(d => d.ChildID); 156 157 protected abstract IEnumerable<Vector2> ComputeLayoutPositions(); 158 159 private void performLayout() 160 { 161 OnLayout?.Invoke(); 162 163 if (!Children.Any()) 164 return; 165 166 var positions = ComputeLayoutPositions().ToArray(); 167 168 int i = 0; 169 170 foreach (var d in FlowingChildren) 171 { 172 if (i > positions.Length) 173 break; 174 175 if (d.RelativePositionAxes != Axes.None) 176 throw new InvalidOperationException($"A flow container cannot contain a child with relative positioning (it is {d.RelativePositionAxes})."); 177 178 var finalPos = positions[i]; 179 180 var existingTransform = d.Transforms.OfType<FlowTransform>().FirstOrDefault(); 181 Vector2 currentTargetPos = existingTransform?.EndValue ?? d.Position; 182 183 if (currentTargetPos != finalPos) 184 { 185 if (LayoutDuration > 0) 186 d.TransformTo(d.PopulateTransform(new FlowTransform { Rewindable = false }, finalPos, LayoutDuration, LayoutEasing)); 187 else 188 { 189 if (existingTransform != null) d.ClearTransforms(false, nameof(FlowTransform)); 190 d.Position = finalPos; 191 } 192 } 193 194 ++i; 195 } 196 197 if (i != positions.Length) 198 { 199 throw new InvalidOperationException( 200 $"{GetType().FullName}.{nameof(ComputeLayoutPositions)} returned a total of {positions.Length} positions for {i} children. {nameof(ComputeLayoutPositions)} must return 1 position per child."); 201 } 202 } 203 204 protected override void UpdateAfterChildren() 205 { 206 base.UpdateAfterChildren(); 207 208 if (!childLayout.IsValid) 209 { 210 layout.Invalidate(); 211 childLayout.Validate(); 212 } 213 214 if (!layout.IsValid) 215 { 216 performLayout(); 217 layout.Validate(); 218 } 219 } 220 221 private class FlowTransform : TransformCustom<Vector2, Drawable> 222 { 223 public FlowTransform() 224 : base(nameof(Position)) 225 { 226 } 227 } 228 } 229}