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:
James Turner 2022-02-15 16:53:30 +02:00
parent d146c0561f
commit 7ee427c202
10 changed files with 1189 additions and 338 deletions

View File

@ -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)

View File

@ -102,39 +102,74 @@ 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
typedef const int& (SGVec2i::*CoordGetter)() const;
CoordGetter _get_layout_coord, //!< getter for coordinate in layout
// direction
_get_fixed_coord; //!< getter for coordinate in secondary
// (fixed) direction
/** Clear values (reset to default/empty state) */
void reset();
int _padding;
Direction _direction;
int hfw(int w) const;
int mhfw(int w) const;
};
typedef std::vector<ItemData> LayoutItems;
using LayoutItems = std::vector<ItemData>;
mutable LayoutItems _layout_items;
mutable ItemData _layout_data;
/**
* Distribute the available @a space to all @a items
*/
void distribute(LayoutItems& items, const ItemData& space);
// Cache for last height-for-width query
mutable int _hfw_width,
_hfw_height,
_hfw_min_height;
void updateSizeHints() const;
void updateWFHCache(int w) const;
typedef const int& (SGVec2i::*CoordGetter)() const;
CoordGetter _get_layout_coord, //!< getter for coordinate in layout
// direction
_get_fixed_coord; //!< getter for coordinate in secondary
// (fixed) direction
virtual SGVec2i sizeHintImpl() const;
virtual SGVec2i minimumSizeImpl() const;
virtual SGVec2i maximumSizeImpl() const;
int _padding;
Direction _direction;
virtual int heightForWidthImpl(int w) const;
virtual int minimumHeightForWidthImpl(int w) const;
virtual void doLayout(const SGRecti& geom);
mutable LayoutItems _layout_items;
mutable ItemData _layout_data;
virtual void visibilityChanged(bool visible);
// Cache for last height-for-width query
mutable int _hfw_width,
_hfw_height,
_hfw_min_height;
void updateSizeHints() const;
void updateWFHCache(int w) const;
virtual SGVec2i sizeHintImpl() const;
virtual SGVec2i minimumSizeImpl() const;
virtual SGVec2i maximumSizeImpl() const;
virtual int heightForWidthImpl(int w) const;
virtual int minimumHeightForWidthImpl(int w) const;
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
};
/**

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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
};

View File

@ -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)
{

View File

@ -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,

View File

@ -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,
const SGVec2i& size_hint,
const SGVec2i& max_size = MAX_SIZE )
{
_size_hint = size_hint;
_min_size = min_size;
_max_size = max_size;
TestWidget(const SGVec2i& min_size,
const SGVec2i& size_hint,
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));
}