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 System;
5using System.Collections.Generic;
6using osuTK;
7using System.Linq;
8using osu.Framework.Extensions.EnumExtensions;
9using osu.Framework.Utils;
10
11namespace osu.Framework.Graphics.Containers
12{
13 /// <summary>
14 /// A <see cref="FlowContainer{Drawable}"/> that fills space by arranging its children
15 /// next to each other.
16 /// <see cref="Container{T}.Children"/> can be arranged horizontally, vertically, and in a
17 /// combined fashion, which is controlled by <see cref="Direction"/>.
18 /// <see cref="Container{T}.Children"/> are arranged from left-to-right if their
19 /// <see cref="Drawable.Anchor"/> is to the left or centered horizontally.
20 /// They are arranged from right-to-left otherwise.
21 /// <see cref="Container{T}.Children"/> are arranged from top-to-bottom if their
22 /// <see cref="Drawable.Anchor"/> is to the top or centered vertically.
23 /// They are arranged from bottom-to-top otherwise.
24 /// If non-<see cref="Drawable"/> <see cref="Container{T}.Children"/> are desired, use
25 /// <see cref="FillFlowContainer{T}"/>.
26 /// </summary>
27 public class FillFlowContainer : FillFlowContainer<Drawable>
28 {
29 }
30
31 /// <summary>
32 /// A <see cref="FlowContainer{T}"/> that fills space by arranging its children
33 /// next to each other.
34 /// <see cref="Container{T}.Children"/> can be arranged horizontally, vertically, and in a
35 /// combined fashion, which is controlled by <see cref="Direction"/>.
36 /// <see cref="Container{T}.Children"/> are arranged from left-to-right if their
37 /// <see cref="Drawable.Anchor"/> is to the left or centered horizontally.
38 /// They are arranged from right-to-left otherwise.
39 /// <see cref="Container{T}.Children"/> are arranged from top-to-bottom if their
40 /// <see cref="Drawable.Anchor"/> is to the top or centered vertically.
41 /// They are arranged from bottom-to-top otherwise.
42 /// </summary>
43 public class FillFlowContainer<T> : FlowContainer<T>, IFillFlowContainer where T : Drawable
44 {
45 private FillDirection direction = FillDirection.Full;
46
47 /// <summary>
48 /// If <see cref="FillDirection.Full"/> or <see cref="FillDirection.Horizontal"/>,
49 /// <see cref="Container{T}.Children"/> are arranged from left-to-right if their
50 /// <see cref="Drawable.Anchor"/> is to the left or centered horizontally.
51 /// They are arranged from right-to-left otherwise.
52 /// If <see cref="FillDirection.Full"/> or <see cref="FillDirection.Vertical"/>,
53 /// <see cref="Container{T}.Children"/> are arranged from top-to-bottom if their
54 /// <see cref="Drawable.Anchor"/> is to the top or centered vertically.
55 /// They are arranged from bottom-to-top otherwise.
56 /// </summary>
57 public FillDirection Direction
58 {
59 get => direction;
60 set
61 {
62 if (direction == value)
63 return;
64
65 direction = value;
66 InvalidateLayout();
67 }
68 }
69
70 private Vector2 spacing;
71
72 /// <summary>
73 /// The spacing between individual elements. Default is <see cref="Vector2.Zero"/>.
74 /// </summary>
75 public Vector2 Spacing
76 {
77 get => spacing;
78 set
79 {
80 if (spacing == value)
81 return;
82
83 spacing = value;
84 InvalidateLayout();
85 }
86 }
87
88 private Vector2 spacingFactor(Drawable c)
89 {
90 Vector2 result = c.RelativeOriginPosition;
91 if (c.Anchor.HasFlagFast(Anchor.x2))
92 result.X = 1 - result.X;
93 if (c.Anchor.HasFlagFast(Anchor.y2))
94 result.Y = 1 - result.Y;
95 return result;
96 }
97
98 protected override IEnumerable<Vector2> ComputeLayoutPositions()
99 {
100 var max = MaximumSize;
101
102 if (max == Vector2.Zero)
103 {
104 var s = ChildSize;
105
106 // If we are autosize and haven't specified a maximum size, we should allow infinite expansion.
107 // If we are inheriting then we need to use the parent size (our ActualSize).
108 max.X = AutoSizeAxes.HasFlagFast(Axes.X) ? float.MaxValue : s.X;
109 max.Y = AutoSizeAxes.HasFlagFast(Axes.Y) ? float.MaxValue : s.Y;
110 }
111
112 var children = FlowingChildren.ToArray();
113 if (children.Length == 0)
114 return new List<Vector2>();
115
116 // The positions for each child we will return later on.
117 Vector2[] result = new Vector2[children.Length];
118
119 // We need to keep track of row widths such that we can compute correct
120 // positions for horizontal centre anchor children.
121 // We also store for each child to which row it belongs.
122 int[] rowIndices = new int[children.Length];
123 List<float> rowOffsetsToMiddle = new List<float> { 0 };
124
125 // Variables keeping track of the current state while iterating over children
126 // and computing initial flow positions.
127 float rowHeight = 0;
128 float rowBeginOffset = 0;
129 var current = Vector2.Zero;
130
131 // First pass, computing initial flow positions
132 Vector2 size = Vector2.Zero;
133
134 for (int i = 0; i < children.Length; ++i)
135 {
136 Drawable c = children[i];
137
138 static Axes toAxes(FillDirection direction)
139 {
140 switch (direction)
141 {
142 case FillDirection.Full:
143 return Axes.Both;
144
145 case FillDirection.Horizontal:
146 return Axes.X;
147
148 case FillDirection.Vertical:
149 return Axes.Y;
150
151 default:
152 throw new ArgumentException($"{direction.ToString()} is not defined");
153 }
154 }
155
156 // In some cases (see the right hand side of the conditional) we want to permit relatively sized children
157 // in our fill direction; specifically, when children use FillMode.Fit to preserve the aspect ratio.
158 // Consider the following use case: A fill flow container has a fixed width but an automatic height, and fills
159 // in the vertical direction. Now, we can add relatively sized children with FillMode.Fit to make sure their
160 // aspect ratio is preserved while still allowing them to fill vertically. This special case can not result
161 // in an autosize-related feedback loop, and we can thus simply allow it.
162 if ((c.RelativeSizeAxes & AutoSizeAxes & toAxes(Direction)) != 0 && (c.FillMode != FillMode.Fit || c.RelativeSizeAxes != Axes.Both || c.Size.X > RelativeChildSize.X || c.Size.Y > RelativeChildSize.Y || AutoSizeAxes == Axes.Both))
163 {
164 throw new InvalidOperationException(
165 "Drawables inside a fill flow container may not have a relative size axis that the fill flow container is filling in and auto sizing for. " +
166 $"The fill flow container is set to flow in the {Direction} direction and autosize in {AutoSizeAxes} axes and the child is set to relative size in {c.RelativeSizeAxes} axes.");
167 }
168
169 // Populate running variables with sane initial values.
170 if (i == 0)
171 {
172 size = c.BoundingBox.Size;
173 rowBeginOffset = spacingFactor(c).X * size.X;
174 }
175
176 float rowWidth = rowBeginOffset + current.X + (1 - spacingFactor(c).X) * size.X;
177
178 //We've exceeded our allowed width, move to a new row
179 if (direction != FillDirection.Horizontal && (Precision.DefinitelyBigger(rowWidth, max.X) || direction == FillDirection.Vertical || ForceNewRow(c)))
180 {
181 current.X = 0;
182 current.Y += rowHeight;
183
184 result[i] = current;
185
186 rowOffsetsToMiddle.Add(0);
187 rowBeginOffset = spacingFactor(c).X * size.X;
188
189 rowHeight = 0;
190 }
191 else
192 {
193 result[i] = current;
194
195 // Compute offset to the middle of the row, to be applied in case of centre anchor
196 // in a second pass.
197 rowOffsetsToMiddle[^1] = rowBeginOffset - rowWidth / 2;
198 }
199
200 rowIndices[i] = rowOffsetsToMiddle.Count - 1;
201
202 Vector2 stride = Vector2.Zero;
203
204 if (i < children.Length - 1)
205 {
206 // Compute stride. Note, that the stride depends on the origins of the drawables
207 // on both sides of the step to be taken.
208 stride = (Vector2.One - spacingFactor(c)) * size;
209
210 c = children[i + 1];
211 size = c.BoundingBox.Size;
212
213 stride += spacingFactor(c) * size;
214 }
215
216 stride += Spacing;
217
218 if (stride.Y > rowHeight)
219 rowHeight = stride.Y;
220 current.X += stride.X;
221 }
222
223 float height = result.Last().Y;
224
225 Vector2 ourRelativeAnchor = children[0].RelativeAnchorPosition;
226
227 // Second pass, adjusting the positions for anchors of children.
228 // Uses rowWidths and height for centre-anchors.
229 for (int i = 0; i < children.Length; ++i)
230 {
231 var c = children[i];
232
233 switch (Direction)
234 {
235 case FillDirection.Vertical:
236 if (c.RelativeAnchorPosition.Y != ourRelativeAnchor.Y)
237 {
238 throw new InvalidOperationException(
239 $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.Y} != {c.RelativeAnchorPosition.Y}). "
240 + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional.");
241 }
242
243 break;
244
245 case FillDirection.Horizontal:
246 if (c.RelativeAnchorPosition.X != ourRelativeAnchor.X)
247 {
248 throw new InvalidOperationException(
249 $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor.X} != {c.RelativeAnchorPosition.X}). "
250 + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional.");
251 }
252
253 break;
254
255 default:
256 if (c.RelativeAnchorPosition != ourRelativeAnchor)
257 {
258 throw new InvalidOperationException(
259 $"All drawables in a {nameof(FillFlowContainer)} must use the same RelativeAnchorPosition for the given {nameof(FillDirection)}({Direction}) ({ourRelativeAnchor} != {c.RelativeAnchorPosition}). "
260 + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional.");
261 }
262
263 break;
264 }
265
266 if (c.Anchor.HasFlagFast(Anchor.x1))
267 // Begin flow at centre of row
268 result[i].X += rowOffsetsToMiddle[rowIndices[i]];
269 else if (c.Anchor.HasFlagFast(Anchor.x2))
270 // Flow right-to-left
271 result[i].X = -result[i].X;
272
273 if (c.Anchor.HasFlagFast(Anchor.y1))
274 // Begin flow at centre of total height
275 result[i].Y -= height / 2;
276 else if (c.Anchor.HasFlagFast(Anchor.y2))
277 // Flow bottom-to-top
278 result[i].Y = -result[i].Y;
279 }
280
281 return result;
282 }
283
284 /// <summary>
285 /// Returns true if the given child should be placed on a new row, false otherwise. This will be called automatically for each child in this FillFlowContainers FlowingChildren-List.
286 /// </summary>
287 /// <param name="child">The child to check.</param>
288 /// <returns>True if the given child should be placed on a new row, false otherwise.</returns>
289 protected virtual bool ForceNewRow(Drawable child) => false;
290 }
291
292 /// <summary>
293 /// Represents the direction children of a <see cref="FillFlowContainer{T}"/> should be filled in.
294 /// </summary>
295 public enum FillDirection
296 {
297 /// <summary>
298 /// Fill horizontally first, then fill vertically via multiple rows.
299 /// </summary>
300 Full,
301
302 /// <summary>
303 /// Fill only horizontally.
304 /// </summary>
305 Horizontal,
306
307 /// <summary>
308 /// Fill only vertically.
309 /// </summary>
310 Vertical
311 }
312}