diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 149bb28a..b30804e7 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -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 wrapTextToLines(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth, + int maxLines) { + std::vector 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(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(charStart), static_cast(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(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(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; } } } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index b5b06e8c..806c5bf0 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -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 wrapTextToLines(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth, + int maxLines) { + std::vector 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(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(charStart), static_cast(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(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(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; } } }