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 | |
37 | struct _MinSizeCache { |
38 | int min_size = 0; |
39 | bool will_stretch = false; |
40 | int final_size = 0; |
41 | }; |
42 | |
43 | void 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 | |
247 | Size2 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 | |
292 | void 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 | |
309 | void 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 | |
315 | void BoxContainer::set_alignment(AlignmentMode p_alignment) { |
316 | if (alignment == p_alignment) { |
317 | return; |
318 | } |
319 | alignment = p_alignment; |
320 | _resort(); |
321 | } |
322 | |
323 | BoxContainer::AlignmentMode BoxContainer::get_alignment() const { |
324 | return alignment; |
325 | } |
326 | |
327 | void 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 | |
334 | bool BoxContainer::is_vertical() const { |
335 | return vertical; |
336 | } |
337 | |
338 | Control *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 | |
356 | Vector<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 | |
368 | Vector<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 | |
380 | BoxContainer::BoxContainer(bool p_vertical) { |
381 | vertical = p_vertical; |
382 | } |
383 | |
384 | void 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 | |
401 | MarginContainer *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 | |