Add GridLayout for Canvas.Layout
Need a grid layout in addition to the existing vbox/hbox layouts, add a basic one. Still work in progress, minimal test cases pass. No height-for-width support yet, but row/column spanning and stretch factors are supported. Move some functionality from Layout.cxx into BoxLayout.cxx, which is specific to the one-dimensions layout types.
This commit is contained in:
parent
d146c0561f
commit
7ee427c202
@ -27,6 +27,41 @@ namespace simgear
|
||||
namespace canvas
|
||||
{
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void BoxLayout::ItemData::reset()
|
||||
{
|
||||
layout_item = 0;
|
||||
size_hint = 0;
|
||||
min_size = 0;
|
||||
max_size = 0;
|
||||
padding_orig = 0;
|
||||
padding = 0;
|
||||
size = 0;
|
||||
stretch = 0;
|
||||
visible = false;
|
||||
has_align = false;
|
||||
has_hfw = false;
|
||||
done = false;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
int BoxLayout::ItemData::hfw(int w) const
|
||||
{
|
||||
if (has_hfw)
|
||||
return layout_item->heightForWidth(w);
|
||||
else
|
||||
return layout_item->sizeHint().y();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
int BoxLayout::ItemData::mhfw(int w) const
|
||||
{
|
||||
if (has_hfw)
|
||||
return layout_item->minimumHeightForWidth(w);
|
||||
else
|
||||
return layout_item->minimumSize().y();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
BoxLayout::BoxLayout(Direction dir):
|
||||
_padding(5)
|
||||
@ -506,6 +541,197 @@ namespace canvas
|
||||
callSetVisibleInternal(_layout_items[i].layout_item.get(), visible);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void BoxLayout::distribute(LayoutItems& items, const ItemData& space)
|
||||
{
|
||||
const int num_children = static_cast<int>(items.size());
|
||||
_num_not_done = 0;
|
||||
|
||||
SG_LOG(SG_GUI,
|
||||
SG_DEBUG,
|
||||
"BoxLayout::distribute(" << space.size << "px for "
|
||||
<< num_children << " items, s.t."
|
||||
<< " min=" << space.min_size
|
||||
<< ", hint=" << space.size_hint
|
||||
<< ", max=" << space.max_size << ")");
|
||||
|
||||
if (space.size < space.min_size) {
|
||||
// TODO
|
||||
SG_LOG(SG_GUI, SG_WARN, "BoxLayout: not enough size (not implemented)");
|
||||
} else if (space.size < space.max_size) {
|
||||
_sum_stretch = 0;
|
||||
_space_stretch = 0;
|
||||
|
||||
bool less_then_hint = space.size < space.size_hint;
|
||||
|
||||
// Give min_size/size_hint to all items
|
||||
_space_left = space.size - (less_then_hint ? space.min_size : space.size_hint);
|
||||
for (int i = 0; i < num_children; ++i) {
|
||||
ItemData& d = items[i];
|
||||
if (!d.visible)
|
||||
continue;
|
||||
|
||||
d.size = less_then_hint ? d.min_size : d.size_hint;
|
||||
d.padding = d.padding_orig;
|
||||
d.done = d.size >= (less_then_hint ? d.size_hint : d.max_size);
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
i << ") initial=" << d.size
|
||||
<< ", min=" << d.min_size
|
||||
<< ", hint=" << d.size_hint
|
||||
<< ", max=" << d.max_size);
|
||||
|
||||
if (d.done)
|
||||
continue;
|
||||
_num_not_done += 1;
|
||||
|
||||
if (d.stretch > 0) {
|
||||
_sum_stretch += d.stretch;
|
||||
_space_stretch += d.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute remaining space to increase the size of each item up to its
|
||||
// size_hint/max_size
|
||||
while (_space_left > 0) {
|
||||
if (_num_not_done <= 0) {
|
||||
SG_LOG(SG_GUI, SG_WARN, "space left, but no more items?");
|
||||
break;
|
||||
}
|
||||
|
||||
int space_per_element = std::max(1, _space_left / _num_not_done);
|
||||
|
||||
SG_LOG(SG_GUI, SG_DEBUG, "space/element=" << space_per_element);
|
||||
|
||||
for (int i = 0; i < num_children; ++i) {
|
||||
ItemData& d = items[i];
|
||||
if (!d.visible)
|
||||
continue;
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
i << ") left=" << _space_left
|
||||
<< ", not_done=" << _num_not_done
|
||||
<< ", sum=" << _sum_stretch
|
||||
<< ", stretch=" << _space_stretch
|
||||
<< ", stretch/unit=" << _space_stretch / std::max(1, _sum_stretch));
|
||||
|
||||
if (d.done)
|
||||
continue;
|
||||
|
||||
if (_sum_stretch > 0 && d.stretch <= 0)
|
||||
d.done = true;
|
||||
else {
|
||||
int target_size = 0;
|
||||
int max_size = less_then_hint ? d.size_hint : d.max_size;
|
||||
|
||||
if (_sum_stretch > 0) {
|
||||
target_size = (d.stretch * (_space_left + _space_stretch)) / _sum_stretch;
|
||||
|
||||
// Item would be smaller than minimum size or larger than maximum
|
||||
// size, so just keep bounded size and ignore stretch factor
|
||||
if (target_size <= d.size || target_size >= max_size) {
|
||||
d.done = true;
|
||||
_sum_stretch -= d.stretch;
|
||||
_space_stretch -= d.size;
|
||||
|
||||
if (target_size >= max_size)
|
||||
target_size = max_size;
|
||||
else
|
||||
target_size = d.size;
|
||||
} else
|
||||
_space_stretch += target_size - d.size;
|
||||
} else {
|
||||
// Give space evenly to all remaining elements in this round
|
||||
target_size = d.size + std::min(_space_left, space_per_element);
|
||||
|
||||
if (target_size >= max_size) {
|
||||
d.done = true;
|
||||
target_size = max_size;
|
||||
}
|
||||
}
|
||||
|
||||
int old_size = d.size;
|
||||
d.size = target_size;
|
||||
_space_left -= d.size - old_size;
|
||||
}
|
||||
|
||||
if (d.done) {
|
||||
_num_not_done -= 1;
|
||||
|
||||
if (_sum_stretch <= 0 && d.stretch > 0)
|
||||
// Distribute remaining space evenly to all non-stretchable items
|
||||
// in a new round
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_space_left = space.size - space.max_size;
|
||||
int num_align = 0;
|
||||
for (int i = 0; i < num_children; ++i) {
|
||||
if (!items[i].visible)
|
||||
continue;
|
||||
|
||||
_num_not_done += 1;
|
||||
|
||||
if (items[i].has_align)
|
||||
num_align += 1;
|
||||
}
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
"Distributing excess space:"
|
||||
" not_done="
|
||||
<< _num_not_done
|
||||
<< ", num_align=" << num_align
|
||||
<< ", space_left=" << _space_left);
|
||||
|
||||
for (int i = 0; i < num_children; ++i) {
|
||||
ItemData& d = items[i];
|
||||
if (!d.visible)
|
||||
continue;
|
||||
|
||||
d.padding = d.padding_orig;
|
||||
d.size = d.max_size;
|
||||
|
||||
int space_add = 0;
|
||||
|
||||
if (d.has_align) {
|
||||
// Equally distribute superfluous space and let each child items
|
||||
// alignment handle the exact usage.
|
||||
space_add = _space_left / num_align;
|
||||
num_align -= 1;
|
||||
|
||||
d.size += space_add;
|
||||
} else if (num_align <= 0) {
|
||||
// Add superfluous space as padding
|
||||
space_add = _space_left
|
||||
// Padding after last child...
|
||||
/ (_num_not_done + 1);
|
||||
_num_not_done -= 1;
|
||||
|
||||
d.padding += space_add;
|
||||
}
|
||||
|
||||
_space_left -= space_add;
|
||||
}
|
||||
}
|
||||
|
||||
SG_LOG(SG_GUI, SG_DEBUG, "distribute:");
|
||||
for (int i = 0; i < num_children; ++i) {
|
||||
ItemData const& d = items[i];
|
||||
if (d.visible)
|
||||
SG_LOG(SG_GUI, SG_DEBUG, i << ") pad=" << d.padding << ", size= " << d.size);
|
||||
else
|
||||
SG_LOG(SG_GUI, SG_DEBUG, i << ") [hidden]");
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
HBoxLayout::HBoxLayout():
|
||||
BoxLayout(LeftToRight)
|
||||
|
@ -102,6 +102,34 @@ namespace canvas
|
||||
bool horiz() const;
|
||||
|
||||
protected:
|
||||
struct ItemData {
|
||||
LayoutItemRef layout_item;
|
||||
int size_hint,
|
||||
min_size,
|
||||
max_size,
|
||||
padding_orig, //!< original padding as specified by the user
|
||||
padding, //!< padding before element (layouted)
|
||||
size, //!< layouted size
|
||||
stretch; //!< stretch factor
|
||||
bool visible : 1,
|
||||
has_align : 1, //!< Has alignment factor set (!= AlignFill)
|
||||
has_hfw : 1, //!< height for width
|
||||
done : 1; //!< layouting done
|
||||
|
||||
/** Clear values (reset to default/empty state) */
|
||||
void reset();
|
||||
|
||||
int hfw(int w) const;
|
||||
int mhfw(int w) const;
|
||||
};
|
||||
|
||||
using LayoutItems = std::vector<ItemData>;
|
||||
|
||||
/**
|
||||
* Distribute the available @a space to all @a items
|
||||
*/
|
||||
void distribute(LayoutItems& items, const ItemData& space);
|
||||
|
||||
|
||||
typedef const int& (SGVec2i::*CoordGetter)() const;
|
||||
CoordGetter _get_layout_coord, //!< getter for coordinate in layout
|
||||
@ -112,7 +140,6 @@ namespace canvas
|
||||
int _padding;
|
||||
Direction _direction;
|
||||
|
||||
typedef std::vector<ItemData> LayoutItems;
|
||||
|
||||
mutable LayoutItems _layout_items;
|
||||
mutable ItemData _layout_data;
|
||||
@ -135,6 +162,14 @@ namespace canvas
|
||||
virtual void doLayout(const SGRecti& geom);
|
||||
|
||||
virtual void visibilityChanged(bool visible);
|
||||
|
||||
private:
|
||||
int _num_not_done = 0, //!< number of children not layouted yet
|
||||
_sum_stretch = 0, //!< sum of stretch factors of all not yet layouted
|
||||
// children
|
||||
_space_stretch = 0, //!< space currently assigned to all not yet layouted
|
||||
// stretchable children
|
||||
_space_left = 0; //!< remaining space not used by any child yet
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ include (SimGearComponent)
|
||||
set(HEADERS
|
||||
AlignFlag_values.hxx
|
||||
BoxLayout.hxx
|
||||
GridLayout.hxx
|
||||
Layout.hxx
|
||||
LayoutItem.hxx
|
||||
NasalWidget.hxx
|
||||
@ -11,6 +12,7 @@ set(HEADERS
|
||||
|
||||
set(SOURCES
|
||||
BoxLayout.cxx
|
||||
GridLayout.cxx
|
||||
Layout.cxx
|
||||
LayoutItem.cxx
|
||||
NasalWidget.cxx
|
||||
|
648
simgear/canvas/layout/GridLayout.cxx
Normal file
648
simgear/canvas/layout/GridLayout.cxx
Normal file
@ -0,0 +1,648 @@
|
||||
// GridLayout.cxx - grid layout for Canvas, closely
|
||||
// modelled on the equivalent layouts in Gtk/Qt
|
||||
// Copyright (C) 2022 James Turner
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include <simgear_config.h>
|
||||
|
||||
#include "GridLayout.hxx"
|
||||
#include "SpacerItem.hxx"
|
||||
#include <simgear/canvas/Canvas.hxx>
|
||||
|
||||
namespace simgear {
|
||||
namespace canvas {
|
||||
|
||||
// see https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/kernel/qgridlayout.cpp?h=dev
|
||||
// for similar code :)
|
||||
|
||||
static bool isValidLocation(const SGVec2i& loc)
|
||||
{
|
||||
return loc.x() >= 0 && loc.y() >= 0;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::ItemData::reset()
|
||||
{
|
||||
layout_item = 0;
|
||||
size = {};
|
||||
visible = false;
|
||||
has_align = false;
|
||||
has_hfw = false;
|
||||
done = false;
|
||||
}
|
||||
|
||||
void GridLayout::RowColumnData::resetSizeData()
|
||||
{
|
||||
minSize = 0;
|
||||
hintSize = 0;
|
||||
maxSize = 0;
|
||||
calcStretch = stretch;
|
||||
calcSize = 0;
|
||||
calcStart = 0;
|
||||
padding = 0;
|
||||
hasVisible = false;
|
||||
}
|
||||
|
||||
bool GridLayout::ItemData::containsCell(const SGVec2i& cell) const
|
||||
{
|
||||
return (layout_item->gridLocation().x() <= cell.x()) &&
|
||||
(layout_item->gridLocation().y() <= cell.y()) &&
|
||||
(layout_item->gridEnd().x() >= cell.x()) &&
|
||||
(layout_item->gridEnd().y() >= cell.y());
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
GridLayout::GridLayout()
|
||||
{
|
||||
// FIXME : share default padding value with BoxLayout
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
GridLayout::~GridLayout()
|
||||
{
|
||||
_parent.reset(); // No need to invalidate parent again...
|
||||
clear();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
void GridLayout::addItem(const LayoutItemRef& item, int column, int row, int colSpan, int rowSpan)
|
||||
{
|
||||
item->setGridLocation({column, row});
|
||||
item->setGridSpan({colSpan, rowSpan});
|
||||
addItem(item);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::addItem(const LayoutItemRef& item)
|
||||
{
|
||||
if (isValidLocation(item->gridLocation())) {
|
||||
// re-dimension as required
|
||||
const auto itemEnd = item->gridEnd();
|
||||
if (itemEnd.x() >= numColumns()) {
|
||||
// expand columns
|
||||
_dimensions.x() = itemEnd.x() + 1;
|
||||
_columns.resize(_dimensions.x());
|
||||
}
|
||||
|
||||
if (itemEnd.y() >= numRows()) {
|
||||
// expand rows
|
||||
_dimensions.y() = itemEnd.y() + 1;
|
||||
_rows.resize(_dimensions.y());
|
||||
}
|
||||
} else {
|
||||
// find first empty grid slot and use
|
||||
auto loc = innerFindUnusedLocation(item->gridLocation());
|
||||
}
|
||||
|
||||
ItemData d;
|
||||
d.reset();
|
||||
d.layout_item = item;
|
||||
|
||||
if (SGWeakReferenced::count(this)) {
|
||||
item->setParent(this);
|
||||
} else {
|
||||
SG_LOG(SG_GUI,
|
||||
SG_DEV_WARN,
|
||||
"Adding item to expired or non-refcounted grid layout");
|
||||
}
|
||||
|
||||
_layout_items.push_back(d);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void GridLayout::invalidate()
|
||||
{
|
||||
Layout::invalidate();
|
||||
_cells.clear();
|
||||
}
|
||||
|
||||
SGVec2i GridLayout::innerFindUnusedLocation(const SGVec2i& curLoc)
|
||||
{
|
||||
updateCells(); // build the cell-map on demand
|
||||
|
||||
// TODO: this code does work for spanning items yet, only for single cell items.
|
||||
|
||||
// special case: row was specified, but not column. This means only
|
||||
// search that row, and maybe extend our dimension if there's no free slots
|
||||
if (curLoc.y() >= 0) {
|
||||
// TODO: implement me :)
|
||||
}
|
||||
|
||||
const auto stride = _dimensions.x();
|
||||
for (int j = 0; j < _dimensions.y(); ++j) {
|
||||
for (int i = 0; i < stride; ++i) {
|
||||
const auto ii = _cells.at(j * stride + i);
|
||||
if (ii < 0) {
|
||||
return {i, j};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// grid is full, add a new row on the bottom and return first column
|
||||
// of it as our unused location.
|
||||
_dimensions.y() += 1;
|
||||
_rows.resize(_dimensions.y());
|
||||
return {0, _dimensions.y() - 1};
|
||||
}
|
||||
|
||||
void GridLayout::updateCells()
|
||||
{
|
||||
const auto dim = _dimensions.x() * _dimensions.y();
|
||||
if (_cells.size() == dim) {
|
||||
return;
|
||||
}
|
||||
|
||||
_cells.resize(dim);
|
||||
|
||||
const auto stride = _dimensions.x();
|
||||
std::fill(_cells.begin(), _cells.end(), -1);
|
||||
int itemIndex = 0;
|
||||
for (auto& item : _layout_items) {
|
||||
const auto topLeftCell = item.layout_item->gridLocation();
|
||||
const auto bottomRightCell = item.layout_item->gridEnd();
|
||||
for (int row = topLeftCell.y(); row <= bottomRightCell.y(); ++row) {
|
||||
for (int cell = topLeftCell.x(); cell <= bottomRightCell.x(); ++cell) {
|
||||
_cells[row * stride + cell] = itemIndex;
|
||||
}
|
||||
}
|
||||
++itemIndex;
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout::LayoutItems::iterator GridLayout::itemInCell(const SGVec2i& cell)
|
||||
{
|
||||
updateCells();
|
||||
|
||||
const auto stride = _dimensions.x();
|
||||
const auto index = _cells.at(cell.y() * stride + cell.x());
|
||||
if (index < 0)
|
||||
return _layout_items.end();
|
||||
return _layout_items.begin() + index;
|
||||
}
|
||||
|
||||
|
||||
GridLayout::LayoutItems::iterator GridLayout::firstInRow(int row)
|
||||
{
|
||||
updateCells();
|
||||
|
||||
const auto stride = _dimensions.x();
|
||||
for (int col = 0; col < _dimensions.y(); ++col) {
|
||||
if (_cells.at(row * stride + col) >= 0) {
|
||||
return itemInCell({col, row});
|
||||
}
|
||||
}
|
||||
|
||||
return _layout_items.end();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// void BoxLayout::insertItem( int index,
|
||||
// const LayoutItemRef& item,
|
||||
// int stretch,
|
||||
// uint8_t alignment )
|
||||
// {
|
||||
// ItemData item_data = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
// item_data.layout_item = item;
|
||||
// item_data.stretch = std::max(0, stretch);
|
||||
|
||||
// if( alignment != AlignFill )
|
||||
// item->setAlignment(alignment);
|
||||
|
||||
// if( SGWeakReferenced::count(this) )
|
||||
// item->setParent(this);
|
||||
// else
|
||||
// SG_LOG( SG_GUI,
|
||||
// SG_WARN,
|
||||
// "Adding item to expired or non-refcounted layout" );
|
||||
|
||||
// if( index < 0 )
|
||||
// _layout_items.push_back(item_data);
|
||||
// else
|
||||
// _layout_items.insert(_layout_items.begin() + index, item_data);
|
||||
|
||||
// invalidate();
|
||||
// }
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
size_t GridLayout::count() const
|
||||
{
|
||||
return _layout_items.size();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
LayoutItemRef GridLayout::itemAt(size_t index)
|
||||
{
|
||||
if (index >= _layout_items.size())
|
||||
return {};
|
||||
|
||||
return _layout_items[index].layout_item;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
LayoutItemRef GridLayout::takeAt(size_t index)
|
||||
{
|
||||
if (index >= _layout_items.size())
|
||||
return {};
|
||||
|
||||
auto it = _layout_items.begin() + index;
|
||||
LayoutItemRef item = it->layout_item;
|
||||
item->onRemove();
|
||||
item->setParent(LayoutItemWeakRef());
|
||||
_layout_items.erase(it);
|
||||
|
||||
invalidate();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::clear()
|
||||
{
|
||||
std::for_each(_layout_items.begin(), _layout_items.end(), [](ItemData item) {
|
||||
item.layout_item->onRemove();
|
||||
item.layout_item->setParent({});
|
||||
});
|
||||
_layout_items.clear();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::setSpacing(int spacing)
|
||||
{
|
||||
if (spacing == _padding)
|
||||
return;
|
||||
|
||||
_padding = spacing;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
int GridLayout::spacing() const
|
||||
{
|
||||
return _padding;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::setCanvas(const CanvasWeakPtr& canvas)
|
||||
{
|
||||
_canvas = canvas;
|
||||
|
||||
for (size_t i = 0; i < _layout_items.size(); ++i)
|
||||
_layout_items[i].layout_item->setCanvas(canvas);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::setDimensions(const SGVec2i& dim)
|
||||
{
|
||||
_dimensions = {
|
||||
std::max(dim.x(), _dimensions.x()),
|
||||
std::max(dim.y(), _dimensions.y())};
|
||||
invalidate();
|
||||
}
|
||||
|
||||
size_t GridLayout::numRows() const
|
||||
{
|
||||
return _dimensions.y();
|
||||
}
|
||||
|
||||
size_t GridLayout::numColumns() const
|
||||
{
|
||||
return _dimensions.x();
|
||||
}
|
||||
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::updateSizeHints() const
|
||||
{
|
||||
_columns.resize(_dimensions.x());
|
||||
_rows.resize(_dimensions.y());
|
||||
|
||||
// pre-pass: reset row/column data, compute stretch totals
|
||||
int totalRowStretch = 0, totalColStretch = 0;
|
||||
for (auto& r : _rows) {
|
||||
r.resetSizeData();
|
||||
totalRowStretch += r.stretch;
|
||||
}
|
||||
|
||||
for (auto& c : _columns) {
|
||||
c.resetSizeData();
|
||||
totalColStretch += c.stretch;
|
||||
}
|
||||
|
||||
// if no row/column has any stretch set, use '1' for every row/column
|
||||
// this means we don't need to special case this in all the rest of the code
|
||||
if (totalColStretch == 0) {
|
||||
std::for_each(_columns.begin(), _columns.end(), [](RowColumnData& a) {
|
||||
a.calcStretch = 1;
|
||||
});
|
||||
totalColStretch = _columns.size();
|
||||
}
|
||||
|
||||
if (totalRowStretch == 0) {
|
||||
std::for_each(_rows.begin(), _rows.end(), [](RowColumnData& a) {
|
||||
a.calcStretch = 1;
|
||||
});
|
||||
totalRowStretch = _rows.size();
|
||||
}
|
||||
|
||||
// first pass: do span=1 items, where the child size values
|
||||
// can be mapped directly to the row/column
|
||||
for (auto& i : _layout_items) {
|
||||
if (!i.layout_item->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isSpacer = dynamic_cast<SpacerItem*>(i.layout_item.get()) != nullptr;
|
||||
const auto minSize = i.layout_item->minimumSize();
|
||||
const auto hint = i.layout_item->sizeHint();
|
||||
const auto maxSize = i.layout_item->maximumSize();
|
||||
|
||||
// TODO: check hfw status of item
|
||||
|
||||
const auto span = i.layout_item->gridSpan();
|
||||
const auto loc = i.layout_item->gridLocation();
|
||||
if (span.x() == 1) {
|
||||
auto& cd = _columns[loc.x()];
|
||||
cd.minSize = std::max(cd.minSize, minSize.x());
|
||||
cd.hintSize = std::max(cd.hintSize, hint.x());
|
||||
cd.hasVisible |= !isSpacer;
|
||||
if (maxSize.x() < MAX_SIZE.x()) {
|
||||
cd.maxSize = std::max(cd.maxSize, maxSize.x());
|
||||
}
|
||||
}
|
||||
|
||||
if (span.y() == 1) {
|
||||
auto& rd = _rows[loc.y()];
|
||||
rd.minSize = std::max(rd.minSize, minSize.y());
|
||||
rd.hintSize = std::max(rd.hintSize, hint.y());
|
||||
rd.hasVisible |= !isSpacer;
|
||||
if (maxSize.y() < MAX_SIZE.y()) {
|
||||
rd.maxSize = std::max(rd.maxSize, maxSize.y());
|
||||
}
|
||||
}
|
||||
} // of span=1 items
|
||||
|
||||
// second pass: spanning directions of items: add remaining min/hint size
|
||||
// based on stretch factors. Doing this as a second pass means only add on
|
||||
// the extra hint amounts, which depending on span=1 items, might not be
|
||||
// very much at all
|
||||
//
|
||||
// when padding is specified for the grid, we need to remove the spanned
|
||||
// padding from our hint/min sizes, since this will always be added back
|
||||
// on to the geometry when laying out
|
||||
for (auto& i : _layout_items) {
|
||||
if (!i.layout_item->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto minSize = i.layout_item->minimumSize();
|
||||
const auto hint = i.layout_item->sizeHint();
|
||||
const auto span = i.layout_item->gridSpan();
|
||||
const auto loc = i.layout_item->gridLocation();
|
||||
|
||||
if (span.x() > 1) {
|
||||
int spanStretch = 0;
|
||||
int spanMinSize = 0;
|
||||
int spanHint = 0;
|
||||
|
||||
for (int c = loc.x(); c < loc.x() + span.x(); ++c) {
|
||||
spanStretch += _columns.at(c).calcStretch;
|
||||
spanMinSize += _columns.at(c).minSize;
|
||||
spanHint += _columns.at(c).hintSize;
|
||||
}
|
||||
|
||||
// add spanned padding onto these totals as part of the 'space we
|
||||
// already get' and hence don't need assign as extra below
|
||||
const int spannedPadding = (span.x() - 1) * _padding;
|
||||
spanHint += spannedPadding;
|
||||
spanMinSize += spannedPadding;
|
||||
|
||||
// no stretch defined, just divide equally. This is not ideal
|
||||
// but the user should specify stretch to get the result they
|
||||
// want
|
||||
if (spanStretch == 0) {
|
||||
spanStretch = span.x();
|
||||
}
|
||||
|
||||
int extraMinSize = minSize.x() - spanMinSize;
|
||||
int extraSizeHint = hint.x() - spanHint;
|
||||
|
||||
for (int c = loc.x(); c < loc.x() + span.x(); ++c) {
|
||||
auto& cd = _columns[c];
|
||||
if (extraMinSize > 0) {
|
||||
cd.minSize += extraMinSize * cd.calcStretch / spanStretch;
|
||||
}
|
||||
|
||||
if (extraSizeHint > 0) {
|
||||
cd.hintSize += extraSizeHint * cd.calcStretch / spanStretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (span.y() > 1) {
|
||||
int spanStretch = 0;
|
||||
int spanMinSize = 0;
|
||||
int spanHint = 0;
|
||||
|
||||
for (int r = loc.y(); r < loc.y() + span.y(); ++r) {
|
||||
spanStretch += _rows.at(r).calcStretch;
|
||||
spanMinSize += _rows.at(r).minSize;
|
||||
spanHint += _rows.at(r).hintSize;
|
||||
}
|
||||
|
||||
// add spanned padding onto these totals as part of the 'space we
|
||||
// already get' and hence don't need assign as extra below
|
||||
const int spannedPadding = (span.y() - 1) * _padding;
|
||||
spanHint += spannedPadding;
|
||||
spanMinSize += spannedPadding;
|
||||
|
||||
if (spanStretch == 0) {
|
||||
spanStretch = span.y();
|
||||
}
|
||||
|
||||
int extraMinSize = minSize.y() - spanMinSize;
|
||||
int extraSizeHint = hint.y() - spanHint;
|
||||
|
||||
for (int r = loc.y(); r < loc.y() + span.y(); ++r) {
|
||||
auto& rd = _rows[r];
|
||||
if (extraMinSize > 0) {
|
||||
rd.minSize += extraMinSize * rd.calcStretch / spanStretch;
|
||||
}
|
||||
|
||||
if (extraSizeHint > 0) {
|
||||
rd.hintSize += extraSizeHint * rd.calcStretch / spanStretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // of second items iteratio
|
||||
|
||||
_min_size = {0, 0};
|
||||
_max_size = MAX_SIZE;
|
||||
_size_hint = {0, 0};
|
||||
|
||||
bool isFirst = true;
|
||||
for (auto& rd : _rows) {
|
||||
_min_size.y() += rd.minSize;
|
||||
// TODO: handle max-size correctly
|
||||
// _max_size.y() +=
|
||||
_size_hint.y() += rd.hintSize;
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else if (rd.hasVisible) {
|
||||
rd.padding = _padding;
|
||||
}
|
||||
}
|
||||
|
||||
isFirst = true;
|
||||
for (auto& cd : _columns) {
|
||||
_min_size.x() += cd.minSize;
|
||||
// TODO: handle max-size correctly
|
||||
// _max_size.x() +=
|
||||
_size_hint.x() += cd.hintSize;
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else if (cd.hasVisible) {
|
||||
cd.padding = _padding;
|
||||
}
|
||||
}
|
||||
|
||||
_flags &= ~SIZE_INFO_DIRTY;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
SGVec2i GridLayout::sizeHintImpl() const
|
||||
{
|
||||
updateSizeHints();
|
||||
return _size_hint;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
SGVec2i GridLayout::minimumSizeImpl() const
|
||||
{
|
||||
updateSizeHints();
|
||||
return _min_size;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
SGVec2i GridLayout::maximumSizeImpl() const
|
||||
{
|
||||
updateSizeHints();
|
||||
return _max_size;
|
||||
}
|
||||
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::doLayout(const SGRecti& geom)
|
||||
{
|
||||
if (_flags & SIZE_INFO_DIRTY)
|
||||
updateSizeHints();
|
||||
|
||||
int availWidth = geom.width();
|
||||
int availHeight = geom.height();
|
||||
// compute extra available space
|
||||
int rowStretchTotal = 0;
|
||||
int columnStretchTotal = 0;
|
||||
|
||||
SGVec2i totalMinSize = {0, 0},
|
||||
totalPreferredSize = {0, 0};
|
||||
for (auto row = 0; row < _rows.size(); ++row) {
|
||||
totalMinSize.y() += _rows.at(row).minSize;
|
||||
totalPreferredSize.y() += _rows.at(row).hintSize;
|
||||
rowStretchTotal += _rows.at(row).calcStretch;
|
||||
availHeight -= _rows.at(row).padding;
|
||||
}
|
||||
|
||||
for (auto col = 0; col < _columns.size(); ++col) {
|
||||
totalMinSize.x() += _columns.at(col).minSize;
|
||||
totalPreferredSize.x() += _columns.at(col).hintSize;
|
||||
columnStretchTotal += _columns.at(col).calcStretch;
|
||||
availWidth -= _columns.at(col).padding;
|
||||
}
|
||||
|
||||
SGVec2i toDistribute = {0, 0};
|
||||
bool havePreferredWidth = false,
|
||||
havePreferredHeight = false;
|
||||
if (availWidth >= totalPreferredSize.x()) {
|
||||
havePreferredWidth = true;
|
||||
toDistribute.x() = availWidth - totalPreferredSize.x();
|
||||
} else if (availWidth >= totalMinSize.x()) {
|
||||
toDistribute.x() = availWidth - totalMinSize.x();
|
||||
} else {
|
||||
// available width is less than min, we will overflow
|
||||
}
|
||||
|
||||
if (availHeight >= totalPreferredSize.y()) {
|
||||
havePreferredHeight = true;
|
||||
toDistribute.y() = availHeight - totalPreferredSize.y();
|
||||
} else if (geom.height() >= totalMinSize.y()) {
|
||||
toDistribute.y() = availHeight - totalMinSize.y();
|
||||
} else {
|
||||
// available height is less than min, we will overflow
|
||||
}
|
||||
|
||||
// distribute according to stretch factors
|
||||
for (auto col = 0; col < _columns.size(); ++col) {
|
||||
auto& c = _columns[col];
|
||||
c.calcSize = havePreferredWidth ? c.hintSize : c.minSize;
|
||||
c.calcSize += (toDistribute.x() * c.calcStretch) / columnStretchTotal;
|
||||
|
||||
// compute running total of size to give us the actual start coordinate
|
||||
if (col > 0) {
|
||||
c.calcStart = _columns.at(col - 1).calcStart + _columns.at(col - 1).calcSize + c.padding;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: apply height-for-width to all items, to calculate real heights now
|
||||
|
||||
// re-calcualate row min/preferred now? Or is it not dependant?
|
||||
|
||||
for (auto row = 0; row < _rows.size(); ++row) {
|
||||
auto& r = _rows[row];
|
||||
r.calcSize = havePreferredHeight ? r.hintSize : r.minSize;
|
||||
r.calcSize += (toDistribute.y() * r.calcStretch) / rowStretchTotal;
|
||||
|
||||
if (row > 0) {
|
||||
r.calcStart = _rows.at(row - 1).calcStart + _rows.at(row - 1).calcSize + r.padding;
|
||||
}
|
||||
}
|
||||
|
||||
// set layout-ed geometry on items
|
||||
for (auto& i : _layout_items) {
|
||||
const auto loc = i.layout_item->gridLocation();
|
||||
|
||||
// from the end location, we can use the start+size to ensure all padding
|
||||
// etc, in between was covered, since we already summed those above.
|
||||
const auto end = i.layout_item->gridEnd();
|
||||
const auto& endRow = _rows.at(end.y());
|
||||
const auto& endCol = _columns.at(end.x());
|
||||
|
||||
// note this is building the rect as a min,max pair, and not as min+(w,h)
|
||||
// as we normally do.
|
||||
const auto geom = SGRecti{
|
||||
SGVec2i{_columns.at(loc.x()).calcStart,
|
||||
_rows.at(loc.y()).calcStart},
|
||||
SGVec2i{endCol.calcStart + endCol.calcSize,
|
||||
endRow.calcStart + endRow.calcSize},
|
||||
};
|
||||
// set geometry : alignment is handled internally
|
||||
i.layout_item->setGeometry(geom);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void GridLayout::visibilityChanged(bool visible)
|
||||
{
|
||||
for (size_t i = 0; i < _layout_items.size(); ++i)
|
||||
callSetVisibleInternal(_layout_items[i].layout_item.get(), visible);
|
||||
}
|
||||
|
||||
bool GridLayout::hasHeightForWidth() const
|
||||
{
|
||||
// FIXME
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
} // namespace canvas
|
||||
} // namespace simgear
|
117
simgear/canvas/layout/GridLayout.hxx
Executable file
117
simgear/canvas/layout/GridLayout.hxx
Executable file
@ -0,0 +1,117 @@
|
||||
// GridLayout.hxx - grid layout for Canvas, closely
|
||||
// modelled on the equivalent layouts in Gtk/Qt
|
||||
// Copyright (C) 2022 James Turner
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Layout.hxx"
|
||||
|
||||
namespace simgear {
|
||||
namespace canvas {
|
||||
|
||||
/**
|
||||
* Align LayoutItems in a grid (which fills a rectangular are)
|
||||
* Each column / row has consistent sizing for its items
|
||||
*/
|
||||
class GridLayout : public Layout
|
||||
{
|
||||
public:
|
||||
GridLayout();
|
||||
~GridLayout();
|
||||
|
||||
void setDimensions(const SGVec2i& dim);
|
||||
size_t numRows() const;
|
||||
size_t numColumns() const;
|
||||
|
||||
void addItem(const LayoutItemRef& item, int column, int row, int colSpan = 1, int rowSpan = 1);
|
||||
|
||||
virtual void addItem(const LayoutItemRef& item);
|
||||
|
||||
virtual size_t count() const;
|
||||
virtual LayoutItemRef itemAt(size_t index);
|
||||
virtual LayoutItemRef takeAt(size_t index);
|
||||
virtual void clear();
|
||||
|
||||
virtual void setSpacing(int spacing);
|
||||
virtual int spacing() const;
|
||||
|
||||
void invalidate() override;
|
||||
|
||||
virtual bool hasHeightForWidth() const;
|
||||
|
||||
virtual void setCanvas(const CanvasWeakPtr& canvas);
|
||||
|
||||
void setRowStretch(size_t index, int stretch);
|
||||
void setColumnStretch(size_t index, int stretch);
|
||||
|
||||
protected:
|
||||
int _padding = 5;
|
||||
|
||||
struct ItemData {
|
||||
LayoutItemRef layout_item;
|
||||
SGVec2i size; //!< layouted size
|
||||
bool visible : 1,
|
||||
has_align : 1, //!< Has alignment factor set (!= AlignFill)
|
||||
has_hfw : 1, //!< height for width
|
||||
done : 1; //!< layouting done
|
||||
|
||||
/** Clear values (reset to default/empty state) */
|
||||
void reset();
|
||||
|
||||
int hfw(int w) const;
|
||||
int mhfw(int w) const;
|
||||
|
||||
bool containsCell(const SGVec2i& cell) const;
|
||||
};
|
||||
|
||||
struct RowColumnData {
|
||||
int stretch = 0;
|
||||
int minSize = 0;
|
||||
int hintSize = 0;
|
||||
int maxSize = 0;
|
||||
int calcStretch = 0;
|
||||
int calcSize = 0;
|
||||
int calcStart = 0;
|
||||
bool hasVisible = false;
|
||||
///<padding preceeding this row/col. Zero for first row/col, or if there are no visible items
|
||||
///this ensures spacing items or hidden items don't cause double padding
|
||||
int padding = 0;
|
||||
|
||||
void resetSizeData();
|
||||
};
|
||||
|
||||
using Axis = std::vector<RowColumnData>;
|
||||
mutable Axis _rows, _columns;
|
||||
|
||||
using LayoutItems = std::vector<ItemData>;
|
||||
|
||||
using CellTable = std::vector<int>;
|
||||
|
||||
mutable LayoutItems _layout_items;
|
||||
SGVec2i _dimensions;
|
||||
mutable CellTable _cells; // NxM lookup into _layoutItems
|
||||
|
||||
|
||||
void updateCells();
|
||||
void updateSizeHints() const;
|
||||
SGVec2i sizeHintImpl() const override;
|
||||
SGVec2i minimumSizeImpl() const override;
|
||||
SGVec2i maximumSizeImpl() const override;
|
||||
|
||||
|
||||
void doLayout(const SGRecti& geom) override;
|
||||
|
||||
void visibilityChanged(bool visible) override;
|
||||
|
||||
LayoutItems::iterator firstInRow(int row);
|
||||
|
||||
LayoutItems::iterator itemInCell(const SGVec2i& cell);
|
||||
|
||||
SGVec2i innerFindUnusedLocation(const SGVec2i& curLoc);
|
||||
};
|
||||
|
||||
typedef SGSharedPtr<GridLayout> GridLayoutRef;
|
||||
|
||||
} // namespace canvas
|
||||
} // namespace simgear
|
@ -61,51 +61,6 @@ namespace canvas
|
||||
: LayoutItem::alignmentRect(geom);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void Layout::ItemData::reset()
|
||||
{
|
||||
layout_item = 0;
|
||||
size_hint = 0;
|
||||
min_size = 0;
|
||||
max_size = 0;
|
||||
padding_orig= 0;
|
||||
padding = 0;
|
||||
size = 0;
|
||||
stretch = 0;
|
||||
visible = false;
|
||||
has_align = false;
|
||||
has_hfw = false;
|
||||
done = false;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
int Layout::ItemData::hfw(int w) const
|
||||
{
|
||||
if( has_hfw )
|
||||
return layout_item->heightForWidth(w);
|
||||
else
|
||||
return layout_item->sizeHint().y();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
int Layout::ItemData::mhfw(int w) const
|
||||
{
|
||||
if( has_hfw )
|
||||
return layout_item->minimumHeightForWidth(w);
|
||||
else
|
||||
return layout_item->minimumSize().y();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
Layout::Layout():
|
||||
_num_not_done(0),
|
||||
_sum_stretch(0),
|
||||
_space_stretch(0),
|
||||
_space_left(0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void Layout::contentsRectChanged(const SGRecti& rect)
|
||||
{
|
||||
@ -114,225 +69,6 @@ namespace canvas
|
||||
_flags &= ~LAYOUT_DIRTY;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void Layout::distribute(std::vector<ItemData>& items, const ItemData& space)
|
||||
{
|
||||
const int num_children = static_cast<int>(items.size());
|
||||
_num_not_done = 0;
|
||||
|
||||
SG_LOG( SG_GUI,
|
||||
SG_DEBUG,
|
||||
"Layout::distribute(" << space.size << "px for "
|
||||
<< num_children << " items, s.t."
|
||||
<< " min=" << space.min_size
|
||||
<< ", hint=" << space.size_hint
|
||||
<< ", max=" << space.max_size << ")" );
|
||||
|
||||
if( space.size < space.min_size )
|
||||
{
|
||||
// TODO
|
||||
SG_LOG( SG_GUI, SG_WARN, "Layout: not enough size (not implemented)");
|
||||
}
|
||||
else if( space.size < space.max_size )
|
||||
{
|
||||
_sum_stretch = 0;
|
||||
_space_stretch = 0;
|
||||
|
||||
bool less_then_hint = space.size < space.size_hint;
|
||||
|
||||
// Give min_size/size_hint to all items
|
||||
_space_left = space.size
|
||||
- (less_then_hint ? space.min_size : space.size_hint);
|
||||
for(int i = 0; i < num_children; ++i)
|
||||
{
|
||||
ItemData& d = items[i];
|
||||
if( !d.visible )
|
||||
continue;
|
||||
|
||||
d.size = less_then_hint ? d.min_size : d.size_hint;
|
||||
d.padding = d.padding_orig;
|
||||
d.done = d.size >= (less_then_hint ? d.size_hint : d.max_size);
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
i << ") initial=" << d.size
|
||||
<< ", min=" << d.min_size
|
||||
<< ", hint=" << d.size_hint
|
||||
<< ", max=" << d.max_size
|
||||
);
|
||||
|
||||
if( d.done )
|
||||
continue;
|
||||
_num_not_done += 1;
|
||||
|
||||
if( d.stretch > 0 )
|
||||
{
|
||||
_sum_stretch += d.stretch;
|
||||
_space_stretch += d.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute remaining space to increase the size of each item up to its
|
||||
// size_hint/max_size
|
||||
while( _space_left > 0 )
|
||||
{
|
||||
if( _num_not_done <= 0 )
|
||||
{
|
||||
SG_LOG(SG_GUI, SG_WARN, "space left, but no more items?");
|
||||
break;
|
||||
}
|
||||
|
||||
int space_per_element = std::max(1, _space_left / _num_not_done);
|
||||
|
||||
SG_LOG(SG_GUI, SG_DEBUG, "space/element=" << space_per_element);
|
||||
|
||||
for(int i = 0; i < num_children; ++i)
|
||||
{
|
||||
ItemData& d = items[i];
|
||||
if( !d.visible )
|
||||
continue;
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
i << ") left=" << _space_left
|
||||
<< ", not_done=" << _num_not_done
|
||||
<< ", sum=" << _sum_stretch
|
||||
<< ", stretch=" << _space_stretch
|
||||
<< ", stretch/unit=" << _space_stretch / std::max(1, _sum_stretch)
|
||||
);
|
||||
|
||||
if( d.done )
|
||||
continue;
|
||||
|
||||
if( _sum_stretch > 0 && d.stretch <= 0 )
|
||||
d.done = true;
|
||||
else
|
||||
{
|
||||
int target_size = 0;
|
||||
int max_size = less_then_hint ? d.size_hint : d.max_size;
|
||||
|
||||
if( _sum_stretch > 0 )
|
||||
{
|
||||
target_size = (d.stretch * (_space_left + _space_stretch))
|
||||
/ _sum_stretch;
|
||||
|
||||
// Item would be smaller than minimum size or larger than maximum
|
||||
// size, so just keep bounded size and ignore stretch factor
|
||||
if( target_size <= d.size || target_size >= max_size )
|
||||
{
|
||||
d.done = true;
|
||||
_sum_stretch -= d.stretch;
|
||||
_space_stretch -= d.size;
|
||||
|
||||
if( target_size >= max_size )
|
||||
target_size = max_size;
|
||||
else
|
||||
target_size = d.size;
|
||||
}
|
||||
else
|
||||
_space_stretch += target_size - d.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Give space evenly to all remaining elements in this round
|
||||
target_size = d.size + std::min(_space_left, space_per_element);
|
||||
|
||||
if( target_size >= max_size )
|
||||
{
|
||||
d.done = true;
|
||||
target_size = max_size;
|
||||
}
|
||||
}
|
||||
|
||||
int old_size = d.size;
|
||||
d.size = target_size;
|
||||
_space_left -= d.size - old_size;
|
||||
}
|
||||
|
||||
if( d.done )
|
||||
{
|
||||
_num_not_done -= 1;
|
||||
|
||||
if( _sum_stretch <= 0 && d.stretch > 0 )
|
||||
// Distribute remaining space evenly to all non-stretchable items
|
||||
// in a new round
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_space_left = space.size - space.max_size;
|
||||
int num_align = 0;
|
||||
for(int i = 0; i < num_children; ++i)
|
||||
{
|
||||
if( !items[i].visible )
|
||||
continue;
|
||||
|
||||
_num_not_done += 1;
|
||||
|
||||
if( items[i].has_align )
|
||||
num_align += 1;
|
||||
}
|
||||
|
||||
SG_LOG(
|
||||
SG_GUI,
|
||||
SG_DEBUG,
|
||||
"Distributing excess space:"
|
||||
" not_done=" << _num_not_done
|
||||
<< ", num_align=" << num_align
|
||||
<< ", space_left=" << _space_left
|
||||
);
|
||||
|
||||
for(int i = 0; i < num_children; ++i)
|
||||
{
|
||||
ItemData& d = items[i];
|
||||
if( !d.visible )
|
||||
continue;
|
||||
|
||||
d.padding = d.padding_orig;
|
||||
d.size = d.max_size;
|
||||
|
||||
int space_add = 0;
|
||||
|
||||
if( d.has_align )
|
||||
{
|
||||
// Equally distribute superfluous space and let each child items
|
||||
// alignment handle the exact usage.
|
||||
space_add = _space_left / num_align;
|
||||
num_align -= 1;
|
||||
|
||||
d.size += space_add;
|
||||
}
|
||||
else if( num_align <= 0 )
|
||||
{
|
||||
// Add superfluous space as padding
|
||||
space_add = _space_left
|
||||
// Padding after last child...
|
||||
/ (_num_not_done + 1);
|
||||
_num_not_done -= 1;
|
||||
|
||||
d.padding += space_add;
|
||||
}
|
||||
|
||||
_space_left -= space_add;
|
||||
}
|
||||
}
|
||||
|
||||
SG_LOG(SG_GUI, SG_DEBUG, "distribute:");
|
||||
for(int i = 0; i < num_children; ++i)
|
||||
{
|
||||
ItemData const& d = items[i];
|
||||
if( d.visible )
|
||||
SG_LOG(SG_GUI, SG_DEBUG, i << ") pad=" << d.padding
|
||||
<< ", size= " << d.size);
|
||||
else
|
||||
SG_LOG(SG_GUI, SG_DEBUG, i << ") [hidden]");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace canvas
|
||||
} // namespace simgear
|
||||
|
@ -87,29 +87,7 @@ namespace canvas
|
||||
LAST_FLAG = LayoutItem::LAST_FLAG
|
||||
};
|
||||
|
||||
struct ItemData
|
||||
{
|
||||
LayoutItemRef layout_item;
|
||||
int size_hint,
|
||||
min_size,
|
||||
max_size,
|
||||
padding_orig, //!< original padding as specified by the user
|
||||
padding, //!< padding before element (layouted)
|
||||
size, //!< layouted size
|
||||
stretch; //!< stretch factor
|
||||
bool visible : 1,
|
||||
has_align: 1, //!< Has alignment factor set (!= AlignFill)
|
||||
has_hfw : 1, //!< height for width
|
||||
done : 1; //!< layouting done
|
||||
|
||||
/** Clear values (reset to default/empty state) */
|
||||
void reset();
|
||||
|
||||
int hfw(int w) const;
|
||||
int mhfw(int w) const;
|
||||
};
|
||||
|
||||
Layout();
|
||||
Layout() = default;
|
||||
|
||||
virtual void contentsRectChanged(const SGRecti& rect);
|
||||
|
||||
@ -118,19 +96,6 @@ namespace canvas
|
||||
*/
|
||||
virtual void doLayout(const SGRecti& geom) = 0;
|
||||
|
||||
/**
|
||||
* Distribute the available @a space to all @a items
|
||||
*/
|
||||
void distribute(std::vector<ItemData>& items, const ItemData& space);
|
||||
|
||||
private:
|
||||
|
||||
int _num_not_done, //!< number of children not layouted yet
|
||||
_sum_stretch, //!< sum of stretch factors of all not yet layouted
|
||||
// children
|
||||
_space_stretch,//!< space currently assigned to all not yet layouted
|
||||
// stretchable children
|
||||
_space_left; //!< remaining space not used by any child yet
|
||||
|
||||
};
|
||||
|
||||
|
@ -86,12 +86,6 @@ namespace canvas
|
||||
invalidate();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
LayoutItem::~LayoutItem()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void LayoutItem::setContentsMargins(const Margins& margins)
|
||||
{
|
||||
@ -107,6 +101,24 @@ namespace canvas
|
||||
_margins.b = bottom;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
SGVec2i LayoutItem::gridLocation() const
|
||||
{
|
||||
return _gridLocation;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
SGVec2i LayoutItem::gridSpan() const
|
||||
{
|
||||
return _span;
|
||||
}
|
||||
|
||||
SGVec2i LayoutItem::gridEnd() const
|
||||
{
|
||||
return _gridLocation + _span + SGVec2i{-1, -1};
|
||||
}
|
||||
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void LayoutItem::setContentsMargin(int margin)
|
||||
{
|
||||
@ -301,6 +313,18 @@ namespace canvas
|
||||
return SGRecti(pos, pos + size);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void LayoutItem::setGridLocation(const SGVec2i& loc)
|
||||
{
|
||||
_gridLocation = loc;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void LayoutItem::setGridSpan(const SGVec2i& span)
|
||||
{
|
||||
_span = span;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
void LayoutItem::setCanvas(const CanvasWeakPtr& canvas)
|
||||
{
|
||||
|
@ -107,7 +107,7 @@ namespace canvas
|
||||
static const SGVec2i MAX_SIZE;
|
||||
|
||||
LayoutItem();
|
||||
virtual ~LayoutItem();
|
||||
virtual ~LayoutItem() = default;
|
||||
|
||||
/**
|
||||
* Set the margins to use by the layout system around the item.
|
||||
@ -222,6 +222,28 @@ namespace canvas
|
||||
void show() { setVisible(true); }
|
||||
void hide() { setVisible(false); }
|
||||
|
||||
/**
|
||||
* The desired row/column of this item, if added to a grid layout.
|
||||
* For single-dimensional layouts, the X value is the position in the layout
|
||||
*/
|
||||
SGVec2i gridLocation() const;
|
||||
|
||||
/**
|
||||
* The rows/columns spanned by this item, if added into a grid layout
|
||||
*/
|
||||
SGVec2i gridSpan() const;
|
||||
|
||||
void setGridLocation(const SGVec2i& loc);
|
||||
|
||||
void setGridSpan(const SGVec2i& span);
|
||||
|
||||
|
||||
/**
|
||||
* lower-right corner of this item, in the grid: computed as gridLocation + gridSpan - 1
|
||||
* in each axis.
|
||||
*/
|
||||
SGVec2i gridEnd() const;
|
||||
|
||||
/**
|
||||
* Mark all cached data as invalid and require it to be recalculated.
|
||||
*/
|
||||
@ -309,6 +331,8 @@ namespace canvas
|
||||
SGRecti _geometry;
|
||||
Margins _margins;
|
||||
uint8_t _alignment;
|
||||
SGVec2i _gridLocation = {-1, -1};
|
||||
SGVec2i _span = {1, 1};
|
||||
|
||||
mutable uint32_t _flags;
|
||||
mutable SGVec2i _size_hint,
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
#include "BoxLayout.hxx"
|
||||
#include "GridLayout.hxx"
|
||||
#include "NasalWidget.hxx"
|
||||
|
||||
#include <simgear/debug/logstream.hxx>
|
||||
@ -32,7 +33,7 @@ struct SetLogLevelFixture
|
||||
{
|
||||
SetLogLevelFixture()
|
||||
{
|
||||
sglog().set_log_priority(SG_DEBUG);
|
||||
// sglog().set_log_priority(SG_DEBUG);
|
||||
}
|
||||
};
|
||||
BOOST_GLOBAL_FIXTURE(SetLogLevelFixture);
|
||||
@ -44,13 +45,17 @@ class TestWidget:
|
||||
public sc::LayoutItem
|
||||
{
|
||||
public:
|
||||
TestWidget( const SGVec2i& min_size,
|
||||
TestWidget(const SGVec2i& min_size,
|
||||
const SGVec2i& size_hint,
|
||||
const SGVec2i& max_size = MAX_SIZE )
|
||||
const SGVec2i& max_size = MAX_SIZE,
|
||||
const SGVec2i& gridLoc = {0, 0},
|
||||
const SGVec2i& span = {1, 1})
|
||||
{
|
||||
_size_hint = size_hint;
|
||||
_min_size = min_size;
|
||||
_max_size = max_size;
|
||||
_gridLocation = gridLoc;
|
||||
_span = span;
|
||||
}
|
||||
|
||||
TestWidget(const TestWidget& rhs)
|
||||
@ -58,6 +63,8 @@ class TestWidget:
|
||||
_size_hint = rhs._size_hint;
|
||||
_min_size = rhs._min_size;
|
||||
_max_size = rhs._max_size;
|
||||
_gridLocation = rhs._gridLocation;
|
||||
_span = rhs._span;
|
||||
}
|
||||
|
||||
void setMinSize(const SGVec2i& size) { _min_size = size; }
|
||||
@ -733,3 +740,70 @@ BOOST_AUTO_TEST_CASE( nasal_widget )
|
||||
w->setVisible(false);
|
||||
BOOST_CHECK_EQUAL(w->geometry(), SGRecti(0, 0, -1, -1));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
BOOST_AUTO_TEST_CASE(gridlayout_layout)
|
||||
{
|
||||
sc::GridLayoutRef grid(new sc::GridLayout);
|
||||
|
||||
BOOST_CHECK_EQUAL(grid->count(), 0);
|
||||
BOOST_CHECK(!grid->itemAt(0));
|
||||
BOOST_CHECK(!grid->takeAt(0));
|
||||
|
||||
TestWidgetRef w1(new TestWidget(SGVec2i(16, 16),
|
||||
SGVec2i(32, 32),
|
||||
SGVec2i(9999, 100))),
|
||||
w2(new TestWidget(*w1)),
|
||||
w3(new TestWidget(*w1)),
|
||||
w4(new TestWidget(*w1));
|
||||
|
||||
w1->setMaxSize({9999, 32});
|
||||
grid->addItem(w1);
|
||||
BOOST_CHECK_EQUAL(grid->count(), 1);
|
||||
BOOST_CHECK_EQUAL(grid->itemAt(0), w1);
|
||||
BOOST_CHECK_EQUAL(w1->getParent(), grid);
|
||||
|
||||
grid->addItem(w2, 1, 1);
|
||||
grid->addItem(w3, 2, 1);
|
||||
grid->addItem(w4, 0, 2, 3 /* col span */, 1);
|
||||
|
||||
grid->setGeometry(SGRecti(0, 0, 160, 130));
|
||||
|
||||
BOOST_CHECK_EQUAL(w1->geometry(), SGRecti(0, 4, 50, 32));
|
||||
BOOST_CHECK_EQUAL(w2->geometry(), SGRecti(55, 45, 50, 40));
|
||||
|
||||
// check width includes padding of spanned columns
|
||||
BOOST_CHECK_EQUAL(w4->geometry(), SGRecti(0, 90, 160, 40));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
BOOST_AUTO_TEST_CASE(gridlayout_min_size_layout)
|
||||
{
|
||||
sc::GridLayoutRef grid(new sc::GridLayout);
|
||||
grid->setSpacing(4);
|
||||
|
||||
TestWidgetRef w1(new TestWidget(SGVec2i(16, 16),
|
||||
SGVec2i(32, 32),
|
||||
SGVec2i(9999, 96))),
|
||||
w2(new TestWidget(*w1)),
|
||||
w3(new TestWidget(*w1)),
|
||||
w4(new TestWidget(*w1));
|
||||
|
||||
w1->setMinSize({72, 24});
|
||||
w1->setSizeHint({72, 32});
|
||||
w1->setGridSpan({1, 2});
|
||||
|
||||
w2->setMinSize({72, 48});
|
||||
w2->setSizeHint({96, 64});
|
||||
|
||||
grid->addItem(w1);
|
||||
grid->addItem(w2, 1, 1);
|
||||
grid->addItem(w3, 2, 1);
|
||||
grid->addItem(w4, 0, 2, 2 /* col span */, 1);
|
||||
|
||||
grid->setGeometry(SGRecti(0, 0, 248, 148));
|
||||
|
||||
BOOST_CHECK_EQUAL(w1->geometry(), SGRecti(0, 0, 85, 96));
|
||||
BOOST_CHECK_EQUAL(w2->geometry(), SGRecti(89, 18, 109, 78));
|
||||
BOOST_CHECK_EQUAL(w4->geometry(), SGRecti(0, 100, 198, 46));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user