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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user