A game framework written with osu! in mind.
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}