Files
MuditaOS/module-gui/gui/widgets/BoxLayout.cpp
Przemyslaw Brudny bff70a4ab3 [BH-1029] Refactored Time widgets with colon and content change
Refactored Time widget colon handling to be mapped with used
font. Added contentChange flag and handling to BoxLayout
for dynamic recalculation of child elements.
2021-10-18 18:15:40 +02:00

641 lines
20 KiB
C++

// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
#include "BoxLayout.hpp"
#include "BoxLayoutSizeStore.hpp"
#include <InputEvent.hpp>
#include <Label.hpp>
#include <log/log.hpp>
#include "assert.h"
namespace gui
{
BoxLayout::BoxLayout(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
: Rect(parent, x, y, w, h)
{
alignment = gui::Alignment(Alignment::Horizontal::None, Alignment::Vertical::None);
sizeStore = std::make_unique<BoxLayoutSizeStore>();
}
BoxLayout::BoxLayout() : BoxLayout(nullptr, 0, 0, 0, 0)
{}
bool BoxLayout::onInput(const InputEvent &inputEvent)
{
if (inputCallback && inputCallback(*this, inputEvent)) {
return true;
}
if (focusItem && focusItem->onInput(inputEvent)) {
return true;
}
if (handleNavigation(inputEvent)) {
return true;
}
if (borderCallback && isInputNavigation(inputEvent) && borderCallback(inputEvent)) {
outOfDrawAreaItems.clear();
return true;
}
// let item logic rule it
return false;
}
bool BoxLayout::onFocus(bool state)
{
if (state)
this->setVisible(state);
else
this->setFocusItem(nullptr);
this->setNavigation();
if (this->focusChangedCallback && state != focus) {
this->focusChangedCallback(*this);
}
return true;
}
void BoxLayout::resizeItems()
{}
void BoxLayout::setAlignment(const Alignment &value)
{
if (alignment != value) {
alignment = value;
resizeItems();
}
}
void BoxLayout::addWidget(Item *item)
{
Rect::addWidget(item);
resizeItems();
}
template <Axis axis> void BoxLayout::addWidget(Item *item)
{
Rect::addWidget(item);
resizeItems<axis>();
}
bool BoxLayout::removeWidget(Item *item)
{
bool ret = Rect::removeWidget(item);
outOfDrawAreaItems.remove(item);
resizeItems();
return ret;
}
bool BoxLayout::erase(Item *item)
{
auto ret = Item::erase(item);
outOfDrawAreaItems.remove(item);
resizeItems();
return ret;
}
void BoxLayout::erase()
{
Item::erase();
}
bool BoxLayout::empty() const noexcept
{
return children.empty();
}
void BoxLayout::setVisible(bool value, bool previous)
{
visible = value; // maybe use parent setVisible(...)? would be better but which one?
if (value == true) {
resizeItems(); // move items in box in proper places
setNavigation(); // set navigation through kids -> TODO handle out of last/first to parent
if (children.size()) { // set first visible kid as focused item - TODO should check for actionItems too...
/// this if back / front is crappy :|
if (previous) {
for (auto el = children.rbegin(); el != children.rend(); ++el) {
if ((*el)->isActive()) {
setFocusItem(*el);
break;
}
}
}
else {
for (auto &el : children) {
if (el->isActive()) {
setFocusItem(el);
break;
}
}
}
}
}
}
void BoxLayout::setVisible(bool value)
{
setVisible(value, false);
}
unsigned int BoxLayout::getVisibleChildrenCount()
{
assert(children.size() >= outOfDrawAreaItems.size());
return children.size() - outOfDrawAreaItems.size();
}
void BoxLayout::setReverseOrder(bool value)
{
reverseOrder = value;
}
bool BoxLayout::getReverseOrder()
{
return reverseOrder;
}
Length BoxLayout::getPrimarySize()
{
if (type == ItemType::HBOX) {
return getSize(Axis::X);
}
else if (type == ItemType::VBOX) {
return getSize(Axis::Y);
}
return 0;
}
Length BoxLayout::getPrimarySizeLeft()
{
if (type == ItemType::HBOX) {
return sizeLeft<Axis::X>(this, Area::Normal);
}
else if (type == ItemType::VBOX) {
return sizeLeft<Axis::Y>(this, Area::Normal);
}
return 0;
}
void BoxLayout::addToOutOfDrawAreaList(Item *it)
{
if (it->visible) {
outOfDrawAreaItems.push_back(it);
it->visible = false;
}
}
void BoxLayout::addFromOutOfDrawAreaList()
{
if (children.size() != 0) {
for (auto it : outOfDrawAreaItems) {
it->setVisible(true);
it->setFocusItem(nullptr);
}
}
outOfDrawAreaItems.clear();
resizeItems();
}
// space left distposition `first is better` tactics
// there could be other i.e. socialism: each element take in equal part up to it's max size
// not needed now == not implemented
template <Axis axis> void BoxLayout::resizeItems()
{
Position startingPosition = reverseOrder ? this->area().size(axis) : 0;
Position leftPosition = this->getSize(axis);
Length toSplit = sizeLeft<axis>(this);
for (auto &el : children) {
if (!el->visible)
continue;
auto axisItemPosition = 0;
auto orthogonalItemPosition = 0;
auto axisItemSize = 0;
auto calculatedResize = 0;
auto orthogonalItemSize = 0;
// 1. Calculate element axis resize.
calculatedResize = calculateElemResize<axis>(el, toSplit);
// 2. Check if new size in axis can be applied and set it or use element minimal size in axis.
axisItemSize = calculateElemAxisSize<axis>(el, calculatedResize, toSplit);
// 3. Calculate orthogonal axis size.
orthogonalItemSize = calculateElemOrtAxisSize<axis>(el);
// 4. Calculate element position in axis and apply in axis alignment.
axisItemPosition = calculateElemAxisPosition<axis>(el, axisItemSize, startingPosition, leftPosition);
// 5. Calculate element orthogonal axis position based on Layout alignment or on element alignment.
orthogonalItemPosition = calculateElemOrtAxisPosition<axis>(el, orthogonalItemSize);
// 6. If element still visible (not added to outOfDrawAreaList) set it Area with calculated values.
if (el->visible)
el->setAreaInAxis(axis, axisItemPosition, orthogonalItemPosition, axisItemSize, orthogonalItemSize);
}
Rect::updateDrawArea();
}
template <Axis axis> Length BoxLayout::calculateElemResize(Item *el, Length &toSplit)
{
auto grantedSize = sizeStore->get(el);
Length calculatedResize = 0;
// Check if element has stored requested size in axis.
if (!grantedSize.isZero()) {
calculatedResize = grantedSize.get(axis) < el->area(Area::Min).size(axis)
? 0
: grantedSize.get(axis) - el->area(Area::Min).size(axis);
// If requested size bigger than left size in layout push out last visible element in layout into
// outOfDrawAreaList.
if (sizeLeft<axis>(this) < calculatedResize) {
addToOutOfDrawAreaList(getLastVisibleElement());
toSplit = sizeLeft<axis>(this);
}
}
else {
// If not calculate possible size in axis from min-max difference.
calculatedResize = el->area(Area::Max).size(axis) < el->area(Area::Min).size(axis)
? 0
: el->area(Area::Max).size(axis) - el->area(Area::Min).size(axis);
}
return calculatedResize;
}
template <Axis axis> Length BoxLayout::calculateElemAxisSize(Item *el, Length calculatedResize, Length &toSplit)
{
Length axisItemSize = 0;
if (toSplit > 0 && calculatedResize > 0) {
axisItemSize = el->area(Area::Min).size(axis) + std::min(calculatedResize, toSplit);
toSplit -= std::min(calculatedResize, toSplit);
}
else {
axisItemSize = el->area(Area::Min).size(axis);
}
return axisItemSize;
}
template <Axis axis> Length BoxLayout::calculateElemOrtAxisSize(Item *el)
{
// Get maximum size that element in orthogonal axis can occupy in current layout size.
Length maxOrthogonalItemInParentSize =
static_cast<Position>(this->area(Area::Normal).size(orthogonal(axis))) <=
el->getMargins().getSumInAxis(orthogonal(axis))
? 0
: this->area(Area::Normal).size(orthogonal(axis)) - el->getMargins().getSumInAxis(orthogonal(axis));
// Get maximum size of element in orthogonal axis based on its min-max.
Length maxOrthogonalItemSize =
el->area(Area::Max).size(orthogonal(axis)) > el->area(Area::Min).size(orthogonal(axis))
? el->area(Area::Max).size(orthogonal(axis))
: el->area(Area::Min).size(orthogonal(axis));
// Return minimal from both max sizes.
return std::min(maxOrthogonalItemInParentSize, maxOrthogonalItemSize);
}
template <Axis axis>
Position BoxLayout::calculateElemAxisPosition(Item *el,
Length axisItemSize,
Position &startingPosition,
Position &leftPosition)
{
auto axisItemPosition = 0;
// Check if elements in axis can fit with margins in layout free space.
if (((Position)(axisItemSize + el->getMargins().getSumInAxis(axis))) <= leftPosition) {
if (reverseOrder) {
startingPosition -= el->getMargins().getMarginInAxis(axis, MarginInAxis::Second);
startingPosition -= axisItemSize;
axisItemPosition = startingPosition;
startingPosition -= el->getMargins().getMarginInAxis(axis, MarginInAxis::First);
}
if (!reverseOrder) {
startingPosition += el->getMargins().getMarginInAxis(axis, MarginInAxis::First);
axisItemPosition = startingPosition;
startingPosition += axisItemSize;
startingPosition += el->getMargins().getMarginInAxis(axis, MarginInAxis::Second);
}
leftPosition -= axisItemSize + el->getMargins().getSumInAxis(axis);
// Recalculate element axis position if axis alignment provided.
axisItemPosition = getAxisAlignmentValue<axis>(axisItemPosition, axisItemSize, el);
}
else {
// If not add it to outOfDrawAreaList.
addToOutOfDrawAreaList(el);
}
return axisItemPosition;
}
template <Axis axis> Position BoxLayout::calculateElemOrtAxisPosition(Item *el, Length orthogonalItemSize)
{
return el->getAxisAlignmentValue(orthogonal(axis), orthogonalItemSize);
}
template <Axis axis> Position BoxLayout::getAxisAlignmentValue(Position calcPos, Length calcSize, Item *el)
{
auto offset = sizeLeftWithoutElem<axis>(this, el, Area::Normal) <= calcSize
? 0
: sizeLeftWithoutElem<axis>(this, el, Area::Normal) - calcSize;
switch (getAlignment(axis).vertical) {
case gui::Alignment::Vertical::Top:
if (reverseOrder) {
return calcPos + el->getMargins().getSumInAxis(axis) - offset;
}
break;
case gui::Alignment::Vertical::Center:
if (reverseOrder) {
return calcPos - ((offset - el->getMargins().getSumInAxis(axis)) / 2);
}
else {
return calcPos + ((offset - el->getMargins().getSumInAxis(axis)) / 2);
}
break;
case gui::Alignment::Vertical::Bottom:
if (!reverseOrder) {
return calcPos - el->getMargins().getSumInAxis(axis) + offset;
}
break;
default:
break;
}
switch (getAlignment(axis).horizontal) {
case gui::Alignment::Horizontal::Left:
if (reverseOrder) {
return calcPos + el->getMargins().getSumInAxis(axis) - offset;
}
break;
case gui::Alignment::Horizontal::Center:
if (reverseOrder) {
return calcPos - ((offset - el->getMargins().getSumInAxis(axis)) / 2);
}
else {
return calcPos + ((offset - el->getMargins().getSumInAxis(axis)) / 2);
}
break;
case gui::Alignment::Horizontal::Right:
if (!reverseOrder) {
return calcPos - el->getMargins().getSumInAxis(axis) + offset;
}
break;
default:
break;
}
return calcPos;
}
std::list<Item *>::iterator BoxLayout::nextNavigationItem(std::list<Item *>::iterator from)
{
return std::find_if(from, this->children.end(), [](auto &el) -> bool {
if (el->isActive()) {
return true;
}
return false;
});
}
std::list<Item *>::iterator BoxLayout::getNavigationFocusedItem()
{
return std::find(this->children.begin(), this->children.end(), this->getFocusItem());
}
void BoxLayout::setNavigation()
{
auto previous = nextNavigationItem(children.begin()), next = children.end();
if (type == ItemType::VBOX) {
while ((previous != children.end()) &&
((next = nextNavigationItem(std::next(previous))) != std::end(children))) {
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::UP : NavigationDirection::DOWN,
*next);
(*next)->setNavigationItem(reverseOrder ? NavigationDirection::DOWN : NavigationDirection::UP,
*previous);
previous = next;
}
if (previous != children.end()) {
if ((*previous) != nullptr) {
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::UP : NavigationDirection::DOWN,
nullptr);
}
}
}
if (type == ItemType::HBOX) {
while ((previous != children.end()) &&
((next = nextNavigationItem(std::next(previous))) != std::end(children))) {
(*previous)->setNavigationItem(reverseOrder ? NavigationDirection::LEFT : NavigationDirection::RIGHT,
*next);
(*next)->setNavigationItem(reverseOrder ? NavigationDirection::RIGHT : NavigationDirection::LEFT,
*previous);
previous = next;
}
if (previous != children.end()) {
if ((*previous) != nullptr) {
(*previous)->setNavigationItem(
reverseOrder ? NavigationDirection::LEFT : NavigationDirection::RIGHT, nullptr);
}
}
}
}
template <Axis axis>
auto BoxLayout::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
{
if (parent != nullptr) {
auto [w, h] = requestSize(request_w, request_h);
request_w = std::min(w, (Length)request_w);
request_h = std::min(h, (Length)request_h);
}
auto el = std::find(children.begin(), children.end(), child);
if (el == std::end(children)) {
return {0, 0};
}
Size granted = {std::min((*el)->area(Area::Max).w, request_w), std::min((*el)->area(Area::Max).h, request_h)};
// Currently not used option for Layouts that don't push out objects.
if (!sizeStore->getReleaseSpaceFlag()) {
if ((granted.get(axis) + (*el)->getMargins().getSumInAxis(axis)) >=
sizeLeftWithoutElem<axis>(this, *el, Area::Min)) {
granted = Size((*el)->area(Area::Normal).w, (*el)->area(Area::Normal).h);
}
}
// If granted size decreased check if pushed out elements can fit
if (sizeStore->isSizeSmaller(*el, granted, axis)) {
addFromOutOfDrawAreaList();
}
sizeStore->store(*el, granted);
resizeItems(); // vs mark dirty
setNavigation();
if (parentOnRequestedResizeCallback != nullptr) {
parentOnRequestedResizeCallback();
}
return granted;
}
bool BoxLayout::setFocusOnElement(unsigned int elementNumber)
{
unsigned int i = 0;
bool success = false;
for (auto child : children) {
if (child->isActive()) {
if (elementNumber == i) {
child->setFocus(true);
focusItem = child;
success = true;
}
else {
child->setFocus(false);
}
++i;
}
}
return success;
}
void BoxLayout::setFocusOnLastElement()
{
auto last = true;
for (auto child = children.rbegin(); child != children.rend(); child++) {
if ((*child)->isActive() && last) {
(*child)->setFocus(true);
focusItem = (*child);
last = false;
}
else {
(*child)->setFocus(false);
}
}
}
unsigned int BoxLayout::getFocusItemIndex() const
{
auto index = 0;
auto focusItem = getFocusItem();
for (auto child : children) {
if (child == focusItem) {
break;
}
if (child->isActive()) {
index++;
}
}
return index;
}
Item *BoxLayout::getLastVisibleElement()
{
for (auto child = children.rbegin(); child != children.rend(); child++) {
if ((*child)->visible) {
return (*child);
}
}
return nullptr;
}
auto BoxLayout::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
{
addFromOutOfDrawAreaList();
resizeItems();
setNavigation();
return Item::onDimensionChanged(oldDim, newDim);
}
void BoxLayout::handleContentChanged()
{
// Check if there is parent and it is Layout
if (parent != nullptr && (dynamic_cast<gui::BoxLayout *>(parent) != nullptr)) {
// If so invalidate content and request Content change to parent
contentChanged = true;
Item::informContentChanged();
}
else {
resizeItems();
}
}
HBox::HBox() : BoxLayout()
{
type = ItemType::HBOX;
}
HBox::HBox(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
: BoxLayout(parent, x, y, w, h)
{
type = ItemType::HBOX;
}
void HBox::resizeItems()
{
BoxLayout::resizeItems<Axis::X>();
}
void HBox::addWidget(Item *item)
{
BoxLayout::addWidget<Axis::X>(item);
}
auto HBox::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
{
return BoxLayout::handleRequestResize<Axis::X>(child, request_w, request_h);
}
VBox::VBox() : BoxLayout()
{
type = ItemType::VBOX;
}
VBox::VBox(Item *parent, const uint32_t &x, const uint32_t &y, const uint32_t &w, const uint32_t &h)
: BoxLayout(parent, x, y, w, h)
{
type = ItemType::VBOX;
}
void VBox::resizeItems()
{
BoxLayout::resizeItems<Axis::Y>();
}
void VBox::addWidget(Item *item)
{
BoxLayout::addWidget<Axis::Y>(item);
}
auto VBox::handleRequestResize(const Item *child, Length request_w, Length request_h) -> Size
{
return BoxLayout::handleRequestResize<Axis::Y>(child, request_w, request_h);
}
} /* namespace gui */