A game framework written with osu! in mind.
at master 312 lines 14 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 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}