Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, Frhun <serenitystuff@frhun.de>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/JsonObject.h>
9#include <LibGUI/BoxLayout.h>
10#include <LibGUI/Margins.h>
11#include <LibGUI/Widget.h>
12#include <LibGfx/Orientation.h>
13#include <stdio.h>
14
15REGISTER_LAYOUT(GUI, HorizontalBoxLayout)
16REGISTER_LAYOUT(GUI, VerticalBoxLayout)
17
18namespace GUI {
19
20BoxLayout::BoxLayout(Orientation orientation, Margins margins, int spacing)
21 : Layout(margins, spacing)
22 , m_orientation(orientation)
23{
24 register_property(
25 "orientation", [this] { return m_orientation == Gfx::Orientation::Vertical ? "Vertical" : "Horizontal"; }, nullptr);
26}
27
28UISize BoxLayout::preferred_size() const
29{
30 VERIFY(m_owner);
31
32 UIDimension result_primary { 0 };
33 UIDimension result_secondary { 0 };
34
35 bool first_item { true };
36 for (auto& entry : m_entries) {
37 if (!entry.widget || !entry.widget->is_visible())
38 continue;
39
40 UISize min_size = entry.widget->effective_min_size();
41 UISize max_size = entry.widget->max_size();
42 UISize preferred_size = entry.widget->effective_preferred_size();
43
44 if (result_primary != SpecialDimension::Grow) {
45 UIDimension item_primary_size = clamp(
46 preferred_size.primary_size_for_orientation(orientation()),
47 min_size.primary_size_for_orientation(orientation()),
48 max_size.primary_size_for_orientation(orientation()));
49
50 if (item_primary_size.is_int())
51 result_primary.add_if_int(item_primary_size.as_int());
52
53 if (item_primary_size.is_grow())
54 result_primary = SpecialDimension::Grow;
55
56 if (!first_item)
57 result_primary.add_if_int(spacing());
58 }
59
60 {
61 UIDimension secondary_preferred_size = preferred_size.secondary_size_for_orientation(orientation());
62
63 if (secondary_preferred_size == SpecialDimension::OpportunisticGrow)
64 secondary_preferred_size = 0;
65
66 UIDimension item_secondary_size = clamp(
67 secondary_preferred_size,
68 min_size.secondary_size_for_orientation(orientation()),
69 max_size.secondary_size_for_orientation(orientation()));
70
71 result_secondary = max(item_secondary_size, result_secondary);
72 }
73
74 first_item = false;
75 }
76
77 result_primary.add_if_int(
78 margins().primary_total_for_orientation(orientation())
79 + m_owner->content_margins().primary_total_for_orientation(orientation()));
80
81 result_secondary.add_if_int(
82 margins().secondary_total_for_orientation(orientation())
83 + m_owner->content_margins().secondary_total_for_orientation(orientation()));
84
85 if (orientation() == Gfx::Orientation::Horizontal)
86 return { result_primary, result_secondary };
87 return { result_secondary, result_primary };
88}
89
90UISize BoxLayout::min_size() const
91{
92 VERIFY(m_owner);
93
94 UIDimension result_primary { 0 };
95 UIDimension result_secondary { 0 };
96
97 bool first_item { true };
98 for (auto& entry : m_entries) {
99 if (!entry.widget || !entry.widget->is_visible())
100 continue;
101
102 UISize min_size = entry.widget->effective_min_size();
103
104 {
105 UIDimension primary_min_size = min_size.primary_size_for_orientation(orientation());
106
107 VERIFY(primary_min_size.is_one_of(SpecialDimension::Shrink, SpecialDimension::Regular));
108
109 if (primary_min_size.is_int())
110 result_primary.add_if_int(primary_min_size.as_int());
111
112 if (!first_item)
113 result_primary.add_if_int(spacing());
114 }
115
116 {
117 UIDimension secondary_min_size = min_size.secondary_size_for_orientation(orientation());
118
119 VERIFY(secondary_min_size.is_one_of(SpecialDimension::Shrink, SpecialDimension::Regular));
120
121 result_secondary = max(result_secondary, secondary_min_size);
122 }
123
124 first_item = false;
125 }
126
127 result_primary.add_if_int(
128 margins().primary_total_for_orientation(orientation())
129 + m_owner->content_margins().primary_total_for_orientation(orientation()));
130
131 result_secondary.add_if_int(
132 margins().secondary_total_for_orientation(orientation())
133 + m_owner->content_margins().secondary_total_for_orientation(orientation()));
134
135 if (orientation() == Gfx::Orientation::Horizontal)
136 return { result_primary, result_secondary };
137 return { result_secondary, result_primary };
138}
139
140void BoxLayout::run(Widget& widget)
141{
142 if (m_entries.is_empty())
143 return;
144
145 struct Item {
146 Widget* widget { nullptr };
147 UIDimension min_size { SpecialDimension::Shrink };
148 UIDimension max_size { SpecialDimension::Grow };
149 UIDimension preferred_size { SpecialDimension::Shrink };
150 int size { 0 };
151 bool final { false };
152 };
153
154 Vector<Item, 32> items;
155 int spacer_count = 0;
156 int opportunistic_growth_item_count = 0;
157 int opportunistic_growth_items_base_size_total = 0;
158
159 for (size_t i = 0; i < m_entries.size(); ++i) {
160 auto& entry = m_entries[i];
161 if (entry.type == Entry::Type::Spacer) {
162 items.append(Item { nullptr, { SpecialDimension::Shrink }, { SpecialDimension::Grow }, { SpecialDimension::Grow } });
163 spacer_count++;
164 continue;
165 }
166 if (!entry.widget)
167 continue;
168 if (!entry.widget->is_visible())
169 continue;
170 auto min_size = entry.widget->effective_min_size().primary_size_for_orientation(orientation());
171 auto max_size = entry.widget->max_size().primary_size_for_orientation(orientation());
172 auto preferred_size = entry.widget->effective_preferred_size().primary_size_for_orientation(orientation());
173
174 if (preferred_size == SpecialDimension::OpportunisticGrow) {
175 opportunistic_growth_item_count++;
176 opportunistic_growth_items_base_size_total += MUST(min_size.shrink_value());
177 } else {
178 preferred_size = clamp(preferred_size, min_size, max_size);
179 }
180
181 items.append(
182 Item {
183 entry.widget.ptr(),
184 min_size,
185 max_size,
186 preferred_size });
187 }
188
189 if (items.is_empty())
190 return;
191
192 Gfx::IntRect content_rect = widget.content_rect();
193 int uncommitted_size = content_rect.size().primary_size_for_orientation(orientation())
194 - spacing() * (items.size() - 1 - spacer_count)
195 - margins().primary_total_for_orientation(orientation());
196 int unfinished_regular_items = items.size() - spacer_count - opportunistic_growth_item_count;
197 int max_amongst_the_min_sizes = 0;
198 int max_amongst_the_min_sizes_of_opportunistically_growing_items = 0;
199 int regular_items_to_layout = 0;
200 int regular_items_min_size_total = 0;
201
202 // Pass 1: Set all items to their minimum size.
203 for (auto& item : items) {
204 VERIFY(item.min_size.is_one_of(SpecialDimension::Regular, SpecialDimension::Shrink));
205 item.size = MUST(item.min_size.shrink_value());
206 uncommitted_size -= item.size;
207
208 if (item.min_size.is_int() && item.max_size.is_int() && item.min_size == item.max_size) {
209 // Fixed-size items finish immediately in the first pass.
210 item.final = true;
211 if (item.preferred_size == SpecialDimension::OpportunisticGrow) {
212 opportunistic_growth_item_count--;
213 opportunistic_growth_items_base_size_total -= MUST(item.min_size.shrink_value());
214 } else {
215 --unfinished_regular_items;
216 }
217 } else if (item.preferred_size != SpecialDimension::OpportunisticGrow && item.widget) {
218 max_amongst_the_min_sizes = max(max_amongst_the_min_sizes, MUST(item.min_size.shrink_value()));
219 regular_items_to_layout++;
220 regular_items_min_size_total += item.size;
221 } else if (item.preferred_size == SpecialDimension::OpportunisticGrow) {
222 max_amongst_the_min_sizes_of_opportunistically_growing_items = max(max_amongst_the_min_sizes_of_opportunistically_growing_items, MUST(item.min_size.shrink_value()));
223 }
224 }
225
226 // Pass 2: Set all non final, non spacer items to the previously encountered maximum min_size of these kind of items
227 // This is done to ensure even growth, if the items don't have the same min_size, which most won't have.
228 // If you are unsure what effect this has, try looking at widget gallery with, and without this, it'll be obvious.
229 if (uncommitted_size > 0) {
230 int total_growth_if_not_overcommitted = regular_items_to_layout * max_amongst_the_min_sizes - regular_items_min_size_total;
231 int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size;
232 for (auto& item : items) {
233 if (item.final || item.preferred_size == SpecialDimension::OpportunisticGrow || !item.widget)
234 continue;
235 int extra_needed_space = max_amongst_the_min_sizes - item.size;
236
237 if (overcommitment_if_all_same_min_size > 0) {
238 extra_needed_space -= (overcommitment_if_all_same_min_size * extra_needed_space + (total_growth_if_not_overcommitted - 1)) / (total_growth_if_not_overcommitted);
239 }
240
241 VERIFY(extra_needed_space >= 0);
242 VERIFY(uncommitted_size >= extra_needed_space);
243
244 item.size += extra_needed_space;
245 if (item.max_size.is_int() && item.size > item.max_size.as_int())
246 item.size = item.max_size.as_int();
247 uncommitted_size -= item.size - MUST(item.min_size.shrink_value());
248 }
249 }
250
251 // Pass 3: Determine final item size for non spacers, and non opportunisticially growing widgets
252 int loop_counter = 0; // This doubles as a safeguard for when the loop below doesn't finish for some reason, and as a mechanism to ensure it runs at least once.
253 // This has to run at least once, to handle the case where the loop for evening out the min sizes was in an overcommitted state,
254 // and gave the Widget a larger size than its preferred size.
255 while (unfinished_regular_items && (uncommitted_size > 0 || loop_counter++ == 0)) {
256 VERIFY(loop_counter < 100);
257 int slice = uncommitted_size / unfinished_regular_items;
258 // If uncommitted_size does not divide evenly by unfinished_regular_items,
259 // there are some extra pixels that have to be distributed.
260 int pixels = uncommitted_size - slice * unfinished_regular_items;
261 uncommitted_size = 0;
262
263 for (auto& item : items) {
264 if (item.final)
265 continue;
266 if (!item.widget)
267 continue;
268 if (item.preferred_size == SpecialDimension::OpportunisticGrow)
269 continue;
270
271 int pixel = pixels ? 1 : 0;
272 pixels -= pixel;
273 int item_size_with_full_slice = item.size + slice + pixel;
274
275 UIDimension resulting_size { 0 };
276 resulting_size = max(item.size, item_size_with_full_slice);
277 resulting_size = min(resulting_size, item.preferred_size);
278 resulting_size = min(resulting_size, item.max_size);
279
280 if (resulting_size.is_shrink()) {
281 // FIXME: Propagate this error, so it is obvious where the mistake is actually made.
282 if (!item.min_size.is_int())
283 dbgln("BoxLayout: underconstrained widget set to zero size: {} {}", item.widget->class_name(), item.widget->name());
284 resulting_size = MUST(item.min_size.shrink_value());
285 item.final = true;
286 }
287
288 if (resulting_size.is_grow())
289 resulting_size = item_size_with_full_slice;
290
291 item.size = resulting_size.as_int();
292
293 // If the slice was more than we needed, return remainder to available_size.
294 // Note that this will in some cases even return more than the slice size.
295 uncommitted_size += item_size_with_full_slice - item.size;
296
297 if (item.final
298 || (item.max_size.is_int() && item.max_size.as_int() == item.size)
299 || (item.preferred_size.is_int() && item.preferred_size.as_int() == item.size)) {
300 item.final = true;
301 --unfinished_regular_items;
302 }
303 }
304 }
305
306 // Pass 4: Even out min_size for opportunistically growing items, analogous to pass 2
307 if (uncommitted_size > 0 && opportunistic_growth_item_count > 0) {
308 int total_growth_if_not_overcommitted = opportunistic_growth_item_count * max_amongst_the_min_sizes_of_opportunistically_growing_items - opportunistic_growth_items_base_size_total;
309 int overcommitment_if_all_same_min_size = total_growth_if_not_overcommitted - uncommitted_size;
310 for (auto& item : items) {
311 if (item.final || item.preferred_size != SpecialDimension::OpportunisticGrow || !item.widget)
312 continue;
313 int extra_needed_space = max_amongst_the_min_sizes_of_opportunistically_growing_items - item.size;
314
315 if (overcommitment_if_all_same_min_size > 0 && total_growth_if_not_overcommitted > 0) {
316 extra_needed_space -= (overcommitment_if_all_same_min_size * extra_needed_space + (total_growth_if_not_overcommitted - 1)) / (total_growth_if_not_overcommitted);
317 }
318
319 VERIFY(extra_needed_space >= 0);
320 VERIFY(uncommitted_size >= extra_needed_space);
321
322 item.size += extra_needed_space;
323 if (item.max_size.is_int() && item.size > item.max_size.as_int())
324 item.size = item.max_size.as_int();
325 uncommitted_size -= item.size - MUST(item.min_size.shrink_value());
326 }
327 }
328
329 loop_counter = 0;
330 // Pass 5: Determine the size for the opportunistically growing items.
331 while (opportunistic_growth_item_count > 0 && uncommitted_size > 0) {
332 VERIFY(loop_counter++ < 200);
333 int opportunistic_growth_item_extra_size = uncommitted_size / opportunistic_growth_item_count;
334 int pixels = uncommitted_size - opportunistic_growth_item_count * opportunistic_growth_item_extra_size;
335 VERIFY(pixels >= 0);
336 for (auto& item : items) {
337 if (item.preferred_size != SpecialDimension::OpportunisticGrow || item.final || !item.widget)
338 continue;
339
340 int pixel = (pixels > 0 ? 1 : 0);
341 pixels -= pixel;
342 int previous_size = item.size;
343 item.size += opportunistic_growth_item_extra_size + pixel;
344 if (item.max_size.is_int() && item.size >= item.max_size.as_int()) {
345 item.size = item.max_size.as_int();
346 item.final = true;
347 opportunistic_growth_item_count--;
348 }
349 uncommitted_size -= item.size - previous_size;
350 }
351 }
352
353 // Determine size of the spacers, according to the still uncommitted size
354 int spacer_width = 0;
355 if (spacer_count > 0 && uncommitted_size > 0) {
356 spacer_width = uncommitted_size / spacer_count;
357 }
358
359 // Pass 6: Place the widgets.
360 int current_x = margins().left() + content_rect.x();
361 int current_y = margins().top() + content_rect.y();
362
363 auto widget_rect_with_margins_subtracted = margins().applied_to(content_rect);
364
365 for (auto& item : items) {
366 Gfx::IntRect rect { current_x, current_y, 0, 0 };
367
368 rect.set_primary_size_for_orientation(orientation(), item.size);
369
370 if (item.widget) {
371 int secondary = widget.content_size().secondary_size_for_orientation(orientation());
372 secondary -= margins().secondary_total_for_orientation(orientation());
373
374 UIDimension min_secondary = item.widget->effective_min_size().secondary_size_for_orientation(orientation());
375 UIDimension max_secondary = item.widget->max_size().secondary_size_for_orientation(orientation());
376 UIDimension preferred_secondary = item.widget->effective_preferred_size().secondary_size_for_orientation(orientation());
377 if (preferred_secondary.is_int())
378 secondary = min(secondary, preferred_secondary.as_int());
379 if (min_secondary.is_int())
380 secondary = max(secondary, min_secondary.as_int());
381 if (max_secondary.is_int())
382 secondary = min(secondary, max_secondary.as_int());
383
384 rect.set_secondary_size_for_orientation(orientation(), secondary);
385
386 if (orientation() == Gfx::Orientation::Horizontal)
387 rect.center_vertically_within(widget_rect_with_margins_subtracted);
388 else
389 rect.center_horizontally_within(widget_rect_with_margins_subtracted);
390
391 item.widget->set_relative_rect(rect);
392
393 if (orientation() == Gfx::Orientation::Horizontal)
394 current_x += rect.width() + spacing();
395 else
396 current_y += rect.height() + spacing();
397 } else {
398 if (orientation() == Gfx::Orientation::Horizontal)
399 current_x += spacer_width;
400 else
401 current_y += spacer_width;
402 }
403 }
404}
405
406}