ES-DE/es-core/src/components/ComponentList.cpp

511 lines
17 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// ComponentList.cpp
//
// Used to lay out and navigate lists in GUI menus.
//
#include "components/ComponentList.h"
2017-11-01 22:21:10 +00:00
#include "resources/Font.h"
#define TOTAL_HORIZONTAL_PADDING_PX 20.0f
ComponentList::ComponentList()
: IList<ComponentListRow, void*> {LIST_SCROLL_STYLE_SLOW, ListLoopType::LIST_NEVER_LOOP}
, mRenderer {Renderer::getInstance()}
, mFocused {false}
, mSetupCompleted {false}
, mBottomCameraOffset {false}
, mSingleRowScroll {false}
, mSelectorBarOffset {0.0f}
, mCameraOffset {0.0f}
, mLoopRows {false}
, mLoopScroll {false}
, mLoopOffset {0}
, mLoopOffset2 {0}
, mLoopTime {0}
, mScrollIndicatorStatus {SCROLL_NONE}
{
// Adjust the padding relative to the aspect ratio and screen resolution to make it look
// coherent regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference.
float aspectValue {1.778f / Renderer::getScreenAspectRatio()};
mHorizontalPadding =
TOTAL_HORIZONTAL_PADDING_PX * aspectValue * Renderer::getScreenWidthModifier();
}
void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere)
{
IList<ComponentListRow, void*>::Entry e;
e.name = "";
e.object = nullptr;
e.data = row;
this->add(e);
for (auto it = mEntries.back().data.elements.cbegin();
it != mEntries.back().data.elements.cend(); ++it)
addChild(it->component.get());
updateElementSize(mEntries.back().data);
updateElementPosition(mEntries.back().data);
if (setCursorHere) {
mCursor = static_cast<int>(mEntries.size()) - 1;
onCursorChanged(CursorState::CURSOR_STOPPED);
}
}
void ComponentList::onSizeChanged()
{
for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it) {
updateElementSize(it->data);
updateElementPosition(it->data);
}
updateCameraOffset();
}
bool ComponentList::input(InputConfig* config, Input input)
{
if (size() == 0)
return false;
mSingleRowScroll = false;
if (input.value &&
(config->isMappedTo("a", input) || config->isMappedLike("lefttrigger", input) ||
config->isMappedLike("righttrigger", input))) {
stopScrolling();
}
// Give it to the current row's input handler.
if (mEntries.at(mCursor).data.input_handler) {
if (mEntries.at(mCursor).data.input_handler(config, input))
return true;
}
else {
// No input handler assigned, do the default, which is to give it
// to the rightmost element in the row.
auto& row = mEntries.at(mCursor).data;
if (row.elements.size()) {
if (row.elements.back().component->input(config, input))
return true;
}
}
// Input handler didn't consume the input - try to scroll.
if (config->isMappedLike("up", input)) {
mSingleRowScroll = true;
return listInput(input.value != 0 ? -1 : 0);
}
else if (config->isMappedLike("down", input)) {
mSingleRowScroll = true;
return listInput(input.value != 0 ? 1 : 0);
}
else if (config->isMappedLike("leftshoulder", input)) {
return listInput(input.value != 0 ? -6 : 0);
}
else if (config->isMappedLike("rightshoulder", input)) {
return listInput(input.value != 0 ? 6 : 0);
}
else if (config->isMappedLike("lefttrigger", input)) {
if (input.value != 0) {
mSelectorBarOffset = 0;
return listFirstRow();
}
}
else if (config->isMappedLike("righttrigger", input)) {
if (input.value != 0) {
2020-12-29 11:54:24 +00:00
mSelectorBarOffset = mEntries.size() - 1.0f;
return listLastRow();
}
}
return false;
}
void ComponentList::update(int deltaTime)
{
if (!mFocused && mLoopRows) {
mLoopOffset = 0;
mLoopOffset2 = 0;
mLoopTime = 0;
}
2021-10-10 16:15:37 +00:00
const float totalHeight = getTotalRowHeight();
// Scroll indicator logic, used by ScrollIndicatorComponent.
bool scrollIndicatorChanged = false;
if (totalHeight > mSize.y) {
if (mCameraOffset == 0) {
if (mScrollIndicatorStatus != SCROLL_DOWN) {
mScrollIndicatorStatus = SCROLL_DOWN;
scrollIndicatorChanged = true;
}
}
else if (mBottomCameraOffset) {
if (mScrollIndicatorStatus != SCROLL_UP) {
mScrollIndicatorStatus = SCROLL_UP;
scrollIndicatorChanged = true;
}
}
else if (mCameraOffset > 0) {
if (mScrollIndicatorStatus != SCROLL_UP_DOWN) {
mScrollIndicatorStatus = SCROLL_UP_DOWN;
scrollIndicatorChanged = true;
}
}
}
if (scrollIndicatorChanged == true && mScrollIndicatorChangedCallback != nullptr)
mScrollIndicatorChangedCallback(mScrollIndicatorStatus, mSingleRowScroll);
2021-10-10 16:15:37 +00:00
listUpdate(deltaTime);
if (size()) {
float rowWidth {0.0f};
// Update our currently selected row.
for (auto it = mEntries.at(mCursor).data.elements.cbegin();
it != mEntries.at(mCursor).data.elements.cend(); ++it) {
it->component->update(deltaTime);
rowWidth += it->component->getSize().x;
}
if (mLoopRows && rowWidth + mHorizontalPadding / 2.0f > mSize.x) {
// Loop the text.
const float speed {
Font::get(FONT_SIZE_MEDIUM)->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f};
const float delay {1500.0f};
const float scrollLength {rowWidth};
const float returnLength {speed * 1.5f};
const float scrollTime {(scrollLength * 1000.0f) / speed};
const float returnTime {(returnLength * 1000.0f) / speed};
const int maxTime {static_cast<int>(delay + scrollTime + returnTime)};
mLoopTime += deltaTime;
while (mLoopTime > maxTime)
mLoopTime -= maxTime;
mLoopOffset = static_cast<int>(Utils::Math::loop(delay, scrollTime + returnTime,
static_cast<float>(mLoopTime),
scrollLength + returnLength));
if (mLoopOffset > (scrollLength - (mSize.x - returnLength)))
mLoopOffset2 = static_cast<int>(mLoopOffset - (scrollLength + returnLength));
else if (mLoopOffset2 < 0)
mLoopOffset2 = 0;
}
}
}
void ComponentList::onCursorChanged(const CursorState& state)
{
2021-10-10 16:15:37 +00:00
mSetupCompleted = true;
if (mLoopRows) {
mLoopOffset = 0;
mLoopOffset2 = 0;
mLoopTime = 0;
}
// Update the selector bar position.
// In the future this might be animated.
mSelectorBarOffset = 0;
for (int i = 0; i < mCursor; ++i)
mSelectorBarOffset += getRowHeight(mEntries.at(i).data);
updateCameraOffset();
// This is terribly inefficient but we don't know what we came from so...
if (size()) {
for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it)
it->data.elements.back().component->onFocusLost();
mEntries.at(mCursor).data.elements.back().component->onFocusGained();
}
if (mCursorChangedCallback)
mCursorChangedCallback(state);
updateHelpPrompts();
}
void ComponentList::updateCameraOffset()
{
2021-10-10 16:15:37 +00:00
float oldCameraOffset = mCameraOffset;
// Move the camera to scroll.
const float totalHeight = getTotalRowHeight();
if (totalHeight > mSize.y) {
float target =
mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data) / 2.0f - (mSize.y / 2.0f);
// Clamp the camera to prevent a fraction of a row from being displayed.
mCameraOffset = 0.0f;
unsigned int i = 0;
while (mCameraOffset < target && i < mEntries.size()) {
mCameraOffset += getRowHeight(mEntries.at(i).data);
2021-10-10 16:15:37 +00:00
if (mCameraOffset > totalHeight - mSize.y) {
if (mSetupCompleted) {
if (mScrollIndicatorStatus == ComponentList::SCROLL_NONE &&
oldCameraOffset == 0.0f)
break;
if (mScrollIndicatorStatus != ComponentList::SCROLL_NONE &&
oldCameraOffset == 0.0f)
mBottomCameraOffset = true;
else if (mCameraOffset != oldCameraOffset)
mBottomCameraOffset = true;
}
break;
2021-10-10 16:15:37 +00:00
}
++i;
}
if (mCameraOffset < oldCameraOffset &&
(oldCameraOffset > mSelectorBarOffset ||
mScrollIndicatorStatus != ComponentList::SCROLL_NONE))
2021-10-10 16:15:37 +00:00
mBottomCameraOffset = false;
if (mCameraOffset < 0.0f)
mCameraOffset = 0.0f;
}
else {
mCameraOffset = 0.0f;
}
}
void ComponentList::render(const glm::mat4& parentTrans)
{
if (!size())
return;
glm::mat4 trans {parentTrans * getTransform()};
// Clip everything to be inside our bounds.
glm::vec3 dim {mSize.x, mSize.y, 0.0f};
dim.x = (trans[0].x * dim.x + trans[3].x) - trans[3].x;
dim.y = (trans[1].y * dim.y + trans[3].y) - trans[3].y;
const int clipRectPosX {static_cast<int>(std::round(trans[3].x))};
const int clipRectPosY {static_cast<int>(std::round(trans[3].y))};
const int clipRectSizeX {static_cast<int>(std::round(dim.x))};
const int clipRectSizeY {static_cast<int>(std::round(dim.y))};
mRenderer->pushClipRect(glm::ivec2 {clipRectPosX, clipRectPosY},
glm::ivec2 {clipRectSizeX, clipRectSizeY});
// Scroll the camera.
trans = glm::translate(trans, glm::vec3 {0.0f, -mCameraOffset, 0.0f});
glm::mat4 loopTrans {trans};
// Draw our entries.
std::vector<GuiComponent*> drawAfterCursor;
bool drawAll;
for (size_t i = 0; i < mEntries.size(); ++i) {
if (mLoopRows && mFocused && mLoopOffset > 0) {
loopTrans =
glm::translate(trans, glm::vec3 {static_cast<float>(-mLoopOffset), 0.0f, 0.0f});
}
auto& entry = mEntries.at(i);
drawAll = !mFocused || i != static_cast<unsigned int>(mCursor);
for (auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); ++it) {
if (drawAll || it->invert_when_selected) {
auto renderLoopFunc = [&]() {
// Needed to avoid flickering when returning to the start position.
if (mLoopOffset == 0 && mLoopOffset2 == 0)
mLoopScroll = false;
it->component->render(loopTrans);
// Render row again if text is moved far enough for it to repeat.
if (mLoopOffset2 < 0 || mLoopScroll) {
mLoopScroll = true;
loopTrans = glm::translate(
trans, glm::vec3 {static_cast<float>(-mLoopOffset2), 0.0f, 0.0f});
it->component->render(loopTrans);
}
};
// For the row where the cursor is at, we want to remove any hue from the
// font or image before inverting, as it would otherwise lead to an ugly
// inverted color (e.g. red inverting to a green hue).
if (mFocused && i == static_cast<size_t>(mCursor) &&
it->component->getValue() != "") {
// Check if we're dealing with text or an image component.
bool isTextComponent = true;
unsigned int origColor = it->component->getColor();
if (origColor == 0) {
origColor = it->component->getColorShift();
isTextComponent = false;
}
// Check if the color is neutral.
unsigned char byteRed = origColor >> 24 & 0xFF;
unsigned char byteGreen = origColor >> 16 & 0xFF;
unsigned char byteBlue = origColor >> 8 & 0xFF;
// If it's neutral, just proceed with normal rendering.
if (byteRed == byteGreen && byteGreen == byteBlue) {
renderLoopFunc();
}
else {
if (isTextComponent)
it->component->setColor(DEFAULT_INVERTED_TEXTCOLOR);
else
it->component->setColorShift(DEFAULT_INVERTED_IMAGECOLOR);
renderLoopFunc();
// Revert to the original color after rendering.
if (isTextComponent)
it->component->setColor(origColor);
else
it->component->setColorShift(origColor);
}
}
else {
it->component->render(trans);
}
}
else {
drawAfterCursor.push_back(it->component.get());
}
}
}
// Custom rendering.
mRenderer->setMatrix(trans);
// Draw selector bar.
if (mFocused) {
const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data);
2020-09-12 17:17:26 +00:00
if (mOpacity == 1.0f) {
mRenderer->drawRect(0.0f, mSelectorBarOffset, std::ceil(mSize.x), selectedRowHeight,
0xFFFFFFFF, 0xFFFFFFFF, false, mOpacity, mDimming,
Renderer::BlendFactor::ONE_MINUS_DST_COLOR,
Renderer::BlendFactor::ZERO);
mRenderer->drawRect(0.0f, mSelectorBarOffset, std::ceil(mSize.x), selectedRowHeight,
0x777777FF, 0x777777FF, false, mOpacity, mDimming,
Renderer::BlendFactor::ONE, Renderer::BlendFactor::ONE);
2020-09-12 17:17:26 +00:00
}
for (auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); ++it)
(*it)->render(trans);
// Reset matrix if one of these components changed it.
if (drawAfterCursor.size())
mRenderer->setMatrix(trans);
}
// Draw separators.
float y = 0;
for (unsigned int i = 0; i < mEntries.size(); ++i) {
mRenderer->drawRect(0.0f, y, std::ceil(mSize.x), 1.0f * Renderer::getScreenHeightModifier(),
0xC6C7C6FF, 0xC6C7C6FF, false, mOpacity, mDimming);
y += getRowHeight(mEntries.at(i).data);
}
mRenderer->drawRect(0.0f, y, std::ceil(mSize.x), 1.0f * Renderer::getScreenHeightModifier(),
0xC6C7C6FF, 0xC6C7C6FF, false, mOpacity, mDimming);
mRenderer->popClipRect();
}
float ComponentList::getRowHeight(const ComponentListRow& row) const
{
// Returns the highest component height found in the row.
float height = 0;
for (unsigned int i = 0; i < row.elements.size(); ++i) {
if (row.elements.at(i).component->getSize().y > height)
height = row.elements.at(i).component->getSize().y;
}
return std::floor(height);
}
float ComponentList::getTotalRowHeight() const
{
float height = 0;
for (auto it = mEntries.cbegin(); it != mEntries.cend(); ++it)
height += getRowHeight(it->data);
return height;
}
void ComponentList::updateElementPosition(const ComponentListRow& row)
{
float yOffset = 0;
for (auto it = mEntries.cbegin(); it != mEntries.cend() && &it->data != &row; ++it)
yOffset += getRowHeight(it->data);
// Assumes updateElementSize has already been called.
float rowHeight = getRowHeight(row);
float x = mHorizontalPadding / 2.0f;
for (unsigned int i = 0; i < row.elements.size(); ++i) {
const auto comp = row.elements.at(i).component;
// Center vertically.
comp->setPosition(x, (rowHeight - std::round(comp->getSize().y)) / 2.0f + yOffset);
x += comp->getSize().x;
}
}
void ComponentList::updateElementSize(const ComponentListRow& row)
{
float width = mSize.x - mHorizontalPadding;
2021-03-27 09:26:13 +00:00
std::vector<std::shared_ptr<GuiComponent>> resizeVec;
for (auto it = row.elements.cbegin(); it != row.elements.cend(); ++it) {
if (it->resize_width)
resizeVec.push_back(it->component);
else
width -= it->component->getSize().x;
}
// Redistribute the "unused" width equally among the components with resize_width set to true.
width = width / resizeVec.size();
for (auto it = resizeVec.cbegin(); it != resizeVec.cend(); ++it)
(*it)->setSize(width, (*it)->getSize().y);
}
2020-12-16 22:59:00 +00:00
void ComponentList::textInput(const std::string& text)
{
if (!size())
return;
mEntries.at(mCursor).data.elements.back().component->textInput(text);
}
std::vector<HelpPrompt> ComponentList::getHelpPrompts()
{
if (!size())
return std::vector<HelpPrompt>();
std::vector<HelpPrompt> prompts =
mEntries.at(mCursor).data.elements.back().component->getHelpPrompts();
if (size() > 1) {
bool addMovePrompt = true;
for (auto it = prompts.cbegin(); it != prompts.cend(); ++it) {
if (it->first == "up/down" || it->first == "up/down/left/right") {
addMovePrompt = false;
break;
}
}
if (addMovePrompt)
prompts.push_back(HelpPrompt("up/down", "choose"));
}
return prompts;
}
bool ComponentList::moveCursor(int amt)
{
bool ret = listInput(amt);
listInput(0);
return ret;
}