BoxLayout.cpp 17 KB

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