diff --git a/simgear/canvas/layout/BoxLayout.cxx b/simgear/canvas/layout/BoxLayout.cxx index d0684f41..98a99c53 100644 --- a/simgear/canvas/layout/BoxLayout.cxx +++ b/simgear/canvas/layout/BoxLayout.cxx @@ -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(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) diff --git a/simgear/canvas/layout/BoxLayout.hxx b/simgear/canvas/layout/BoxLayout.hxx index c5fe6d28..a20af92b 100644 --- a/simgear/canvas/layout/BoxLayout.hxx +++ b/simgear/canvas/layout/BoxLayout.hxx @@ -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 LayoutItems; + using LayoutItems = std::vector; - 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 }; /** diff --git a/simgear/canvas/layout/CMakeLists.txt b/simgear/canvas/layout/CMakeLists.txt index f498a812..c5230a71 100644 --- a/simgear/canvas/layout/CMakeLists.txt +++ b/simgear/canvas/layout/CMakeLists.txt @@ -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 diff --git a/simgear/canvas/layout/GridLayout.cxx b/simgear/canvas/layout/GridLayout.cxx new file mode 100644 index 00000000..76f64606 --- /dev/null +++ b/simgear/canvas/layout/GridLayout.cxx @@ -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 + +#include "GridLayout.hxx" +#include "SpacerItem.hxx" +#include + +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(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 diff --git a/simgear/canvas/layout/GridLayout.hxx b/simgear/canvas/layout/GridLayout.hxx new file mode 100755 index 00000000..15a52cba --- /dev/null +++ b/simgear/canvas/layout/GridLayout.hxx @@ -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; + ///