// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // ComponentGrid.cpp // // Provides basic layout of components in an X*Y grid. // #include "components/ComponentGrid.h" #include "Settings.h" using namespace GridFlags; ComponentGrid::ComponentGrid(const glm::ivec2& gridDimensions) : mGridSize {gridDimensions} , mCursor {0, 0} { assert(gridDimensions.x > 0 && gridDimensions.y > 0); mCells.reserve(gridDimensions.x * gridDimensions.y); mColWidths = new float[gridDimensions.x]; mRowHeights = new float[gridDimensions.y]; for (int x = 0; x < gridDimensions.x; ++x) mColWidths[x] = 0; for (int y = 0; y < gridDimensions.y; ++y) mRowHeights[y] = 0; } ComponentGrid::~ComponentGrid() { delete[] mRowHeights; delete[] mColWidths; } float ComponentGrid::getColWidth(int col) { assert(col >= 0 && col < mGridSize.x); if (mColWidths[col] != 0) return mColWidths[col] * mSize.x; // Calculate automatic width. float freeWidthPerc {1.0}; int between {0}; for (int x = 0; x < mGridSize.x; ++x) { freeWidthPerc -= mColWidths[x]; // If it's 0 it won't do anything. if (mColWidths[x] == 0) ++between; } return (freeWidthPerc * mSize.x) / static_cast(between); } float ComponentGrid::getRowHeight(int row) { assert(row >= 0 && row < mGridSize.y); if (mRowHeights[row] != 0) return mRowHeights[row] * mSize.y; // Calculate automatic height. float freeHeightPerc = 1; int between = 0; for (int y = 0; y < mGridSize.y; ++y) { freeHeightPerc -= mRowHeights[y]; // If it's 0 it won't do anything. if (mRowHeights[y] == 0) ++between; } return (freeHeightPerc * mSize.y) / static_cast(between); } void ComponentGrid::setColWidthPerc(int col, float width, bool update) { assert(col >= 0 && col < mGridSize.x); mColWidths[col] = width; if (update) onSizeChanged(); } void ComponentGrid::setRowHeightPerc(int row, float height, bool update) { assert(height >= 0 && height <= 1); assert(row >= 0 && row < mGridSize.y); mRowHeights[row] = height; if (update) onSizeChanged(); } void ComponentGrid::setEntry(const std::shared_ptr& comp, const glm::ivec2& pos, bool canFocus, bool resize, const glm::ivec2& size, unsigned int border, GridFlags::UpdateType updateType) { assert(pos.x >= 0 && pos.x < mGridSize.x && pos.y >= 0 && pos.y < mGridSize.y); assert(comp != nullptr); assert(comp->getParent() == nullptr); GridEntry entry(pos, size, comp, canFocus, resize, updateType, border); mCells.push_back(entry); addChild(comp.get()); if (!cursorValid() && canFocus) { auto origCursor = mCursor; mCursor = pos; onCursorMoved(origCursor, mCursor); } updateCellComponent(mCells.back()); updateSeparators(); } bool ComponentGrid::removeEntry(const std::shared_ptr& comp) { for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { if (it->component == comp) { removeChild(comp.get()); mCells.erase(it); return true; } } return false; } void ComponentGrid::updateCellComponent(const GridEntry& cell) { // Size. glm::vec2 size {0.0f}; for (int x = cell.pos.x; x < cell.pos.x + cell.dim.x; ++x) size.x += getColWidth(x); for (int y = cell.pos.y; y < cell.pos.y + cell.dim.y; ++y) size.y += getRowHeight(y); if (cell.resize && size != glm::vec2 {} && cell.component->getSize() != size) cell.component->setSize(size); // Find top left corner. glm::vec3 pos {}; for (int x = 0; x < cell.pos.x; ++x) pos.x += getColWidth(x); for (int y = 0; y < cell.pos.y; ++y) pos.y += getRowHeight(y); // Center component. pos.x = pos.x + (size.x - cell.component->getSize().x) / 2.0f; pos.y = pos.y + (size.y - cell.component->getSize().y) / 2.0f; cell.component->setPosition(pos); } void ComponentGrid::updateSeparators() { mSeparators.clear(); bool drawAll = Settings::getInstance()->getBool("DebugGrid"); glm::vec2 pos; glm::vec2 size; for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { if (!it->border && !drawAll) continue; // Find component position + size. pos = glm::vec2 {}; size = glm::vec2 {}; for (int x = 0; x < it->pos.x; ++x) pos[0] += getColWidth(x); for (int y = 0; y < it->pos.y; ++y) pos[1] += getRowHeight(y); for (int x = it->pos.x; x < it->pos.x + it->dim.x; ++x) size[0] += getColWidth(x); for (int y = it->pos.y; y < it->pos.y + it->dim.y; ++y) size[1] += getRowHeight(y); if (size == glm::vec2 {}) return; if (it->border & BORDER_TOP || drawAll) { std::vector coordVector; coordVector.push_back(pos.x); coordVector.push_back(pos.y); coordVector.push_back(size.x); coordVector.push_back(1.0f * Renderer::getScreenHeightModifier()); mSeparators.push_back(coordVector); } if (it->border & BORDER_BOTTOM || drawAll) { std::vector coordVector; coordVector.push_back(pos.x); coordVector.push_back(pos.y + size.y); coordVector.push_back(size.x); coordVector.push_back(1.0f * Renderer::getScreenHeightModifier()); mSeparators.push_back(coordVector); } if (it->border & BORDER_LEFT || drawAll) { std::vector coordVector; coordVector.push_back(pos.x); coordVector.push_back(pos.y); coordVector.push_back(1.0f * Renderer::getScreenWidthModifier()); coordVector.push_back(size.y); mSeparators.push_back(coordVector); } if (it->border & BORDER_RIGHT || drawAll) { std::vector coordVector; coordVector.push_back(pos.x + size.x); coordVector.push_back(pos.y); coordVector.push_back(1.0f * Renderer::getScreenWidthModifier()); coordVector.push_back(size.y); mSeparators.push_back(coordVector); } } } void ComponentGrid::onSizeChanged() { for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) updateCellComponent(*it); updateSeparators(); } const ComponentGrid::GridEntry* ComponentGrid::getCellAt(int x, int y) const { assert(x >= 0 && x < mGridSize.x && y >= 0 && y < mGridSize.y); for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { int xmin = it->pos.x; int xmax = xmin + it->dim.x; int ymin = it->pos.y; int ymax = ymin + it->dim.y; if (x >= xmin && y >= ymin && x < xmax && y < ymax) return &(*it); } return nullptr; } bool ComponentGrid::input(InputConfig* config, Input input) { const GridEntry* cursorEntry = getCellAt(mCursor); if (cursorEntry && cursorEntry->component->input(config, input)) return true; if (!input.value) return false; bool withinBoundary = false; if (config->isMappedLike("down", input)) withinBoundary = moveCursor(glm::ivec2 {0, 1}); if (config->isMappedLike("up", input)) withinBoundary = moveCursor(glm::ivec2 {0, -1}); if (config->isMappedLike("left", input)) withinBoundary = moveCursor(glm::ivec2 {-1, 0}); if (config->isMappedLike("right", input)) withinBoundary = moveCursor(glm::ivec2 {1, 0}); if (!withinBoundary && mPastBoundaryCallback) return mPastBoundaryCallback(config, input); return withinBoundary; } void ComponentGrid::resetCursor() { if (!mCells.size()) return; for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { if (it->canFocus) { glm::ivec2 origCursor = mCursor; mCursor = it->pos; onCursorMoved(origCursor, mCursor); break; } } } bool ComponentGrid::moveCursor(glm::ivec2 dir) { assert(dir.x || dir.y); const glm::ivec2 origCursor {mCursor}; const GridEntry* currentCursorEntry = getCellAt(mCursor); glm::ivec2 searchAxis(dir.x == 0, dir.y == 0); // Logic to handle entries that span several cells. if (currentCursorEntry->dim.x > 1) { if (dir.x < 0 && currentCursorEntry->pos.x == 0 && mCursor.x > currentCursorEntry->pos.x) { onCursorMoved(mCursor, glm::ivec2 {0, mCursor.y}); mCursor.x = 0; return false; } if (dir.x > 0 && currentCursorEntry->pos.x + currentCursorEntry->dim.x == mGridSize.x && mCursor.x < currentCursorEntry->pos.x + currentCursorEntry->dim.x - 1) { onCursorMoved(mCursor, glm::ivec2 {mGridSize.x - 1, mCursor.y}); mCursor.x = mGridSize.x - 1; return false; } if (dir.x > 0 && mCursor.x != currentCursorEntry->pos.x + currentCursorEntry->dim.x - 1) dir.x = currentCursorEntry->dim.x - (mCursor.x - currentCursorEntry->pos.x); else if (dir.x < 0 && mCursor.x != currentCursorEntry->pos.x) dir.x = -(mCursor.x - currentCursorEntry->pos.x + 1); } if (currentCursorEntry->dim.y > 1) { if (dir.y > 0 && mCursor.y != currentCursorEntry->pos.y + currentCursorEntry->dim.y - 1) dir.y = currentCursorEntry->dim.y - (mCursor.y - currentCursorEntry->pos.y); else if (dir.y < 0 && mCursor.y != currentCursorEntry->pos.y) dir.y = -(mCursor.y - currentCursorEntry->pos.y + 1); } while (mCursor.x >= 0 && mCursor.y >= 0 && mCursor.x < mGridSize.x && mCursor.y < mGridSize.y) { mCursor = mCursor + dir; glm::ivec2 curDirPos {mCursor}; const GridEntry* cursorEntry; // Spread out on search axis+ while (mCursor.x < mGridSize.x && mCursor.y < mGridSize.y && mCursor.x >= 0 && mCursor.y >= 0) { cursorEntry = getCellAt(mCursor); // Multi-cell entries. if (cursorEntry != nullptr) { if (dir.x < 0 && cursorEntry->dim.x > 1) mCursor.x = getCellAt(origCursor)->pos.x - cursorEntry->dim.x; if (dir.y < 0 && cursorEntry->dim.y > 1) mCursor.y = getCellAt(origCursor)->pos.y - cursorEntry->dim.y; if (cursorEntry->canFocus && cursorEntry != currentCursorEntry) { onCursorMoved(origCursor, mCursor); return true; } } mCursor += searchAxis; } // Now again on search axis- mCursor = curDirPos; while (mCursor.x >= 0 && mCursor.y >= 0 && mCursor.x < mGridSize.x && mCursor.y < mGridSize.y) { cursorEntry = getCellAt(mCursor); if (cursorEntry && cursorEntry->canFocus && cursorEntry != currentCursorEntry) { onCursorMoved(origCursor, mCursor); return true; } mCursor -= searchAxis; } mCursor = curDirPos; } // Failed to find another focusable element in this direction. mCursor = origCursor; return false; } void ComponentGrid::moveCursorTo(int xPos, int yPos, bool selectLeftCell) { const glm::ivec2 origCursor {mCursor}; if (xPos != -1) mCursor.x = xPos; if (yPos != -1) mCursor.y = yPos; const GridEntry* currentCursorEntry = getCellAt(mCursor); // If requested, select the leftmost cell of entries wider than 1 cell. if (selectLeftCell && mCursor.x > currentCursorEntry->pos.x) mCursor.x = currentCursorEntry->pos.x; onCursorMoved(origCursor, mCursor); } void ComponentGrid::onFocusLost() { const GridEntry* cursorEntry = getCellAt(mCursor); if (cursorEntry) cursorEntry->component->onFocusLost(); } void ComponentGrid::onFocusGained() { const GridEntry* cursorEntry = getCellAt(mCursor); if (cursorEntry) cursorEntry->component->onFocusGained(); } bool ComponentGrid::cursorValid() { const GridEntry* e = getCellAt(mCursor); return (e != nullptr && e->canFocus); } void ComponentGrid::update(int deltaTime) { // Update everything. const GridEntry* cursorEntry = getCellAt(mCursor); for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { if (it->updateType == UPDATE_ALWAYS || (it->updateType == UPDATE_WHEN_SELECTED && cursorEntry == &(*it))) { it->component->update(deltaTime); } } } void ComponentGrid::render(const glm::mat4& parentTrans) { glm::mat4 trans {parentTrans * getTransform()}; renderChildren(trans); // Draw cell separators. for (size_t i = 0; i < mSeparators.size(); ++i) { Renderer::setMatrix(trans); Renderer::drawRect(mSeparators[i][0], mSeparators[i][1], mSeparators[i][2], mSeparators[i][3], 0xC6C7C6FF, 0xC6C7C6FF); } } void ComponentGrid::textInput(const std::string& text) { const GridEntry* selectedEntry = getCellAt(mCursor); if (selectedEntry != nullptr && selectedEntry->canFocus) selectedEntry->component->textInput(text); } void ComponentGrid::onCursorMoved(glm::ivec2 from, glm::ivec2 to) { const GridEntry* cell = getCellAt(from); if (cell) cell->component->onFocusLost(); cell = getCellAt(to); if (cell) cell->component->onFocusGained(); updateHelpPrompts(); } void ComponentGrid::setCursorTo(const std::shared_ptr& comp) { for (auto it = mCells.cbegin(); it != mCells.cend(); ++it) { if (it->component == comp) { glm::ivec2 oldCursor {mCursor}; mCursor = it->pos; onCursorMoved(oldCursor, mCursor); return; } } // Component not found! assert(false); } std::vector ComponentGrid::getHelpPrompts() { std::vector prompts; const GridEntry* e = getCellAt(mCursor); if (e) prompts = e->component->getHelpPrompts(); bool canScrollVert = false; // If the currently selected cell does not fill the entire Y axis, then check if the cells // above or below are actually focusable as otherwise they should not affect the help prompts. if (mGridSize.y > 1 && e->dim.y < mGridSize.y) { if (e->pos.y - e->dim.y >= 0) { const GridEntry* cell = getCellAt(glm::ivec2 {e->pos.x, e->pos.y - e->dim.y}); if (cell != nullptr && cell->canFocus) canScrollVert = true; } if (e->pos.y + e->dim.y < mGridSize.y) { const GridEntry* cell = getCellAt(glm::ivec2 {e->pos.x, e->pos.y + e->dim.y}); if (cell != nullptr && cell->canFocus) canScrollVert = true; } } // There is currently no situation in the application where unfocusable cells are located // next to each other horizontally, so this code is good enough. If this changes in the // future, code similar to the the vertical cell handling above needs to be added. bool canScrollHoriz = (mGridSize.x > 1 && e->dim.x < mGridSize.x); // Check existing capabilities as indicated by the help prompts, and if the prompts should // be combined into "up/down/left/right" then also remove the single-axis prompts. if (!prompts.empty() && prompts.back() == HelpPrompt("up/down", "choose")) { canScrollVert = true; if (canScrollHoriz && canScrollVert) prompts.pop_back(); } else if (!prompts.empty() && prompts.back() == HelpPrompt("left/right", "choose")) { canScrollHoriz = true; if (canScrollHoriz && canScrollVert) prompts.pop_back(); } // Any duplicates will be removed in Window::setHelpPrompts() if (canScrollHoriz && canScrollVert) prompts.push_back(HelpPrompt("up/down/left/right", "choose")); else if (canScrollHoriz) prompts.push_back(HelpPrompt("left/right", "choose")); else if (canScrollVert) prompts.push_back(HelpPrompt("up/down", "choose")); return prompts; }