Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,63 @@ Reference

---

#### search
The search popup is a full-screen overlay launched from the game list. All metadata elements are positioned off-screen and invisible by default — define them in your theme to enable them.

* `image name="background"` - ALL
- Optional background image rendered behind all other elements. Not present by default; omit to keep the built-in translucent dark panel.
* `text name="searchtext"` - ALL ^ TEXT
- The search query input bar at the top of the popup (position/size/font/color only — text content is managed by the engine).
* `text name="listmessage"` - ALL ^ TEXT
- Status/placeholder message shown over the result list (e.g. "TYPE TO SEARCH…", "NO RESULTS FOUND").
* `textlist name="gamelist"` - ALL
- The result list on the left. `primaryColor` for game entries, `secondaryColor` for other entries.

* Metadata
* Labels
* `text name="md_lbl_rating"` - ALL
* `text name="md_lbl_releasedate"` - ALL
* `text name="md_lbl_developer"` - ALL
* `text name="md_lbl_publisher"` - ALL
* `text name="md_lbl_genre"` - ALL
* `text name="md_lbl_players"` - ALL
* `text name="md_lbl_lastplayed"` - ALL
* `text name="md_lbl_playcount"` - ALL

* Values
* All values will follow to the right of their labels if a position isn't specified.

* `image name="md_image"` - POSITION | SIZE | Z_INDEX | ROTATION | VISIBLE
- Path is the "image" metadata for the currently selected game.
* `image name="md_thumbnail"` - POSITION | SIZE | Z_INDEX | ROTATION | VISIBLE
- Path is the "thumbnail" metadata for the currently selected game.
* `image name="md_marquee"` - POSITION | SIZE | Z_INDEX | ROTATION | VISIBLE
- Path is the "marquee" metadata. Hidden by default; set `visible` to show.
* `video name="md_video"` - POSITION | SIZE | DELAY | Z_INDEX | ROTATION | VISIBLE
- Path is the "video" metadata. **Hidden by default** — you must explicitly set `<visible>true</visible>` (or define a position/size) to enable video playback. To disable video entirely, omit this element or set `<visible>false</visible>`.
* `rating name="md_rating"` - ALL
- The "rating" metadata.
* `datetime name="md_releasedate"` - ALL
- The "releasedate" metadata.
* `text name="md_developer"` - ALL
- The "developer" metadata.
* `text name="md_publisher"` - ALL
- The "publisher" metadata.
* `text name="md_genre"` - ALL
- The "genre" metadata.
* `text name="md_players"` - ALL
- The "players" metadata.
* `datetime name="md_lastplayed"` - ALL
- The "lastplayed" metadata. Displayed relative to now (e.g. "3 hours ago").
* `text name="md_playcount"` - ALL
- The "playcount" metadata.
* `text name="md_description"` - POSITION | SIZE | FONT_PATH | FONT_SIZE | COLOR | Z_INDEX
- Text is the "desc" metadata.
* `text name="md_name"` - ALL
- The "name" metadata (game title). Positioned off-screen by default.

---

#### grid
* `helpsystem name="help"` - ALL
- The help system style for this view.
Expand Down
4 changes: 4 additions & 0 deletions es-app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/CharacterRowComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h

# Guis
Expand Down Expand Up @@ -50,6 +51,7 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSearchPopup.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h
Expand Down Expand Up @@ -77,6 +79,7 @@ set(ES_SOURCES
# GuiComponents
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/CharacterRowComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.cpp

# Guis
Expand Down Expand Up @@ -109,6 +112,7 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSearchPopup.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp
Expand Down
1 change: 1 addition & 0 deletions es-app/src/CollectionSystemManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ CollectionSystemManager::~CollectionSystemManager()
}
delete it->second.system;
}

sInstance = NULL;
}

Expand Down
269 changes: 269 additions & 0 deletions es-app/src/components/CharacterRowComponent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#include "components/CharacterRowComponent.h"

#include "renderers/Renderer.h"
#include "InputConfig.h"

const std::string CharacterRowComponent::MODE_SWITCH_123 = "123";
const std::string CharacterRowComponent::MODE_SWITCH_ABC = "ABC";
const std::string CharacterRowComponent::MODE_SWITCH_SYMBOLS = "!@#";
const std::string CharacterRowComponent::CHAR_SPACE = "SPC";
const std::string CharacterRowComponent::CHAR_BACKSPACE = "\xe2\x8c\xab"; // ⌫ U+232B
const std::string CharacterRowComponent::CHAR_CURSOR_LEFT = "\xe2\x86\x90"; // ← U+2190
const std::string CharacterRowComponent::CHAR_CURSOR_RIGHT = "\xe2\x86\x92"; // → U+2192

CharacterRowComponent::CharacterRowComponent(Window* window)
: GuiComponent(window), mMode(LETTERS), mCursor(2), mFocused(true),
mSelectorColor(0x000050FF), mTextColor(0xFFFFFFFF), mTotalWidth(0),
mScrollDir(0), mScrollTimer(0), mBackspaceHeld(false), mBackspaceTimer(0)
{
mFont = Font::get(FONT_SIZE_MEDIUM);
buildCharList();
}

void CharacterRowComponent::buildCharList()
{
mChars.clear();

switch (mMode)
{
case LETTERS:
mChars.push_back(MODE_SWITCH_123);
mChars.push_back(CHAR_SPACE);
for (char c = 'A'; c <= 'Z'; c++)
mChars.push_back(std::string(1, c));
break;

case NUMBERS:
mChars.push_back(MODE_SWITCH_SYMBOLS);
mChars.push_back(CHAR_SPACE);
for (char c = '1'; c <= '9'; c++)
mChars.push_back(std::string(1, c));
mChars.push_back("0");
break;

case SYMBOLS:
mChars.push_back(MODE_SWITCH_ABC);
mChars.push_back(CHAR_SPACE);
{
const char* syms = "`'\";:~=*+-_,.?!@#$%^&|/\\()[]{}<>";
for (int i = 0; syms[i]; i++)
mChars.push_back(std::string(1, syms[i]));
}
break;
}

mChars.push_back(CHAR_BACKSPACE);
mChars.push_back(CHAR_CURSOR_LEFT);
mChars.push_back(CHAR_CURSOR_RIGHT);

if (mCursor >= (int)mChars.size())
mCursor = (int)mChars.size() - 1;

// Cache per-char widths — rebuilt when chars, font, or size changes
// Compute natural padding, then shrink it if all chars don't fit in mSize.x()
float padding = Math::round(mSize.y() * 0.2f);

// First pass: measure raw text widths
std::vector<float> textWidths;
float totalTextWidth = 0;
for (const auto& ch : mChars)
{
float tw = mFont->sizeText(ch).x();
textWidths.push_back(tw);
totalTextWidth += tw;
}

// Shrink padding if needed so everything fits
if (mSize.x() > 0)
{
float maxPadding = (mSize.x() - totalTextWidth) / (2.0f * (float)mChars.size());
if (maxPadding < padding)
padding = std::max(1.0f, maxPadding);
}

mCharWidths.clear();
mTotalWidth = 0;
for (float tw : textWidths)
{
float w = tw + padding * 2;
mCharWidths.push_back(w);
mTotalWidth += w;
}
}

void CharacterRowComponent::scrollStep(int dir)
{
if (dir < 0)
{
if (mCursor > 0) mCursor--;
else mCursor = (int)mChars.size() - 1;
}
else
{
if (mCursor < (int)mChars.size() - 1) mCursor++;
else mCursor = 0;
}
}

void CharacterRowComponent::update(int deltaTime)
{
if (mScrollDir != 0)
{
mScrollTimer += deltaTime;
while (mScrollTimer >= SCROLL_DELAY_MS)
{
mScrollTimer -= SCROLL_REPEAT_MS;
scrollStep(mScrollDir);
}
}
if (mBackspaceHeld)
{
mBackspaceTimer += deltaTime;
while (mBackspaceTimer >= SCROLL_DELAY_MS)
{
mBackspaceTimer -= SCROLL_REPEAT_MS;
if (mBackspaceCb) mBackspaceCb();
}
}
GuiComponent::update(deltaTime);
}

bool CharacterRowComponent::input(InputConfig* config, Input input)
{
if (input.value != 0)
{
if (config->isMappedLike("left", input))
{
mScrollDir = -1;
mScrollTimer = 0;
scrollStep(-1);
return true;
}
else if (config->isMappedLike("right", input))
{
mScrollDir = 1;
mScrollTimer = 0;
scrollStep(1);
return true;
}
else if (config->isMappedTo("x", input))
{
mBackspaceHeld = true;
mBackspaceTimer = 0;
if (mBackspaceCb) mBackspaceCb();
return true;
}
else if (config->isMappedTo("a", input))
{
const std::string& selected = mChars[mCursor];

if (selected == MODE_SWITCH_123)
{
mMode = NUMBERS;
buildCharList();
}
else if (selected == MODE_SWITCH_SYMBOLS)
{
mMode = SYMBOLS;
buildCharList();
}
else if (selected == MODE_SWITCH_ABC)
{
mMode = LETTERS;
buildCharList();
}
else if (selected == CHAR_BACKSPACE)
{
if (mBackspaceCb)
mBackspaceCb();
}
else if (selected == CHAR_CURSOR_LEFT)
{
if (mCursorLeftCb)
mCursorLeftCb();
}
else if (selected == CHAR_CURSOR_RIGHT)
{
if (mCursorRightCb)
mCursorRightCb();
}
else if (selected == CHAR_SPACE)
{
if (mCharSelectedCb)
mCharSelectedCb(" ");
}
else
{
if (mCharSelectedCb)
mCharSelectedCb(selected);
}
return true;
}
}

if (input.value == 0)
{
if (mBackspaceHeld && config->isMappedTo("x", input))
{
mBackspaceHeld = false;
return true;
}
if ((mScrollDir < 0 && config->isMappedLike("left", input)) ||
(mScrollDir > 0 && config->isMappedLike("right", input)))
{
mScrollDir = 0;
return true;
}
}

return GuiComponent::input(config, input);
}

void CharacterRowComponent::onSizeChanged()
{
buildCharList();
}

void CharacterRowComponent::render(const Transform4x4f& parentTrans)
{
Transform4x4f trans = parentTrans * getTransform();

if (mChars.empty())
return;

const float height = mSize.y();

// Rebuild cache if invalidated (shouldn't normally happen at render time)
if (mCharWidths.size() != mChars.size())
buildCharList();

float startX = Math::round((mSize.x() - mTotalWidth) / 2.0f);
if (startX < 0)
startX = 0;

float x = startX;
const float textY = Math::round((height - mFont->getHeight(1.0f)) / 2.0f);

Renderer::setMatrix(trans);

for (int i = 0; i < (int)mChars.size(); i++)
{
float cellWidth = mCharWidths[i];
float cellPadding = Math::round(cellWidth - mFont->sizeText(mChars[i]).x()) / 2.0f;

if (mFocused && i == mCursor)
{
Renderer::drawRect(Math::round(x), 0, Math::round(cellWidth), Math::round(height),
mSelectorColor, mSelectorColor);
}

auto textCache = std::unique_ptr<TextCache>(
mFont->buildTextCache(mChars[i], Math::round(x + cellPadding), Math::round(textY),
(i == mCursor) ? 0xFFFFFFFF : mTextColor));
mFont->renderTextCache(textCache.get());

x += cellWidth;
}

renderChildren(trans);
}
Loading