feat: Expandable selected row for long filenames in File Browser

When the selected row's filename overflows the available text width
(with extension), the row expands to 2 lines with smart text wrapping.
The file extension moves to the second row (right-aligned). Non-selected
rows retain single-line truncation.

Key behaviors:
- 3-tier text wrapping: preferred delimiters (" - ", " -- ", en/em-dash),
  word boundaries, then character-level fallback
- Row-height line spacing for natural visual rhythm
- Icons aligned with line 1 (LyraTheme)
- Pagination uses effectivePageItems with anti-leak clamping to prevent
  page boundary shifts while ensuring all items remain accessible
- Boundary item duplication: items bumped from a page due to expansion
  appear at the top of the next page, guarded against cascading

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-20 19:42:56 -05:00
parent 406c3aeace
commit 51dc498768
2 changed files with 426 additions and 75 deletions

View File

@@ -45,6 +45,105 @@ void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, i
renderer.fillRect(x + 2, y + 2, filledWidth, rectHeight - 4);
}
// Truncate a string with "..." to fit within maxWidth.
std::string truncateWithEllipsis(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth) {
std::string truncated = text;
std::string withEllipsis = truncated + "...";
while (!truncated.empty() && renderer.getTextWidth(fontId, withEllipsis.c_str()) > maxWidth) {
utf8RemoveLastChar(truncated);
withEllipsis = truncated + "...";
}
return truncated.empty() ? std::string("...") : withEllipsis;
}
// Text wrapping with 3-tier break logic:
// 1) Preferred delimiters: " -- ", " - ", en-dash, em-dash (title-author separator)
// 2) Word boundaries: last space or hyphen that fits
// 3) Character-level fallback for long unbroken tokens
// The last allowed line is truncated with "..." if it overflows.
std::vector<std::string> wrapTextToLines(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth,
int maxLines) {
std::vector<std::string> lines;
if (text.empty() || maxWidth <= 0 || maxLines <= 0) return lines;
if (renderer.getTextWidth(fontId, text.c_str()) <= maxWidth) {
lines.push_back(text);
return lines;
}
if (maxLines == 1) {
lines.push_back(truncateWithEllipsis(renderer, fontId, text, maxWidth));
return lines;
}
// Tier 1: Try preferred delimiters (last occurrence to maximize line 1 content).
// \xe2\x80\x93 = en-dash, \xe2\x80\x94 = em-dash
static const char* const preferredDelimiters[] = {" -- ", " - ", " \xe2\x80\x93 ", " \xe2\x80\x94 "};
for (const char* delim : preferredDelimiters) {
size_t delimLen = strlen(delim);
auto pos = text.rfind(delim);
if (pos != std::string::npos && pos > 0) {
std::string firstPart = text.substr(0, pos);
if (renderer.getTextWidth(fontId, firstPart.c_str()) <= maxWidth) {
lines.push_back(firstPart);
std::string remainder = text.substr(pos + delimLen);
if (renderer.getTextWidth(fontId, remainder.c_str()) > maxWidth) {
lines.push_back(truncateWithEllipsis(renderer, fontId, remainder, maxWidth));
} else {
lines.push_back(remainder);
}
return lines;
}
}
}
// Tier 2 & 3: Word-boundary wrapping with character-level fallback.
std::string currentLine;
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
std::string lineAtBreak;
const unsigned char* ptrAtBreak = nullptr;
while (*ptr != 0) {
const unsigned char* charStart = ptr;
uint32_t cp = utf8NextCodepoint(&ptr);
std::string nextChar(reinterpret_cast<const char*>(charStart), static_cast<size_t>(ptr - charStart));
std::string candidate = currentLine + nextChar;
if (renderer.getTextWidth(fontId, candidate.c_str()) <= maxWidth) {
currentLine = candidate;
if (cp == ' ' || cp == '-') {
lineAtBreak = currentLine;
ptrAtBreak = ptr;
}
continue;
}
// Overflow
if (static_cast<int>(lines.size()) < maxLines - 1 && !currentLine.empty()) {
if (ptrAtBreak != nullptr) {
std::string line = lineAtBreak;
while (!line.empty() && line.back() == ' ') line.pop_back();
lines.push_back(line);
ptr = ptrAtBreak;
while (*ptr == ' ') ++ptr;
currentLine.clear();
} else {
lines.push_back(currentLine);
currentLine = nextChar;
}
lineAtBreak.clear();
ptrAtBreak = nullptr;
} else {
lines.push_back(truncateWithEllipsis(renderer, fontId, currentLine, maxWidth));
return lines;
}
}
if (!currentLine.empty()) {
lines.push_back(currentLine);
}
return lines;
}
} // namespace
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
@@ -193,25 +292,36 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
int rowHeight =
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
int pageItems = rect.height / rowHeight;
int contentWidth = rect.width - 5;
auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID;
const int totalPages = (itemCount + pageItems - 1) / pageItems;
// Detect if selected row's title overflows and needs 2-line expansion
bool selectedExpands = false;
if (selectedIndex >= 0 && rowSubtitle == nullptr && rowValue != nullptr) {
int titleTextWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - 60;
auto selTitle = rowTitle(selectedIndex);
if (renderer.getTextWidth(font, selTitle.c_str()) > titleTextWidth) {
selectedExpands = true;
}
}
const int effectivePageItems = selectedExpands ? std::max(1, pageItems - 1) : pageItems;
const int totalPages = (itemCount + effectivePageItems - 1) / effectivePageItems;
if (totalPages > 1) {
constexpr int indicatorWidth = 20;
constexpr int arrowSize = 6;
constexpr int margin = 15; // Offset from right edge
constexpr int margin = 15;
const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin;
const int indicatorTop = rect.y; // Offset to avoid overlapping side button hints
const int indicatorTop = rect.y;
const int indicatorBottom = rect.y + rect.height - arrowSize;
// Draw up arrow at top (^) - narrow point at top, wide base at bottom
for (int i = 0; i < arrowSize; ++i) {
const int lineWidth = 1 + i * 2;
const int startX = centerX - i;
renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i);
}
// Draw down arrow at bottom (v) - wide base at top, narrow point at bottom
for (int i = 0; i < arrowSize; ++i) {
const int lineWidth = 1 + (arrowSize - 1 - i) * 2;
const int startX = centerX - (arrowSize - 1 - i);
@@ -220,37 +330,89 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
}
}
// Draw selection
int contentWidth = rect.width - 5;
if (selectedIndex >= 0) {
renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight);
}
// Draw all items
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
const int itemY = rect.y + (i % pageItems) * rowHeight;
int textWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - (rowValue != nullptr ? 60 : 0);
// Draw name
auto itemName = rowTitle(i);
auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID;
auto item = renderer.truncatedText(font, itemName.c_str(), textWidth);
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex);
if (rowSubtitle != nullptr) {
// Draw subtitle
std::string subtitleText = rowSubtitle(i);
auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth);
renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 30, subtitle.c_str(),
i != selectedIndex);
// Compute page start: use effective page items but prevent backward leak
int pageStartIndex;
if (selectedExpands) {
int rawStart = selectedIndex / effectivePageItems * effectivePageItems;
int originalStart = selectedIndex / pageItems * pageItems;
pageStartIndex = std::max(rawStart, originalStart);
if (selectedIndex >= pageStartIndex + effectivePageItems) {
pageStartIndex = selectedIndex - effectivePageItems + 1;
}
if (pageStartIndex > 0 && pageStartIndex == originalStart
&& selectedIndex < pageStartIndex + effectivePageItems - 1) {
int checkWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - 60;
auto prevTitle = rowTitle(pageStartIndex - 1);
if (renderer.getTextWidth(font, prevTitle.c_str()) > checkWidth) {
pageStartIndex--;
}
}
} else {
pageStartIndex = selectedIndex / pageItems * pageItems;
// Include previous page's boundary item if it would need expansion when selected,
// so it doesn't vanish when navigating from it to the current page.
if (pageStartIndex > 0 && selectedIndex < pageStartIndex + pageItems - 1) {
int checkWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - 60;
auto prevTitle = rowTitle(pageStartIndex - 1);
if (renderer.getTextWidth(font, prevTitle.c_str()) > checkWidth) {
pageStartIndex--;
}
}
}
if (rowValue != nullptr) {
// Draw value
std::string valueText = rowValue(i);
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth,
itemY, valueText.c_str(), i != selectedIndex);
// Draw selection highlight
if (selectedIndex >= 0) {
int selRowsBeforeOnPage = selectedIndex - pageStartIndex;
int selY = rect.y + selRowsBeforeOnPage * rowHeight - 2;
int selHeight = selectedExpands ? 2 * rowHeight : rowHeight;
renderer.fillRect(0, selY, rect.width, selHeight);
}
// Draw all items
int yPos = rect.y;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + effectivePageItems; i++) {
const bool isExpanded = (selectedExpands && i == selectedIndex);
auto itemName = rowTitle(i);
if (isExpanded) {
int wrapWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2;
auto lines = wrapTextToLines(renderer, font, itemName, wrapWidth, 2);
for (size_t l = 0; l < lines.size(); ++l) {
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding,
yPos + static_cast<int>(l) * rowHeight, lines[l].c_str(), false);
}
if (rowValue != nullptr) {
std::string valueText = rowValue(i);
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID,
rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth,
yPos + rowHeight, valueText.c_str(), false);
}
yPos += 2 * rowHeight;
} else {
int textWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - (rowValue != nullptr ? 60 : 0);
auto item = renderer.truncatedText(font, itemName.c_str(), textWidth);
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, yPos, item.c_str(),
i != selectedIndex);
if (rowSubtitle != nullptr) {
std::string subtitleText = rowSubtitle(i);
auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth);
renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, yPos + 30,
subtitle.c_str(), i != selectedIndex);
}
if (rowValue != nullptr) {
std::string valueText = rowValue(i);
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID,
rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth, yPos,
valueText.c_str(), i != selectedIndex);
}
yPos += rowHeight;
}
}
}

View File

@@ -84,6 +84,95 @@ const uint8_t* iconForName(UIIcon icon, int size) {
}
return nullptr;
}
std::string truncateWithEllipsis(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth) {
std::string truncated = text;
std::string withEllipsis = truncated + "...";
while (!truncated.empty() && renderer.getTextWidth(fontId, withEllipsis.c_str()) > maxWidth) {
utf8RemoveLastChar(truncated);
withEllipsis = truncated + "...";
}
return truncated.empty() ? std::string("...") : withEllipsis;
}
std::vector<std::string> wrapTextToLines(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth,
int maxLines) {
std::vector<std::string> lines;
if (text.empty() || maxWidth <= 0 || maxLines <= 0) return lines;
if (renderer.getTextWidth(fontId, text.c_str()) <= maxWidth) {
lines.push_back(text);
return lines;
}
if (maxLines == 1) {
lines.push_back(truncateWithEllipsis(renderer, fontId, text, maxWidth));
return lines;
}
static const char* const preferredDelimiters[] = {" -- ", " - ", " \xe2\x80\x93 ", " \xe2\x80\x94 "};
for (const char* delim : preferredDelimiters) {
size_t delimLen = strlen(delim);
auto pos = text.rfind(delim);
if (pos != std::string::npos && pos > 0) {
std::string firstPart = text.substr(0, pos);
if (renderer.getTextWidth(fontId, firstPart.c_str()) <= maxWidth) {
lines.push_back(firstPart);
std::string remainder = text.substr(pos + delimLen);
if (renderer.getTextWidth(fontId, remainder.c_str()) > maxWidth) {
lines.push_back(truncateWithEllipsis(renderer, fontId, remainder, maxWidth));
} else {
lines.push_back(remainder);
}
return lines;
}
}
}
std::string currentLine;
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
std::string lineAtBreak;
const unsigned char* ptrAtBreak = nullptr;
while (*ptr != 0) {
const unsigned char* charStart = ptr;
uint32_t cp = utf8NextCodepoint(&ptr);
std::string nextChar(reinterpret_cast<const char*>(charStart), static_cast<size_t>(ptr - charStart));
std::string candidate = currentLine + nextChar;
if (renderer.getTextWidth(fontId, candidate.c_str()) <= maxWidth) {
currentLine = candidate;
if (cp == ' ' || cp == '-') {
lineAtBreak = currentLine;
ptrAtBreak = ptr;
}
continue;
}
if (static_cast<int>(lines.size()) < maxLines - 1 && !currentLine.empty()) {
if (ptrAtBreak != nullptr) {
std::string line = lineAtBreak;
while (!line.empty() && line.back() == ' ') line.pop_back();
lines.push_back(line);
ptr = ptrAtBreak;
while (*ptr == ' ') ++ptr;
currentLine.clear();
} else {
lines.push_back(currentLine);
currentLine = nextChar;
}
lineAtBreak.clear();
ptrAtBreak = nullptr;
} else {
lines.push_back(truncateWithEllipsis(renderer, fontId, currentLine, maxWidth));
return lines;
}
}
if (!currentLine.empty()) {
lines.push_back(currentLine);
}
return lines;
}
} // namespace
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
@@ -278,13 +367,35 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
int pageItems = rect.height / rowHeight;
const int totalPages = (itemCount + pageItems - 1) / pageItems;
// Detect if selected row's title overflows and needs 2-line expansion
bool selectedExpands = false;
if (selectedIndex >= 0 && rowSubtitle == nullptr && rowValue != nullptr) {
int prelTotalPages = (itemCount + pageItems - 1) / pageItems;
int prelContentWidth =
rect.width -
(prelTotalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
int prelTextWidth = prelContentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
if (rowIcon != nullptr) prelTextWidth -= listIconSize + hPaddingInSelection;
auto selTitle = rowTitle(selectedIndex);
auto selValue = rowValue(selectedIndex);
int selValueWidth = 0;
if (!selValue.empty()) {
selValue = renderer.truncatedText(UI_10_FONT_ID, selValue.c_str(), maxListValueWidth);
selValueWidth = renderer.getTextWidth(UI_10_FONT_ID, selValue.c_str()) + hPaddingInSelection;
}
if (renderer.getTextWidth(UI_10_FONT_ID, selTitle.c_str()) > prelTextWidth - selValueWidth) {
selectedExpands = true;
}
}
const int effectivePageItems = selectedExpands ? std::max(1, pageItems - 1) : pageItems;
const int totalPages = (itemCount + effectivePageItems - 1) / effectivePageItems;
if (totalPages > 1) {
const int scrollAreaHeight = rect.height;
// Draw scroll bar
const int scrollBarHeight = (scrollAreaHeight * pageItems) / itemCount;
const int currentPage = selectedIndex / pageItems;
const int scrollBarHeight = (scrollAreaHeight * effectivePageItems) / itemCount;
const int currentPage = selectedIndex / effectivePageItems;
const int scrollBarY = rect.y + ((scrollAreaHeight - scrollBarHeight) * currentPage) / (totalPages - 1);
const int scrollBarX = rect.x + rect.width - LyraMetrics::values.scrollBarRightOffset;
renderer.drawLine(scrollBarX, rect.y, scrollBarX, rect.y + scrollAreaHeight, true);
@@ -292,19 +403,71 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
scrollBarHeight, true);
}
// Draw selection
int contentWidth =
rect.width -
(totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
// Compute page start: use effective page items but prevent backward leak
int pageStartIndex;
if (selectedExpands) {
int rawStart = selectedIndex / effectivePageItems * effectivePageItems;
int originalStart = selectedIndex / pageItems * pageItems;
pageStartIndex = std::max(rawStart, originalStart);
if (selectedIndex >= pageStartIndex + effectivePageItems) {
pageStartIndex = selectedIndex - effectivePageItems + 1;
}
if (pageStartIndex > 0 && pageStartIndex == originalStart
&& selectedIndex < pageStartIndex + effectivePageItems - 1) {
int checkTextWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
if (rowIcon != nullptr) checkTextWidth -= listIconSize + hPaddingInSelection;
auto prevTitle = rowTitle(pageStartIndex - 1);
int prevValueWidth = 0;
if (rowValue != nullptr) {
auto prevValue = rowValue(pageStartIndex - 1);
prevValue = renderer.truncatedText(UI_10_FONT_ID, prevValue.c_str(), maxListValueWidth);
if (!prevValue.empty()) {
prevValueWidth = renderer.getTextWidth(UI_10_FONT_ID, prevValue.c_str()) + hPaddingInSelection;
}
}
if (renderer.getTextWidth(UI_10_FONT_ID, prevTitle.c_str()) > checkTextWidth - prevValueWidth) {
pageStartIndex--;
}
}
} else {
pageStartIndex = selectedIndex / pageItems * pageItems;
// Include previous page's boundary item if it would need expansion when selected,
// so it doesn't vanish when navigating from it to the current page.
if (pageStartIndex > 0 && selectedIndex < pageStartIndex + pageItems - 1) {
int checkTextWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
if (rowIcon != nullptr) checkTextWidth -= listIconSize + hPaddingInSelection;
auto prevTitle = rowTitle(pageStartIndex - 1);
int prevValueWidth = 0;
if (rowValue != nullptr) {
auto prevValue = rowValue(pageStartIndex - 1);
prevValue = renderer.truncatedText(UI_10_FONT_ID, prevValue.c_str(), maxListValueWidth);
if (!prevValue.empty()) {
prevValueWidth = renderer.getTextWidth(UI_10_FONT_ID, prevValue.c_str()) + hPaddingInSelection;
}
}
if (renderer.getTextWidth(UI_10_FONT_ID, prevTitle.c_str()) > checkTextWidth - prevValueWidth) {
pageStartIndex--;
}
}
}
// Draw selection highlight
if (selectedIndex >= 0) {
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius,
int selRowsBeforeOnPage = selectedIndex - pageStartIndex;
int selY = rect.y + selRowsBeforeOnPage * rowHeight;
int selHeight = selectedExpands ? 2 * rowHeight : rowHeight;
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, selY,
contentWidth - LyraMetrics::values.contentSidePadding * 2, selHeight, cornerRadius,
Color::LightGray);
}
int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection;
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
int iconSize;
int iconSize = listIconSize;
if (rowIcon != nullptr) {
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
textX += iconSize + hPaddingInSelection;
@@ -312,52 +475,78 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
}
// Draw all items
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
const int itemY = rect.y + (i % pageItems) * rowHeight;
int rowTextWidth = textWidth;
int yPos = rect.y;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + effectivePageItems; i++) {
const bool isExpanded = (selectedExpands && i == selectedIndex);
// Draw name
int valueWidth = 0;
std::string valueText = "";
std::string valueText;
if (rowValue != nullptr) {
valueText = rowValue(i);
valueText = renderer.truncatedText(UI_10_FONT_ID, valueText.c_str(), maxListValueWidth);
valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection;
rowTextWidth -= valueWidth;
if (!valueText.empty()) {
valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection;
}
}
auto itemName = rowTitle(i);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
renderer.drawText(UI_10_FONT_ID, textX, itemY + 7, item.c_str(), true);
if (rowIcon != nullptr) {
UIIcon icon = rowIcon(i);
const uint8_t* iconBitmap = iconForName(icon, iconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
itemY + iconY, iconSize, iconSize);
}
}
if (isExpanded) {
int wrapWidth = textWidth;
auto lines = wrapTextToLines(renderer, UI_10_FONT_ID, itemName, wrapWidth, 2);
if (rowSubtitle != nullptr) {
// Draw subtitle
std::string subtitleText = rowSubtitle(i);
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
renderer.drawText(SMALL_FONT_ID, textX, itemY + 30, subtitle.c_str(), true);
}
// Draw value
if (!valueText.empty()) {
if (i == selectedIndex && highlightValue) {
renderer.fillRoundedRect(
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY,
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
for (size_t l = 0; l < lines.size(); ++l) {
renderer.drawText(UI_10_FONT_ID, textX, yPos + 7 + static_cast<int>(l) * rowHeight, lines[l].c_str(), true);
}
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
itemY + 6, valueText.c_str(), !(i == selectedIndex && highlightValue));
if (rowIcon != nullptr) {
UIIcon icon = rowIcon(i);
const uint8_t* iconBitmap = iconForName(icon, iconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
yPos + iconY, iconSize, iconSize);
}
}
if (!valueText.empty()) {
renderer.drawText(UI_10_FONT_ID,
rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
yPos + rowHeight + 7, valueText.c_str(), true);
}
yPos += 2 * rowHeight;
} else {
int rowTextWidth = textWidth - valueWidth;
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
renderer.drawText(UI_10_FONT_ID, textX, yPos + 7, item.c_str(), true);
if (rowIcon != nullptr) {
UIIcon icon = rowIcon(i);
const uint8_t* iconBitmap = iconForName(icon, iconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
yPos + iconY, iconSize, iconSize);
}
}
if (rowSubtitle != nullptr) {
std::string subtitleText = rowSubtitle(i);
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
renderer.drawText(SMALL_FONT_ID, textX, yPos + 30, subtitle.c_str(), true);
}
if (!valueText.empty()) {
if (i == selectedIndex && highlightValue) {
renderer.fillRoundedRect(
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, yPos,
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
}
renderer.drawText(UI_10_FONT_ID,
rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, yPos + 6,
valueText.c_str(), !(i == selectedIndex && highlightValue));
}
yPos += rowHeight;
}
}
}