/* * Copyright (c) 2018-2020, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include REGISTER_LAYOUT(GUI, HorizontalBoxLayout) REGISTER_LAYOUT(GUI, VerticalBoxLayout) namespace GUI { BoxLayout::BoxLayout(Orientation orientation) : m_orientation(orientation) { register_property( "orientation", [this] { return m_orientation == Gfx::Orientation::Vertical ? "Vertical" : "Horizontal"; }, nullptr); } UISize BoxLayout::preferred_size() const { VERIFY(m_owner); UIDimension result_primary { 0 }; UIDimension result_secondary { 0 }; bool first_item { true }; for (auto& entry : m_entries) { if (!entry.widget || !entry.widget->is_visible()) continue; UISize min_size = entry.widget->min_size(); UISize max_size = entry.widget->max_size(); UISize preferred_size = entry.widget->preferred_size(); if (result_primary != SpecialDimension::Grow) { UIDimension item_primary_size = clamp( preferred_size.primary_size_for_orientation(orientation()), min_size.primary_size_for_orientation(orientation()), max_size.primary_size_for_orientation(orientation())); if (item_primary_size.is_int()) result_primary.add_if_int(item_primary_size.as_int()); if (item_primary_size.is_grow()) result_primary = SpecialDimension::Grow; if (!first_item) result_primary.add_if_int(spacing()); } { UIDimension secondary_preferred_size = preferred_size.secondary_size_for_orientation(orientation()); if (secondary_preferred_size == SpecialDimension::OpportunisticGrow) secondary_preferred_size = 0; UIDimension item_secondary_size = clamp( secondary_preferred_size, min_size.secondary_size_for_orientation(orientation()), max_size.secondary_size_for_orientation(orientation())); result_secondary = max(item_secondary_size, result_secondary); } first_item = false; } result_primary.add_if_int( margins().primary_total_for_orientation(orientation()) + m_owner->content_margins().primary_total_for_orientation(orientation())); result_secondary.add_if_int( margins().secondary_total_for_orientation(orientation()) + m_owner->content_margins().secondary_total_for_orientation(orientation())); if (orientation() == Gfx::Orientation::Horizontal) return { result_primary, result_secondary }; return { result_secondary, result_primary }; } UISize BoxLayout::min_size() const { VERIFY(m_owner); UIDimension result_primary { 0 }; UIDimension result_secondary { 0 }; bool first_item { true }; for (auto& entry : m_entries) { if (!entry.widget || !entry.widget->is_visible()) continue; UISize min_size = entry.widget->min_size(); { UIDimension primary_min_size = min_size.primary_size_for_orientation(orientation()); VERIFY(primary_min_size.is_one_of(SpecialDimension::Shrink, SpecialDimension::Regular)); if (primary_min_size.is_int()) result_primary.add_if_int(primary_min_size.as_int()); if (!first_item) result_primary.add_if_int(spacing()); } { UIDimension secondary_min_size = min_size.secondary_size_for_orientation(orientation()); VERIFY(secondary_min_size.is_one_of(SpecialDimension::Shrink, SpecialDimension::Regular)); result_secondary = max(result_secondary, secondary_min_size); } first_item = false; } result_primary.add_if_int( margins().primary_total_for_orientation(orientation()) + m_owner->content_margins().primary_total_for_orientation(orientation())); result_secondary.add_if_int( margins().secondary_total_for_orientation(orientation()) + m_owner->content_margins().secondary_total_for_orientation(orientation())); if (orientation() == Gfx::Orientation::Horizontal) return { result_primary, result_secondary }; return { result_secondary, result_primary }; } void BoxLayout::run(Widget& widget) { if (m_entries.is_empty()) return; struct Item { Widget* widget { nullptr }; int min_size { -1 }; int max_size { -1 }; int size { 0 }; bool final { false }; }; Vector items; for (size_t i = 0; i < m_entries.size(); ++i) { auto& entry = m_entries[i]; if (entry.type == Entry::Type::Spacer) { items.append(Item { nullptr, -1, -1 }); continue; } if (!entry.widget) continue; if (!entry.widget->is_visible()) continue; auto min_size = entry.widget->min_size(); auto max_size = entry.widget->max_size(); if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) { auto preferred_size = entry.widget->layout()->preferred_size(); min_size = max_size = preferred_size; } items.append(Item { entry.widget.ptr(), min_size.primary_size_for_orientation(orientation()), max_size.primary_size_for_orientation(orientation()) }); } if (items.is_empty()) return; Gfx::IntRect content_rect = widget.content_rect(); int available_size = content_rect.size().primary_size_for_orientation(orientation()) - spacing() * (items.size() - 1); int unfinished_items = items.size(); if (orientation() == Gfx::Orientation::Horizontal) available_size -= margins().left() + margins().right(); else available_size -= margins().top() + margins().bottom(); // Pass 1: Set all items to their minimum size. for (auto& item : items) { item.size = 0; if (item.min_size >= 0) item.size = item.min_size; available_size -= item.size; if (item.min_size >= 0 && item.max_size >= 0 && item.min_size == item.max_size) { // Fixed-size items finish immediately in the first pass. item.final = true; --unfinished_items; } } // Pass 2: Distribute remaining available size evenly, respecting each item's maximum size. while (unfinished_items && available_size > 0) { int slice = available_size / unfinished_items; // If available_size does not divide evenly by unfinished_items, // there are some extra pixels that have to be distributed. int pixels = available_size - slice * unfinished_items; available_size = 0; for (auto& item : items) { if (item.final) continue; int pixel = pixels ? 1 : 0; pixels -= pixel; int item_size_with_full_slice = item.size + slice + pixel; item.size = item_size_with_full_slice; if (item.max_size >= 0) item.size = min(item.max_size, item_size_with_full_slice); // If the slice was more than we needed, return remained to available_size. int remainder_to_give_back = item_size_with_full_slice - item.size; available_size += remainder_to_give_back; if (item.max_size >= 0 && item.size == item.max_size) { // We've hit the item's max size. Don't give it any more space. item.final = true; --unfinished_items; } } } // Pass 3: Place the widgets. int current_x = margins().left() + content_rect.x(); int current_y = margins().top() + content_rect.y(); auto widget_rect_with_margins_subtracted = margins().applied_to(content_rect); for (auto& item : items) { Gfx::IntRect rect { current_x, current_y, 0, 0 }; rect.set_primary_size_for_orientation(orientation(), item.size); if (item.widget) { int secondary = widget.content_size().secondary_size_for_orientation(orientation()); int min_secondary = item.widget->min_size().secondary_size_for_orientation(orientation()); int max_secondary = item.widget->max_size().secondary_size_for_orientation(orientation()); if (min_secondary >= 0) secondary = max(secondary, min_secondary); if (max_secondary >= 0) secondary = min(secondary, max_secondary); secondary -= margins().secondary_total_for_orientation(orientation()); rect.set_secondary_size_for_orientation(orientation(), secondary); if (orientation() == Gfx::Orientation::Horizontal) rect.center_vertically_within(widget_rect_with_margins_subtracted); else rect.center_horizontally_within(widget_rect_with_margins_subtracted); item.widget->set_relative_rect(rect); } if (orientation() == Gfx::Orientation::Horizontal) current_x += rect.width() + spacing(); else current_y += rect.height() + spacing(); } } }