diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 7e54bc8a..a105a80f 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +26,6 @@ #include "fontIds.h" namespace { -// Mirror of HomeActivity::convertSidecarToBmp — converts a JPG/PNG sidecar cover to a -// 1-bit BMP at the requested WxH size and caches it under /.crosspoint/sidecar_/. std::string convertSidecarToBmp(const std::string& bookPath, const std::string& sidecarPath, int width, int height, const std::string& fileName) { const std::string cacheDir = "/.crosspoint/sidecar_" + std::to_string(std::hash{}(bookPath)); @@ -57,14 +56,12 @@ std::string convertSidecarToBmp(const std::string& bookPath, const std::string& return bmpPath; } -// Compute the grid thumbnail width from the available content width. int gridThumbWidth(int contentWidth) { const int margin = RecentBooksActivity::GRID_THUMB_MARGIN; const int cols = RecentBooksActivity::GRID_COLS; return (contentWidth - (cols + 1) * margin) / cols; } -// Resolve the [HEIGHT] placeholder in a cover BMP path to the WxH thumbnail path. std::string gridThumbPath(const std::string& coverBmpPath, int tw, int th) { return UITheme::getCoverThumbPath(coverBmpPath, tw, th); } @@ -98,8 +95,7 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = ""; continue; } - const std::string cacheBase = - "/.crosspoint/sidecar_" + std::to_string(std::hash{}(book.path)); + const std::string cacheBase = "/.crosspoint/sidecar_" + std::to_string(std::hash{}(book.path)); const std::string placeholder = cacheBase + "/[HEIGHT].bmp"; const std::string name = std::to_string(tw) + "x" + std::to_string(th) + ".bmp"; const std::string result = convertSidecarToBmp(book.path, book.coverBmpPath, tw, th, name); @@ -108,7 +104,7 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = placeholder; } nextCoverIndex++; - return false; // yield — one conversion per loop tick + return false; } const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); @@ -127,11 +123,11 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = ""; } nextCoverIndex++; - return false; // yield — one generation per loop tick + return false; } } - return true; // all thumbnails are ready + return true; } void RecentBooksActivity::onEnter() { @@ -149,6 +145,8 @@ void RecentBooksActivity::onEnter() { coversLoading = false; firstRenderDone = false; nextCoverIndex = 0; + prevSelectorIndex = -1; + fullRedrawNeeded = true; requestUpdate(); } @@ -158,13 +156,57 @@ void RecentBooksActivity::onExit() { recentBooks.clear(); } +void RecentBooksActivity::switchViewMode(bool grid) { + APP_STATE.recentBooksGridView = grid; + APP_STATE.saveToFile(); + coversLoaded = false; + coversLoading = false; + firstRenderDone = false; + nextCoverIndex = 0; + prevSelectorIndex = -1; + fullRedrawNeeded = true; + requestUpdate(true); +} + +void RecentBooksActivity::removeSelectedBook() { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string bookPath = recentBooks[selectorIndex].path; + const std::string bookTitle = recentBooks[selectorIndex].title; + auto handler = [this, bookPath](const ActivityResult& res) { + if (!res.isCancelled) { + LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); + RECENT_BOOKS.removeBook(bookPath); + loadRecentBooks(); + if (recentBooks.empty()) { + selectorIndex = 0; + } else if (selectorIndex >= static_cast(recentBooks.size())) { + selectorIndex = static_cast(recentBooks.size()) - 1; + } + prevSelectorIndex = -1; + fullRedrawNeeded = true; + requestUpdate(true); + } + }; + std::string heading = tr(STR_REMOVE) + std::string("? "); + startActivityForResult(std::make_unique(renderer, mappedInput, heading, bookTitle), handler); +} + +void RecentBooksActivity::showSelectedBookInfo() { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string& path = recentBooks[selectorIndex].path; + if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { + startActivityForResult(std::make_unique(renderer, mappedInput, path), + [this](const ActivityResult&) { requestUpdate(); }); + } +} + void RecentBooksActivity::loop() { const bool gridView = APP_STATE.recentBooksGridView; + const int listSize = static_cast(recentBooks.size()); - // --- Consume named button events first --- ButtonEventManager::ButtonEvent ev; while (buttonEvents.consumeEvent(ev)) { - // Open book (Confirm short/long) + // Confirm short/long: open book (long = KOReader sync for EPUBs) if (ev.button == MappedInputManager::Button::Confirm && (ev.type == ButtonEventManager::PressType::Short || ev.type == ButtonEventManager::PressType::Long)) { if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; @@ -186,113 +228,69 @@ void RecentBooksActivity::loop() { return; } - // Back → home + // Back short: go home if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { onGoHome(); return; } - // Left short: switch view or (in list view) remove book - if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short) { - if (!gridView) { - // List view → switch to grid view - APP_STATE.recentBooksGridView = true; - APP_STATE.saveToFile(); - coversLoaded = false; - coversLoading = false; - firstRenderDone = false; - nextCoverIndex = 0; - requestUpdate(true); - return; + // Up short: navigate (row up in grid, previous in list) + if (ev.button == MappedInputManager::Button::Up && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + if (gridView) { + selectorIndex = std::max(0, selectorIndex - GRID_COLS); + } else { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, listSize); + } + requestUpdate(); } - // Grid view: Left navigates to the previous item (column nav). The event is already - // consumed from the queue, so handle it directly here. - if (recentBooks.empty()) return; - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, static_cast(recentBooks.size())); - requestUpdate(); + continue; + } + + // Down short: navigate (row down in grid, next in list) + if (ev.button == MappedInputManager::Button::Down && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + if (gridView) { + selectorIndex = std::min(listSize - 1, selectorIndex + GRID_COLS); + } else { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, listSize); + } + requestUpdate(); + } + continue; + } + + // Up long: toggle between list and grid view + if (ev.button == MappedInputManager::Button::Up && ev.type == ButtonEventManager::PressType::Long) { + switchViewMode(!gridView); return; } - // Right short (list view only): show book info - if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { - if (!gridView) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string& path = recentBooks[selectorIndex].path; - if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { - startActivityForResult(std::make_unique(renderer, mappedInput, path), - [this](const ActivityResult&) { requestUpdate(); }); - return; - } - } else { - // Grid view: Right → move to next item (column navigation) - if (recentBooks.empty()) return; - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, static_cast(recentBooks.size())); - requestUpdate(); - return; - } + // Left long: remove selected book (both views) + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { + removeSelectedBook(); + return; } - // Left long (list view only): remove book - if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { - if (!gridView) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string bookPath = recentBooks[selectorIndex].path; - const std::string bookTitle = recentBooks[selectorIndex].title; - - auto handler = [this, bookPath](const ActivityResult& res) { - if (!res.isCancelled) { - LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); - RECENT_BOOKS.removeBook(bookPath); - loadRecentBooks(); - if (recentBooks.empty()) { - selectorIndex = 0; - } else if (selectorIndex >= static_cast(recentBooks.size())) { - selectorIndex = static_cast(recentBooks.size()) - 1; - } - requestUpdate(true); - } - }; - std::string heading = tr(STR_REMOVE) + std::string("? "); - startActivityForResult( - std::make_unique(renderer, mappedInput, heading, bookTitle), handler); - return; - } + // Right long: show book info (both views) + if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Long) { + showSelectedBookInfo(); + return; } } - // --- ButtonNavigator for Up/Down (both views), and list-view Remove shortcut --- - const int listSize = static_cast(recentBooks.size()); - if (gridView) { - // Up/Down navigate by full row (GRID_COLS items at a time) - if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { - if (!recentBooks.empty()) { - selectorIndex = std::max(0, selectorIndex - GRID_COLS); - requestUpdate(); - } - } - if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { - if (!recentBooks.empty()) { - selectorIndex = std::min(listSize - 1, selectorIndex + GRID_COLS); - requestUpdate(); - } - } - - // Lazy cover loading after first render - if (firstRenderDone && !coversLoaded && !coversLoading) { - coversLoading = true; - if (loadNextCover()) { - coversLoaded = true; - } else { - requestUpdate(); - } - coversLoading = false; - } - } else { - // List view: Up/Down with double-click and long-press special behaviour - buttonNavigator.onNextList({MappedInputManager::Button::Down}, selectorIndex, listSize, + // Left/Right short: column navigation (no long-press conflict on these buttons) + buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, [this] { requestUpdate(); }); - buttonNavigator.onPreviousList({MappedInputManager::Button::Up}, selectorIndex, listSize, + buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, + [this] { requestUpdate(); }); + // (Up/Down handled via buttonEvents Short above) + } else { + // Left/Right short: consumed cleanly (no-op in list view but prevents phantom events) + buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, + [this] { requestUpdate(); }); + buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, [this] { requestUpdate(); }); } } @@ -303,6 +301,15 @@ void RecentBooksActivity::render(RenderLock&& lock) { if (!firstRenderDone) { firstRenderDone = true; requestUpdate(); + } else if (!coversLoaded && !coversLoading) { + coversLoading = true; + if (loadNextCover()) { + coversLoaded = true; + } else { + fullRedrawNeeded = true; + requestUpdate(); + } + coversLoading = false; } } else { renderListView(std::move(lock)); @@ -327,8 +334,7 @@ void RecentBooksActivity::renderListView(RenderLock&&) { } else { GUI.drawList( renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, - static_cast(recentBooks.size()), selectorIndex, - [this](int index) { return recentBooks[index].title; }, + static_cast(recentBooks.size()), selectorIndex, [this](int index) { return recentBooks[index].title; }, [this](int index) { const auto& book = recentBooks[index]; if (!book.author.empty() && !book.series.empty()) return book.author + "\n" + book.series; @@ -338,113 +344,134 @@ void RecentBooksActivity::renderListView(RenderLock&&) { [this](int index) { return UITheme::getFileIcon(recentBooks[index].path); }); } - const bool hasInfo = !recentBooks.empty() && selectorIndex < static_cast(recentBooks.size()) && - (FsHelpers::hasEpubExtension(recentBooks[selectorIndex].path) || - FsHelpers::hasXtcExtension(recentBooks[selectorIndex].path)); const bool hasBooks = !recentBooks.empty(); - // Left short switches to grid view; label it accordingly. Long Left = remove. - const auto labels = - mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", tr(STR_VIEW_GRID), - hasInfo ? tr(STR_INFO) : ""); - + const auto labels = mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); renderer.displayBuffer(); } -void RecentBooksActivity::renderGridView(RenderLock&&) { - renderer.clearScreen(); +void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, int cellY, int tw, int th, int labelW) { + const auto& book = recentBooks[index]; + const int labelY = cellY + th + 3; + const int cellFillHeight = th + GRID_LABEL_HEIGHT + 3; + if (selected) { + renderer.fillRect(cellX, cellY, tw, cellFillHeight); + renderer.drawRect(cellX, cellY, tw, th, false); + } else { + // Clear to white before redrawing (needed when deselecting) + renderer.fillRect(cellX, cellY, tw, cellFillHeight, false); + renderer.drawRect(cellX, cellY, tw, th); + } + + // White prefill inside thumbnail frame so 1-bit BMP transparent pixels show white + renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); + + if (!book.coverBmpPath.empty()) { + const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); + FsFile file; + if (Storage.openFileForRead("RBA", thumbPath, file)) { + Bitmap bmp(file); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + const int imgW = bmp.getWidth(); + const int imgH = bmp.getHeight(); + const int innerW = tw - 2; + const int innerH = th - 2; + if (imgW > 0 && imgH > 0) { + // Mirror drawBitmap1Bit scale = min(maxW/imgW, maxH/imgH) to get rendered size, + // then center the image within the frame. + const float scaleX = static_cast(innerW) / imgW; + const float scaleY = static_cast(innerH) / imgH; + const float scale = std::min(scaleX, scaleY); + const int rendW = static_cast(imgW * scale); + const int rendH = static_cast(imgH * scale); + const int offsetX = std::max(1, (tw - rendW) / 2); + const int offsetY = std::max(1, (th - rendH) / 2); + renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, innerW, innerH); + } + } + file.close(); + } + } + + // Label: title line 1, author line 2; white text on black for selected, black on white otherwise + const bool black = !selected; + std::string titleStr = renderer.truncatedText(SMALL_FONT_ID, book.title.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY, titleStr.c_str(), black); + if (!book.author.empty()) { + std::string authorStr = renderer.truncatedText(SMALL_FONT_ID, book.author.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY + 17, authorStr.c_str(), black); + } +} + +void RecentBooksActivity::renderGridView(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect contentRect = UITheme::getContentRect(renderer, true, true); + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; + const int margin = GRID_THUMB_MARGIN; + const int tw = gridThumbWidth(contentRect.width); + const int th = GRID_THUMB_HEIGHT; + const int cellHeight = th + GRID_LABEL_HEIGHT + margin; + const int visibleRows = std::max(1, contentHeight / cellHeight); + const int totalRows = (static_cast(recentBooks.size()) + GRID_COLS - 1) / GRID_COLS; + const int selectedRow = selectorIndex / GRID_COLS; + const int pageStartRow = (selectedRow / visibleRows) * visibleRows; + const int startIndex = pageStartRow * GRID_COLS; + const int labelW = tw - 4; + + auto cellPos = [&](int i, int& cx, int& cy) { + const int row = (i / GRID_COLS) - pageStartRow; + const int col = i % GRID_COLS; + cx = contentRect.x + margin + col * (tw + margin); + cy = contentTop + row * cellHeight; + }; + + // Partial fast path: only the selection changed within the same page + const int prevPage = prevSelectorIndex >= 0 ? (prevSelectorIndex / GRID_COLS / visibleRows) : -1; + const int curPage = selectedRow / visibleRows; + if (!fullRedrawNeeded && prevSelectorIndex >= 0 && prevSelectorIndex != selectorIndex && prevPage == curPage) { + int cx, cy; + cellPos(prevSelectorIndex, cx, cy); + renderGridCell(prevSelectorIndex, false, cx, cy, tw, th, labelW); + cellPos(selectorIndex, cx, cy); + renderGridCell(selectorIndex, true, cx, cy, tw, th, labelW); + prevSelectorIndex = selectorIndex; + renderer.displayBuffer(); + return; + } + + // Full redraw + fullRedrawNeeded = false; + prevSelectorIndex = selectorIndex; + + renderer.clearScreen(); GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, tr(STR_MENU_RECENT_BOOKS)); - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; - if (recentBooks.empty()) { renderer.drawText(UI_10_FONT_ID, contentRect.x + metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS)); - const auto labels = mappedInput.mapLabels(tr(STR_HOME), "", tr(STR_VIEW_LIST), ""); + const auto labels = mappedInput.mapLabels(tr(STR_HOME), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; } - const int margin = GRID_THUMB_MARGIN; - const int tw = gridThumbWidth(contentRect.width); - const int th = GRID_THUMB_HEIGHT; - const int cellHeight = th + GRID_LABEL_HEIGHT + margin; - - const int visibleRows = std::max(1, contentHeight / cellHeight); - const int totalRows = (static_cast(recentBooks.size()) + GRID_COLS - 1) / GRID_COLS; - - const int selectedRow = selectorIndex / GRID_COLS; - const int pageStartRow = (selectedRow / visibleRows) * visibleRows; - - const int startIndex = pageStartRow * GRID_COLS; const int endIndex = std::min(startIndex + visibleRows * GRID_COLS, static_cast(recentBooks.size())); - for (int i = startIndex; i < endIndex; i++) { - const int row = (i / GRID_COLS) - pageStartRow; - const int col = i % GRID_COLS; - - const int cellX = contentRect.x + margin + col * (tw + margin); - const int cellY = contentTop + row * cellHeight; - - const bool selected = (i == selectorIndex); - - // Draw thumbnail border (double rect when selected) - if (selected) { - renderer.drawRect(cellX - 2, cellY - 2, tw + 4, th + 4); - renderer.drawRect(cellX - 1, cellY - 1, tw + 2, th + 2); - } else { - renderer.drawRect(cellX, cellY, tw, th); - } - - // Draw cover BMP thumbnail - const auto& book = recentBooks[i]; - bool coverDrawn = false; - if (!book.coverBmpPath.empty()) { - const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); - FsFile file; - if (Storage.openFileForRead("RBA", thumbPath, file)) { - Bitmap bmp(file); - if (bmp.parseHeaders() == BmpReaderError::Ok) { - renderer.drawBitmap1Bit(bmp, cellX, cellY, tw, th); - coverDrawn = true; - } - file.close(); - } - } - - if (!coverDrawn) { - // Empty placeholder — just a white box inside the border - renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); - } - - // Label: title on line 1, author on line 2 - const int labelY = cellY + th + 3; - const int labelW = tw - 4; - const bool invert = !selected; - - std::string titleStr = renderer.truncatedText(SMALL_FONT_ID, book.title.c_str(), labelW); - renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY, titleStr.c_str(), invert); - - if (!book.author.empty()) { - std::string authorStr = renderer.truncatedText(SMALL_FONT_ID, book.author.c_str(), labelW); - renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY + 17, authorStr.c_str(), invert); - } + int cx, cy; + cellPos(i, cx, cy); + renderGridCell(i, i == selectorIndex, cx, cy, tw, th, labelW); } // Scroll arrows when content spans multiple pages if (totalRows > visibleRows) { constexpr int arrowSize = 6; const int centerX = contentRect.x + contentRect.width / 2; - if (pageStartRow > 0) { const int arrowY = contentTop + 2; for (int j = 0; j < arrowSize; ++j) { @@ -460,7 +487,16 @@ void RecentBooksActivity::renderGridView(RenderLock&&) { } } - const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_VIEW_LIST), ""); + // On X4 (taller screen) there is room for a one-line gesture hint below the grid. + if (!gpio.deviceIsX3()) { + const int hintY = contentRect.y + contentRect.height - metrics.verticalSpacing - 14; + const std::string hint = std::string(tr(STR_DIR_UP)) + "+L: " + tr(STR_VIEW_GRID) + "/" + tr(STR_VIEW_LIST) + + " " + tr(STR_DIR_LEFT) + "+L: " + tr(STR_REMOVE) + " " + tr(STR_DIR_RIGHT) + + "+L: " + tr(STR_INFO); + renderer.drawText(SMALL_FONT_ID, contentRect.x + metrics.contentSidePadding, hintY, hint.c_str()); + } + + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index f11bafb6..0469699a 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -13,7 +13,7 @@ class RecentBooksActivity final : public Activity { public: static constexpr int GRID_COLS = 3; - static constexpr int GRID_THUMB_HEIGHT = 180; + static constexpr int GRID_THUMB_HEIGHT = 160; static constexpr int GRID_THUMB_MARGIN = 10; static constexpr int GRID_LABEL_HEIGHT = 36; // two small-font lines below each thumbnail @@ -31,10 +31,21 @@ class RecentBooksActivity final : public Activity { bool firstRenderDone = false; size_t nextCoverIndex = 0; + // Partial selection repaint: track previous index so we only redraw two cells + int prevSelectorIndex = -1; + bool fullRedrawNeeded = true; + void loadRecentBooks(); // Generates the next missing grid thumbnail (one per call). Returns true when all done. bool loadNextCover(); + void switchViewMode(bool grid); + void removeSelectedBook(); + void showSelectedBookInfo(); + + // Draws a single grid cell (used for both full render and partial selection update). + void renderGridCell(int index, bool selected, int cellX, int cellY, int tw, int th, int labelW); + void renderListView(RenderLock&&); void renderGridView(RenderLock&&);