mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-27 00:25:38 +00:00
525 lines
16 KiB
C++
525 lines
16 KiB
C++
// 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<float>(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<float>(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<GuiComponent>& 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<GuiComponent>& 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<float> 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<float> 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<float> 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<float> 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<GuiComponent>& 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<HelpPrompt> ComponentGrid::getHelpPrompts()
|
|
{
|
|
std::vector<HelpPrompt> 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;
|
|
}
|