1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
3 | #include "GUI/BsGUILayoutY.h" |
4 | #include "GUI/BsGUIElement.h" |
5 | #include "GUI/BsGUISpace.h" |
6 | #include "Math/BsMath.h" |
7 | #include "Math/BsVector2I.h" |
8 | |
9 | namespace bs |
10 | { |
11 | GUILayoutY::GUILayoutY(const GUIDimensions& dimensions) |
12 | : GUILayout(dimensions) |
13 | { } |
14 | |
15 | LayoutSizeRange GUILayoutY::_calculateLayoutSizeRange() const |
16 | { |
17 | Vector2I optimalSize; |
18 | Vector2I minSize; |
19 | |
20 | for (auto& child : mChildren) |
21 | { |
22 | if (!child->_isActive()) |
23 | continue; |
24 | |
25 | LayoutSizeRange sizeRange = child->_calculateLayoutSizeRange(); |
26 | |
27 | if (child->_getType() == GUIElementBase::Type::FixedSpace) |
28 | sizeRange.optimal.x = sizeRange.min.x = 0; |
29 | |
30 | UINT32 paddingX = child->_getPadding().left + child->_getPadding().right; |
31 | UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom; |
32 | |
33 | optimalSize.y += sizeRange.optimal.y + paddingY; |
34 | optimalSize.x = std::max((UINT32)optimalSize.x, sizeRange.optimal.x + paddingX); |
35 | |
36 | minSize.y += sizeRange.min.y + paddingY; |
37 | minSize.x = std::max((UINT32)minSize.x, sizeRange.min.x + paddingX); |
38 | } |
39 | |
40 | LayoutSizeRange sizeRange = _getDimensions().calculateSizeRange(optimalSize); |
41 | sizeRange.min.x = std::max(sizeRange.min.x, minSize.x); |
42 | sizeRange.min.y = std::max(sizeRange.min.y, minSize.y); |
43 | |
44 | return sizeRange; |
45 | } |
46 | |
47 | void GUILayoutY::_updateOptimalLayoutSizes() |
48 | { |
49 | // Update all children first, otherwise we can't determine our own optimal size |
50 | GUIElementBase::_updateOptimalLayoutSizes(); |
51 | |
52 | if(mChildren.size() != mChildSizeRanges.size()) |
53 | mChildSizeRanges.resize(mChildren.size()); |
54 | |
55 | Vector2I optimalSize; |
56 | Vector2I minSize; |
57 | |
58 | UINT32 childIdx = 0; |
59 | for(auto& child : mChildren) |
60 | { |
61 | LayoutSizeRange& childSizeRange = mChildSizeRanges[childIdx]; |
62 | |
63 | if (child->_isActive()) |
64 | { |
65 | childSizeRange = child->_getLayoutSizeRange(); |
66 | if (child->_getType() == GUIElementBase::Type::FixedSpace) |
67 | { |
68 | childSizeRange.optimal.x = 0; |
69 | childSizeRange.min.x = 0; |
70 | } |
71 | |
72 | UINT32 paddingX = child->_getPadding().left + child->_getPadding().right; |
73 | UINT32 paddingY = child->_getPadding().top + child->_getPadding().bottom; |
74 | |
75 | optimalSize.y += childSizeRange.optimal.y + paddingY; |
76 | optimalSize.x = std::max((UINT32)optimalSize.x, childSizeRange.optimal.x + paddingX); |
77 | |
78 | minSize.y += childSizeRange.min.y + paddingY; |
79 | minSize.x = std::max((UINT32)minSize.x, childSizeRange.min.x + paddingX); |
80 | } |
81 | else |
82 | childSizeRange = LayoutSizeRange(); |
83 | |
84 | childIdx++; |
85 | } |
86 | |
87 | mSizeRange = _getDimensions().calculateSizeRange(optimalSize); |
88 | mSizeRange.min.x = std::max(mSizeRange.min.x, minSize.x); |
89 | mSizeRange.min.y = std::max(mSizeRange.min.y, minSize.y); |
90 | } |
91 | |
92 | void GUILayoutY::_getElementAreas(const Rect2I& layoutArea, Rect2I* elementAreas, UINT32 numElements, |
93 | const Vector<LayoutSizeRange>& sizeRanges, const LayoutSizeRange& mySizeRange) const |
94 | { |
95 | assert(mChildren.size() == numElements); |
96 | |
97 | UINT32 totalOptimalSize = mySizeRange.optimal.y; |
98 | UINT32 totalNonClampedSize = 0; |
99 | UINT32 numNonClampedElements = 0; |
100 | UINT32 numFlexibleSpaces = 0; |
101 | |
102 | bool* processedElements = nullptr; |
103 | float* elementScaleWeights = nullptr; |
104 | |
105 | if (mChildren.size() > 0) |
106 | { |
107 | processedElements = bs_stack_alloc<bool>((UINT32)mChildren.size()); |
108 | memset(processedElements, 0, mChildren.size() * sizeof(bool)); |
109 | |
110 | elementScaleWeights = bs_stack_alloc<float>((UINT32)mChildren.size()); |
111 | memset(elementScaleWeights, 0, mChildren.size() * sizeof(float)); |
112 | } |
113 | |
114 | // Set initial sizes, count number of children per type and mark fixed elements as already processed |
115 | UINT32 childIdx = 0; |
116 | for (auto& child : mChildren) |
117 | { |
118 | elementAreas[childIdx].height = sizeRanges[childIdx].optimal.y; |
119 | |
120 | if (child->_getType() == GUIElementBase::Type::FixedSpace) |
121 | { |
122 | processedElements[childIdx] = true; |
123 | } |
124 | else if (child->_getType() == GUIElementBase::Type::FlexibleSpace) |
125 | { |
126 | if (child->_isActive()) |
127 | { |
128 | numFlexibleSpaces++; |
129 | numNonClampedElements++; |
130 | } |
131 | else |
132 | processedElements[childIdx] = true; |
133 | } |
134 | else |
135 | { |
136 | const GUIDimensions& dimensions = child->_getDimensions(); |
137 | |
138 | if (dimensions.fixedHeight()) |
139 | processedElements[childIdx] = true; |
140 | else |
141 | { |
142 | if (elementAreas[childIdx].height > 0) |
143 | { |
144 | numNonClampedElements++; |
145 | totalNonClampedSize += elementAreas[childIdx].height; |
146 | } |
147 | else |
148 | processedElements[childIdx] = true; |
149 | } |
150 | } |
151 | |
152 | childIdx++; |
153 | } |
154 | |
155 | // If there is some room left, calculate flexible space sizes (since they will fill up all that extra room) |
156 | if ((UINT32)layoutArea.height > totalOptimalSize) |
157 | { |
158 | UINT32 = layoutArea.height - totalOptimalSize; |
159 | UINT32 remainingSize = extraSize; |
160 | |
161 | // Flexible spaces always expand to fill up all unused space |
162 | if (numFlexibleSpaces > 0) |
163 | { |
164 | float avgSize = remainingSize / (float)numFlexibleSpaces; |
165 | |
166 | childIdx = 0; |
167 | for (auto& child : mChildren) |
168 | { |
169 | if (processedElements[childIdx]) |
170 | { |
171 | childIdx++; |
172 | continue; |
173 | } |
174 | |
175 | UINT32 = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize); |
176 | UINT32 elementHeight = elementAreas[childIdx].height + extraHeight; |
177 | |
178 | // Clamp if needed |
179 | if (child->_getType() == GUIElementBase::Type::FlexibleSpace) |
180 | { |
181 | processedElements[childIdx] = true; |
182 | numNonClampedElements--; |
183 | elementAreas[childIdx].height = elementHeight; |
184 | |
185 | remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight); |
186 | } |
187 | |
188 | childIdx++; |
189 | } |
190 | |
191 | totalOptimalSize = layoutArea.height; |
192 | } |
193 | } |
194 | |
195 | // Determine weight scale for every element. When scaling elements up/down they will be scaled based on this weight. |
196 | // Weight is to ensure all elements are scaled fairly, so elements that are large will get effected more than smaller elements. |
197 | childIdx = 0; |
198 | float invOptimalSize = 1.0f / totalNonClampedSize; |
199 | UINT32 childCount = (UINT32)mChildren.size(); |
200 | for (UINT32 i = 0; i < childCount; i++) |
201 | { |
202 | if (processedElements[childIdx]) |
203 | { |
204 | childIdx++; |
205 | continue; |
206 | } |
207 | |
208 | elementScaleWeights[childIdx] = invOptimalSize * elementAreas[childIdx].height; |
209 | |
210 | childIdx++; |
211 | } |
212 | |
213 | // Our optimal size is larger than maximum allowed, so we need to reduce size of some elements |
214 | if (totalOptimalSize > (UINT32)layoutArea.height) |
215 | { |
216 | UINT32 = totalOptimalSize - layoutArea.height; |
217 | UINT32 remainingSize = extraSize; |
218 | |
219 | // Iterate until we reduce everything so it fits, while maintaining |
220 | // equal average sizes using the weights we calculated earlier |
221 | while (remainingSize > 0 && numNonClampedElements > 0) |
222 | { |
223 | UINT32 totalRemainingSize = remainingSize; |
224 | |
225 | childIdx = 0; |
226 | for (auto& child : mChildren) |
227 | { |
228 | if (processedElements[childIdx]) |
229 | { |
230 | childIdx++; |
231 | continue; |
232 | } |
233 | |
234 | float avgSize = totalRemainingSize * elementScaleWeights[childIdx]; |
235 | |
236 | UINT32 = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize); |
237 | UINT32 elementHeight = (UINT32)std::max(0, (INT32)elementAreas[childIdx].height - (INT32)extraHeight); |
238 | |
239 | // Clamp if needed |
240 | switch (child->_getType()) |
241 | { |
242 | case GUIElementBase::Type::FlexibleSpace: |
243 | elementAreas[childIdx].height = 0; |
244 | processedElements[childIdx] = true; |
245 | numNonClampedElements--; |
246 | break; |
247 | case GUIElementBase::Type::Element: |
248 | case GUIElementBase::Type::Layout: |
249 | case GUIElementBase::Type::Panel: |
250 | { |
251 | const LayoutSizeRange& childSizeRange = sizeRanges[childIdx]; |
252 | |
253 | if (elementHeight == 0) |
254 | { |
255 | processedElements[childIdx] = true; |
256 | numNonClampedElements--; |
257 | } |
258 | else if (childSizeRange.min.y > 0 && (INT32)elementHeight < childSizeRange.min.y) |
259 | { |
260 | elementHeight = childSizeRange.min.y; |
261 | |
262 | processedElements[childIdx] = true; |
263 | numNonClampedElements--; |
264 | } |
265 | |
266 | extraHeight = elementAreas[childIdx].height - elementHeight; |
267 | elementAreas[childIdx].height = elementHeight; |
268 | remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight); |
269 | } |
270 | break; |
271 | case GUIElementBase::Type::FixedSpace: |
272 | break; |
273 | } |
274 | |
275 | childIdx++; |
276 | } |
277 | } |
278 | } |
279 | else // We are smaller than the allowed maximum, so try to expand some elements |
280 | { |
281 | UINT32 = layoutArea.height - totalOptimalSize; |
282 | UINT32 remainingSize = extraSize; |
283 | |
284 | // Iterate until we reduce everything so it fits, while maintaining |
285 | // equal average sizes using the weights we calculated earlier |
286 | while (remainingSize > 0 && numNonClampedElements > 0) |
287 | { |
288 | UINT32 totalRemainingSize = remainingSize; |
289 | |
290 | childIdx = 0; |
291 | for (auto& child : mChildren) |
292 | { |
293 | if (processedElements[childIdx]) |
294 | { |
295 | childIdx++; |
296 | continue; |
297 | } |
298 | |
299 | float avgSize = totalRemainingSize * elementScaleWeights[childIdx]; |
300 | UINT32 = std::min((UINT32)Math::ceilToInt(avgSize), remainingSize); |
301 | UINT32 elementHeight = elementAreas[childIdx].height + extraHeight; |
302 | |
303 | // Clamp if needed |
304 | switch (child->_getType()) |
305 | { |
306 | case GUIElementBase::Type::FlexibleSpace: |
307 | processedElements[childIdx] = true; |
308 | numNonClampedElements--; |
309 | break; |
310 | case GUIElementBase::Type::Element: |
311 | case GUIElementBase::Type::Layout: |
312 | case GUIElementBase::Type::Panel: |
313 | { |
314 | const LayoutSizeRange& childSizeRange = sizeRanges[childIdx]; |
315 | |
316 | if (elementHeight == 0) |
317 | { |
318 | processedElements[childIdx] = true; |
319 | numNonClampedElements--; |
320 | } |
321 | else if (childSizeRange.max.y > 0 && (INT32)elementHeight > childSizeRange.max.y) |
322 | { |
323 | elementHeight = childSizeRange.max.y; |
324 | |
325 | processedElements[childIdx] = true; |
326 | numNonClampedElements--; |
327 | } |
328 | |
329 | extraHeight = elementHeight - elementAreas[childIdx].height; |
330 | elementAreas[childIdx].height = elementHeight; |
331 | remainingSize = (UINT32)std::max(0, (INT32)remainingSize - (INT32)extraHeight); |
332 | } |
333 | break; |
334 | case GUIElementBase::Type::FixedSpace: |
335 | break; |
336 | } |
337 | |
338 | childIdx++; |
339 | } |
340 | } |
341 | } |
342 | |
343 | if (elementScaleWeights != nullptr) |
344 | bs_stack_free(elementScaleWeights); |
345 | |
346 | if (processedElements != nullptr) |
347 | bs_stack_free(processedElements); |
348 | |
349 | // Compute offsets and width |
350 | UINT32 yOffset = 0; |
351 | childIdx = 0; |
352 | |
353 | for (auto& child : mChildren) |
354 | { |
355 | UINT32 elemHeight = elementAreas[childIdx].height; |
356 | yOffset += child->_getPadding().top; |
357 | |
358 | const LayoutSizeRange& sizeRange = sizeRanges[childIdx]; |
359 | UINT32 elemWidth = (UINT32)sizeRanges[childIdx].optimal.x; |
360 | const GUIDimensions& dimensions = child->_getDimensions(); |
361 | if (!dimensions.fixedWidth()) |
362 | { |
363 | elemWidth = layoutArea.width; |
364 | if (sizeRange.min.x > 0 && elemWidth < (UINT32)sizeRange.min.x) |
365 | elemWidth = (UINT32)sizeRange.min.x; |
366 | |
367 | if (sizeRange.max.x > 0 && elemWidth > (UINT32)sizeRange.max.x) |
368 | elemWidth = (UINT32)sizeRange.max.x; |
369 | } |
370 | |
371 | elementAreas[childIdx].width = elemWidth; |
372 | |
373 | if (child->_getType() == GUIElementBase::Type::Element) |
374 | { |
375 | GUIElement* element = static_cast<GUIElement*>(child); |
376 | |
377 | UINT32 xPadding = element->_getPadding().left + element->_getPadding().right; |
378 | INT32 xOffset = Math::ceilToInt((INT32)(layoutArea.width - (INT32)(elemWidth + xPadding)) * 0.5f); |
379 | xOffset = std::max(0, xOffset); |
380 | |
381 | elementAreas[childIdx].x = layoutArea.x + xOffset; |
382 | elementAreas[childIdx].y = layoutArea.y + yOffset; |
383 | } |
384 | else |
385 | { |
386 | elementAreas[childIdx].x = layoutArea.x; |
387 | elementAreas[childIdx].y = layoutArea.y + yOffset; |
388 | } |
389 | |
390 | yOffset += elemHeight + child->_getPadding().bottom; |
391 | childIdx++; |
392 | } |
393 | } |
394 | |
395 | void GUILayoutY::_updateLayoutInternal(const GUILayoutData& data) |
396 | { |
397 | UINT32 numElements = (UINT32)mChildren.size(); |
398 | Rect2I* elementAreas = nullptr; |
399 | |
400 | if (numElements > 0) |
401 | elementAreas = bs_stack_new<Rect2I>(numElements); |
402 | |
403 | _getElementAreas(data.area, elementAreas, numElements, mChildSizeRanges, mSizeRange); |
404 | |
405 | // Now that we have all the areas, actually assign them |
406 | UINT32 childIdx = 0; |
407 | |
408 | GUILayoutData childData = data; |
409 | for(auto& child : mChildren) |
410 | { |
411 | if (child->_isActive()) |
412 | { |
413 | childData.area = elementAreas[childIdx]; |
414 | childData.clipRect = childData.area; |
415 | childData.clipRect.clip(data.clipRect); |
416 | |
417 | child->_setLayoutData(childData); |
418 | child->_updateLayoutInternal(childData); |
419 | } |
420 | |
421 | childIdx++; |
422 | } |
423 | |
424 | if (elementAreas != nullptr) |
425 | bs_stack_free(elementAreas); |
426 | } |
427 | |
428 | GUILayoutY* GUILayoutY::create() |
429 | { |
430 | return bs_new<GUILayoutY>(); |
431 | } |
432 | |
433 | GUILayoutY* GUILayoutY::create(const GUIOptions& options) |
434 | { |
435 | return bs_new<GUILayoutY>(GUIDimensions::create(options)); |
436 | } |
437 | } |