| 345 | } |
| 346 | |
| 347 | void FileBrowserActivity::render(RenderLock&&) { |
| 348 | renderer.clearScreen(); |
| 349 | |
| 350 | const auto pageWidth = renderer.getScreenWidth(); |
| 351 | const auto pageHeight = renderer.getScreenHeight(); |
| 352 | const auto& metrics = UITheme::getInstance().getMetrics(); |
| 353 | |
| 354 | std::string folderName = |
| 355 | (mode == Mode::PickFirmware) |
| 356 | ? std::string(tr(STR_SELECT_FIRMWARE_FILE)) |
| 357 | : ((basepath == "/") ? std::string(tr(STR_SD_CARD)) : basepath.substr(basepath.rfind('/') + 1)); |
| 358 | GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str()); |
| 359 | |
| 360 | const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); |
| 361 | const int pathReserved = pathLineHeight + metrics.verticalSpacing; |
| 362 | const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; |
| 363 | const int contentHeight = |
| 364 | pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved; |
| 365 | if (files.empty()) { |
| 366 | const char* emptyMsg = (mode == Mode::PickFirmware) ? tr(STR_NO_BIN_FILES) : tr(STR_NO_FILES_FOUND); |
| 367 | renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, emptyMsg); |
| 368 | } else { |
| 369 | GUI.drawList( |
| 370 | renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex, |
| 371 | [this](int index) { return getFileName(files[index]); }, nullptr, |
| 372 | [this](int index) { return UITheme::getFileIcon(files[index]); }, |
| 373 | [this](int index) { return getFileExtension(files[index]); }, false); |
| 374 | } |
| 375 | |
| 376 | // Full path display |
| 377 | { |
| 378 | const int pathY = pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - pathLineHeight; |
| 379 | const int separatorY = pathY - metrics.verticalSpacing / 2; |
| 380 | renderer.drawLine(0, separatorY, pageWidth - 1, separatorY, 3, true); |
| 381 | const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2; |
| 382 | // Left-truncate so the deepest directory is always visible |
| 383 | const char* pathStr = basepath.c_str(); |
| 384 | const char* pathDisplay = pathStr; |
| 385 | char leftTruncBuf[256]; |
| 386 | if (renderer.getTextWidth(SMALL_FONT_ID, pathStr) > pathMaxWidth) { |
| 387 | const char ellipsis[] = "\xe2\x80\xa6"; // UTF-8 ellipsis (…) |
| 388 | const int ellipsisWidth = renderer.getTextWidth(SMALL_FONT_ID, ellipsis); |
| 389 | const int available = pathMaxWidth - ellipsisWidth; |
| 390 | // Walk forward from the start until the suffix fits, skipping UTF-8 continuation bytes |
| 391 | const char* p = pathStr; |
| 392 | while (*p) { |
| 393 | if (renderer.getTextWidth(SMALL_FONT_ID, p) <= available) break; |
| 394 | ++p; |
| 395 | while (*p && (static_cast<unsigned char>(*p) & 0xC0) == 0x80) ++p; |
| 396 | } |
| 397 | snprintf(leftTruncBuf, sizeof(leftTruncBuf), "%s%s", ellipsis, p); |
| 398 | pathDisplay = leftTruncBuf; |
| 399 | } |
| 400 | renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, pathY, pathDisplay); |
| 401 | } |
| 402 | |
| 403 | // Help text |
| 404 | const char* backLabel = (basepath == "/") ? (mode == Mode::PickFirmware ? tr(STR_BACK) : tr(STR_HOME)) : tr(STR_BACK); |
nothing calls this directly
no test coverage detected