1/**************************************************************************/
2/* box_container.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "box_container.h"
32
33#include "scene/gui/label.h"
34#include "scene/gui/margin_container.h"
35#include "scene/theme/theme_db.h"
36
37struct _MinSizeCache {
38 int min_size = 0;
39 bool will_stretch = false;
40 int final_size = 0;
41};
42
43void BoxContainer::_resort() {
44 /** First pass, determine minimum size AND amount of stretchable elements */
45
46 Size2i new_size = get_size();
47
48 bool rtl = is_layout_rtl();
49
50 bool first = true;
51 int children_count = 0;
52 int stretch_min = 0;
53 int stretch_avail = 0;
54 float stretch_ratio_total = 0.0;
55 HashMap<Control *, _MinSizeCache> min_size_cache;
56
57 for (int i = 0; i < get_child_count(); i++) {
58 Control *c = Object::cast_to<Control>(get_child(i));
59 if (!c || !c->is_visible_in_tree()) {
60 continue;
61 }
62 if (c->is_set_as_top_level()) {
63 continue;
64 }
65
66 Size2i size = c->get_combined_minimum_size();
67 _MinSizeCache msc;
68
69 if (vertical) { /* VERTICAL */
70 stretch_min += size.height;
71 msc.min_size = size.height;
72 msc.will_stretch = c->get_v_size_flags().has_flag(SIZE_EXPAND);
73
74 } else { /* HORIZONTAL */
75 stretch_min += size.width;
76 msc.min_size = size.width;
77 msc.will_stretch = c->get_h_size_flags().has_flag(SIZE_EXPAND);
78 }
79
80 if (msc.will_stretch) {
81 stretch_avail += msc.min_size;
82 stretch_ratio_total += c->get_stretch_ratio();
83 }
84 msc.final_size = msc.min_size;
85 min_size_cache[c] = msc;
86 children_count++;
87 }
88
89 if (children_count == 0) {
90 return;
91 }
92
93 int stretch_max = (vertical ? new_size.height : new_size.width) - (children_count - 1) * theme_cache.separation;
94 int stretch_diff = stretch_max - stretch_min;
95 if (stretch_diff < 0) {
96 //avoid negative stretch space
97 stretch_diff = 0;
98 }
99
100 stretch_avail += stretch_diff; //available stretch space.
101 /** Second, pass successively to discard elements that can't be stretched, this will run while stretchable
102 elements exist */
103
104 bool has_stretched = false;
105 while (stretch_ratio_total > 0) { // first of all, don't even be here if no stretchable objects exist
106
107 has_stretched = true;
108 bool refit_successful = true; //assume refit-test will go well
109 float error = 0.0; // Keep track of accumulated error in pixels
110
111 for (int i = 0; i < get_child_count(); i++) {
112 Control *c = Object::cast_to<Control>(get_child(i));
113 if (!c || !c->is_visible_in_tree()) {
114 continue;
115 }
116 if (c->is_set_as_top_level()) {
117 continue;
118 }
119
120 ERR_FAIL_COND(!min_size_cache.has(c));
121 _MinSizeCache &msc = min_size_cache[c];
122
123 if (msc.will_stretch) { //wants to stretch
124 //let's see if it can really stretch
125 float final_pixel_size = stretch_avail * c->get_stretch_ratio() / stretch_ratio_total;
126 // Add leftover fractional pixels to error accumulator
127 error += final_pixel_size - (int)final_pixel_size;
128 if (final_pixel_size < msc.min_size) {
129 //if available stretching area is too small for widget,
130 //then remove it from stretching area
131 msc.will_stretch = false;
132 stretch_ratio_total -= c->get_stretch_ratio();
133 refit_successful = false;
134 stretch_avail -= msc.min_size;
135 msc.final_size = msc.min_size;
136 break;
137 } else {
138 msc.final_size = final_pixel_size;
139 // Dump accumulated error if one pixel or more
140 if (error >= 1) {
141 msc.final_size += 1;
142 error -= 1;
143 }
144 }
145 }
146 }
147
148 if (refit_successful) { //uf refit went well, break
149 break;
150 }
151 }
152
153 /** Final pass, draw and stretch elements **/
154
155 int ofs = 0;
156 if (!has_stretched) {
157 if (!vertical) {
158 switch (alignment) {
159 case ALIGNMENT_BEGIN:
160 if (rtl) {
161 ofs = stretch_diff;
162 }
163 break;
164 case ALIGNMENT_CENTER:
165 ofs = stretch_diff / 2;
166 break;
167 case ALIGNMENT_END:
168 if (!rtl) {
169 ofs = stretch_diff;
170 }
171 break;
172 }
173 } else {
174 switch (alignment) {
175 case ALIGNMENT_BEGIN:
176 break;
177 case ALIGNMENT_CENTER:
178 ofs = stretch_diff / 2;
179 break;
180 case ALIGNMENT_END:
181 ofs = stretch_diff;
182 break;
183 }
184 }
185 }
186
187 first = true;
188 int idx = 0;
189
190 int start;
191 int end;
192 int delta;
193 if (!rtl || vertical) {
194 start = 0;
195 end = get_child_count();
196 delta = +1;
197 } else {
198 start = get_child_count() - 1;
199 end = -1;
200 delta = -1;
201 }
202
203 for (int i = start; i != end; i += delta) {
204 Control *c = Object::cast_to<Control>(get_child(i));
205 if (!c || !c->is_visible_in_tree()) {
206 continue;
207 }
208 if (c->is_set_as_top_level()) {
209 continue;
210 }
211
212 _MinSizeCache &msc = min_size_cache[c];
213
214 if (first) {
215 first = false;
216 } else {
217 ofs += theme_cache.separation;
218 }
219
220 int from = ofs;
221 int to = ofs + msc.final_size;
222
223 if (msc.will_stretch && idx == children_count - 1) {
224 //adjust so the last one always fits perfect
225 //compensating for numerical imprecision
226
227 to = vertical ? new_size.height : new_size.width;
228 }
229
230 int size = to - from;
231
232 Rect2 rect;
233
234 if (vertical) {
235 rect = Rect2(0, from, new_size.width, size);
236 } else {
237 rect = Rect2(from, 0, size, new_size.height);
238 }
239
240 fit_child_in_rect(c, rect);
241
242 ofs = to;
243 idx++;
244 }
245}
246
247Size2 BoxContainer::get_minimum_size() const {
248 /* Calculate MINIMUM SIZE */
249
250 Size2i minimum;
251
252 bool first = true;
253
254 for (int i = 0; i < get_child_count(); i++) {
255 Control *c = Object::cast_to<Control>(get_child(i));
256 if (!c) {
257 continue;
258 }
259 if (c->is_set_as_top_level()) {
260 continue;
261 }
262
263 if (!c->is_visible()) {
264 continue;
265 }
266
267 Size2i size = c->get_combined_minimum_size();
268
269 if (vertical) { /* VERTICAL */
270
271 if (size.width > minimum.width) {
272 minimum.width = size.width;
273 }
274
275 minimum.height += size.height + (first ? 0 : theme_cache.separation);
276
277 } else { /* HORIZONTAL */
278
279 if (size.height > minimum.height) {
280 minimum.height = size.height;
281 }
282
283 minimum.width += size.width + (first ? 0 : theme_cache.separation);
284 }
285
286 first = false;
287 }
288
289 return minimum;
290}
291
292void BoxContainer::_notification(int p_what) {
293 switch (p_what) {
294 case NOTIFICATION_SORT_CHILDREN: {
295 _resort();
296 } break;
297
298 case NOTIFICATION_THEME_CHANGED: {
299 update_minimum_size();
300 } break;
301
302 case NOTIFICATION_TRANSLATION_CHANGED:
303 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
304 queue_sort();
305 } break;
306 }
307}
308
309void BoxContainer::_validate_property(PropertyInfo &p_property) const {
310 if (is_fixed && p_property.name == "vertical") {
311 p_property.usage = PROPERTY_USAGE_NONE;
312 }
313}
314
315void BoxContainer::set_alignment(AlignmentMode p_alignment) {
316 if (alignment == p_alignment) {
317 return;
318 }
319 alignment = p_alignment;
320 _resort();
321}
322
323BoxContainer::AlignmentMode BoxContainer::get_alignment() const {
324 return alignment;
325}
326
327void BoxContainer::set_vertical(bool p_vertical) {
328 ERR_FAIL_COND_MSG(is_fixed, "Can't change orientation of " + get_class() + ".");
329 vertical = p_vertical;
330 update_minimum_size();
331 _resort();
332}
333
334bool BoxContainer::is_vertical() const {
335 return vertical;
336}
337
338Control *BoxContainer::add_spacer(bool p_begin) {
339 Control *c = memnew(Control);
340 c->set_mouse_filter(MOUSE_FILTER_PASS); //allow spacer to pass mouse events
341
342 if (vertical) {
343 c->set_v_size_flags(SIZE_EXPAND_FILL);
344 } else {
345 c->set_h_size_flags(SIZE_EXPAND_FILL);
346 }
347
348 add_child(c);
349 if (p_begin) {
350 move_child(c, 0);
351 }
352
353 return c;
354}
355
356Vector<int> BoxContainer::get_allowed_size_flags_horizontal() const {
357 Vector<int> flags;
358 flags.append(SIZE_FILL);
359 if (!vertical) {
360 flags.append(SIZE_EXPAND);
361 }
362 flags.append(SIZE_SHRINK_BEGIN);
363 flags.append(SIZE_SHRINK_CENTER);
364 flags.append(SIZE_SHRINK_END);
365 return flags;
366}
367
368Vector<int> BoxContainer::get_allowed_size_flags_vertical() const {
369 Vector<int> flags;
370 flags.append(SIZE_FILL);
371 if (vertical) {
372 flags.append(SIZE_EXPAND);
373 }
374 flags.append(SIZE_SHRINK_BEGIN);
375 flags.append(SIZE_SHRINK_CENTER);
376 flags.append(SIZE_SHRINK_END);
377 return flags;
378}
379
380BoxContainer::BoxContainer(bool p_vertical) {
381 vertical = p_vertical;
382}
383
384void BoxContainer::_bind_methods() {
385 ClassDB::bind_method(D_METHOD("add_spacer", "begin"), &BoxContainer::add_spacer);
386 ClassDB::bind_method(D_METHOD("set_alignment", "alignment"), &BoxContainer::set_alignment);
387 ClassDB::bind_method(D_METHOD("get_alignment"), &BoxContainer::get_alignment);
388 ClassDB::bind_method(D_METHOD("set_vertical", "vertical"), &BoxContainer::set_vertical);
389 ClassDB::bind_method(D_METHOD("is_vertical"), &BoxContainer::is_vertical);
390
391 BIND_ENUM_CONSTANT(ALIGNMENT_BEGIN);
392 BIND_ENUM_CONSTANT(ALIGNMENT_CENTER);
393 BIND_ENUM_CONSTANT(ALIGNMENT_END);
394
395 ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment", "get_alignment");
396 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vertical"), "set_vertical", "is_vertical");
397
398 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, BoxContainer, separation);
399}
400
401MarginContainer *VBoxContainer::add_margin_child(const String &p_label, Control *p_control, bool p_expand) {
402 Label *l = memnew(Label);
403 l->set_theme_type_variation("HeaderSmall");
404 l->set_text(p_label);
405 add_child(l);
406 MarginContainer *mc = memnew(MarginContainer);
407 mc->add_theme_constant_override("margin_left", 0);
408 mc->add_child(p_control, true);
409 add_child(mc);
410 if (p_expand) {
411 mc->set_v_size_flags(SIZE_EXPAND_FILL);
412 }
413
414 return mc;
415}
416