
Previously, the layout algorithm preferred to give every item an equally sized slice of the remaining space. This meant that not the entire area was used when the remaining size did not divide evenly by the number of items. This caused, for example, the ResizeCorner in HexEditor to be a couple of pixels left of the actual corner for some sizes of the window. Now, the remaining pixels are distributed on a first come, first served basis. However, only one pixel is distributed at a time. This means items towards the left might me a pixel larger than their siblings towards the right.
228 lines
7.8 KiB
C++
228 lines
7.8 KiB
C++
/*
|
|
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/JsonObject.h>
|
|
#include <LibGUI/BoxLayout.h>
|
|
#include <LibGUI/Widget.h>
|
|
#include <LibGfx/Orientation.h>
|
|
#include <stdio.h>
|
|
|
|
REGISTER_CORE_OBJECT(GUI, HorizontalBoxLayout)
|
|
REGISTER_CORE_OBJECT(GUI, VerticalBoxLayout)
|
|
|
|
namespace GUI {
|
|
|
|
BoxLayout::BoxLayout(Orientation orientation)
|
|
: m_orientation(orientation)
|
|
{
|
|
register_property(
|
|
"orientation", [this] { return m_orientation == Gfx::Orientation::Vertical ? "Vertical" : "Horizontal"; }, nullptr);
|
|
}
|
|
|
|
Gfx::IntSize BoxLayout::preferred_size() const
|
|
{
|
|
Gfx::IntSize size;
|
|
size.set_primary_size_for_orientation(orientation(), preferred_primary_size());
|
|
size.set_secondary_size_for_orientation(orientation(), preferred_secondary_size());
|
|
return size;
|
|
}
|
|
|
|
int BoxLayout::preferred_primary_size() const
|
|
{
|
|
int size = 0;
|
|
|
|
for (auto& entry : m_entries) {
|
|
if (!entry.widget || !entry.widget->is_visible())
|
|
continue;
|
|
int min_size = entry.widget->min_size().primary_size_for_orientation(orientation());
|
|
int max_size = entry.widget->max_size().primary_size_for_orientation(orientation());
|
|
int preferred_primary_size = -1;
|
|
if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
|
|
preferred_primary_size = entry.widget->layout()->preferred_size().primary_size_for_orientation(orientation());
|
|
}
|
|
int item_size = max(0, preferred_primary_size);
|
|
item_size = max(min_size, item_size);
|
|
item_size = min(max_size, item_size);
|
|
size += item_size + spacing();
|
|
}
|
|
if (size > 0)
|
|
size -= spacing();
|
|
|
|
if (orientation() == Gfx::Orientation::Horizontal)
|
|
size += margins().left() + margins().right();
|
|
else
|
|
size += margins().top() + margins().bottom();
|
|
|
|
if (!size)
|
|
return -1;
|
|
return size;
|
|
}
|
|
|
|
int BoxLayout::preferred_secondary_size() const
|
|
{
|
|
int size = 0;
|
|
for (auto& entry : m_entries) {
|
|
if (!entry.widget || !entry.widget->is_visible())
|
|
continue;
|
|
int min_size = entry.widget->min_size().secondary_size_for_orientation(orientation());
|
|
int preferred_secondary_size = -1;
|
|
if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
|
|
preferred_secondary_size = entry.widget->layout()->preferred_size().secondary_size_for_orientation(orientation());
|
|
size = max(size, preferred_secondary_size);
|
|
}
|
|
size = max(min_size, size);
|
|
}
|
|
|
|
if (orientation() == Gfx::Orientation::Horizontal)
|
|
size += margins().top() + margins().bottom();
|
|
else
|
|
size += margins().left() + margins().right();
|
|
|
|
if (!size)
|
|
return -1;
|
|
return size;
|
|
}
|
|
|
|
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<Item, 32> 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;
|
|
|
|
int available_size = widget.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();
|
|
int current_y = margins().top();
|
|
|
|
auto widget_rect_with_margins_subtracted = widget.rect();
|
|
widget_rect_with_margins_subtracted.take_from_left(margins().left());
|
|
widget_rect_with_margins_subtracted.take_from_top(margins().top());
|
|
widget_rect_with_margins_subtracted.take_from_right(margins().right());
|
|
widget_rect_with_margins_subtracted.take_from_bottom(margins().bottom());
|
|
|
|
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.size().secondary_size_for_orientation(orientation());
|
|
if (orientation() == Gfx::Orientation::Horizontal)
|
|
secondary -= margins().top() + margins().bottom();
|
|
else
|
|
secondary -= margins().left() + margins().right();
|
|
|
|
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);
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
}
|