Serenity Operating System
at master 406 lines 17 kB view raw
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}