From d7931dab1aed095b1e16ed2b12e5e98cfe1e8576 Mon Sep 17 00:00:00 2001 From: PoloNX Date: Sat, 3 May 2025 16:36:48 +0200 Subject: [PATCH] ui : added a grid to list mods --- include/views/mods_list.hpp | 23 +- include/views/recycling_grid.hpp | 267 +++++++++++ resources/xml/tabs/mods.xml | 2 +- source/main.cpp | 2 + source/views/mod_preview.cpp | 186 ++++---- source/views/mods_list.cpp | 79 +++- source/views/recycling_grip.cpp | 763 +++++++++++++++++++++++++++++++ source/views/settings_tab.cpp | 2 +- 8 files changed, 1193 insertions(+), 131 deletions(-) create mode 100644 include/views/recycling_grid.hpp create mode 100644 source/views/recycling_grip.cpp diff --git a/include/views/mods_list.hpp b/include/views/mods_list.hpp index 09a01bd..8d5a135 100644 --- a/include/views/mods_list.hpp +++ b/include/views/mods_list.hpp @@ -2,10 +2,11 @@ #include +#include "views/recycling_grid.hpp" #include "api/mod.hpp" #include "api/game.hpp" -class ModCell : public brls::RecyclerCell +class ModCell : public RecyclingGridItem { public: ModCell(); @@ -34,16 +35,21 @@ class CategorieCell : public brls::RecyclerCell static CategorieCell* create(); }; -class ModData : public brls::RecyclerDataSource +class ModData : public RecyclingGridDataSource { public: ModData(Game game); - int numberOfSections(brls::RecyclerFrame* recycler) override; - int numberOfRows(brls::RecyclerFrame* recycler, int section) override; - brls::RecyclerCell* cellForRow(brls::RecyclerFrame* recycler, brls::IndexPath index) override; - void didSelectRowAt(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) override; - std::string titleForHeader(brls::RecyclerFrame* recycler, int section) override; + // int numberOfSections(brls::RecyclerFrame* recycler) override; + // int numberOfRows(brls::RecyclerFrame* recycler, int section) override; + // brls::RecyclerCell* cellForRow(brls::RecyclerFrame* recycler, brls::IndexPath index) override; + // void didSelectRowAt(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) override; + // std::string titleForHeader(brls::RecyclerFrame* recycler, int section) override; + + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override; + size_t getItemCount() override; + void onItemSelected(RecyclingGrid* recycler, size_t index) override; + void clearData() override; std::unique_ptr& getModList() { return modList; } private: @@ -57,11 +63,12 @@ class ModListTab : public brls::Box { public: ModListTab(Game& game); ModListTab(); + ~ModListTab() override; //static brls::View* create(); private: - BRLS_BIND(brls::RecyclerFrame, recycler, "modrecycler"); + BRLS_BIND(RecyclingGrid, recycler, "modrecycler"); std::unique_ptr modData; }; \ No newline at end of file diff --git a/include/views/recycling_grid.hpp b/include/views/recycling_grid.hpp new file mode 100644 index 0000000..6eb6a5e --- /dev/null +++ b/include/views/recycling_grid.hpp @@ -0,0 +1,267 @@ +// +// Created by fang on 2022/6/15. +// + +// register this view in main.cpp +//#include "view/recycling_grid.hpp" +// brls::Application::registerXMLView("RecyclingGrid", RecyclingGrid::create); +// +#include +#include +#include + +namespace brls { +class Label; +class Image; +} // namespace brls +class RecyclingGrid; +class ButtonRefresh; + +class RecyclingGridItem : public brls::Box { +public: + RecyclingGridItem(); + ~RecyclingGridItem() override; + + /* + * Cell's position inside recycler frame + */ + size_t getIndex() const; + + /* + * DO NOT USE! FOR INTERNAL USAGE ONLY! + */ + void setIndex(size_t value); + + /* + * A string used to identify a cell that is reusable. + */ + std::string reuseIdentifier; + + /* + * Prepares a reusable cell for reuse by the recycler frame's data source. + */ + virtual void prepareForReuse() {} + + /* + * 表单项回收 + */ + virtual void cacheForReuse() {} + +private: + size_t index; +}; + +class RecyclingGridDataSource { +public: + virtual ~RecyclingGridDataSource() = default; + + /* + * Tells the data source to return the number of items in a recycler frame. + */ + virtual size_t getItemCount() { return 0; } + + /* + * Asks the data source for a cell to insert in a particular location of the recycler frame. + */ + virtual RecyclingGridItem* cellForRow(RecyclingGrid* recycler, + size_t index) { + return nullptr; + } + + /* + * Asks the data source for the height to use for a row in a specified location. + * Return -1 to use autoscaling. + */ + virtual float heightForRow(RecyclingGrid* recycler, size_t index) { + return -1; + } + + /* + * Tells the data source a row is selected. + */ + virtual void onItemSelected(RecyclingGrid* recycler, size_t index) {} + + virtual void clearData() = 0; +}; + +class RecyclingGridContentBox; + +class RecyclingGrid : public brls::ScrollingFrame { +public: + RecyclingGrid(); + + void draw(NVGcontext* vg, float x, float y, float width, float height, + brls::Style style, brls::FrameContext* ctx) override; + + void registerCell(std::string identifier, + std::function allocation); + + void setDefaultCellFocus(size_t index); + + size_t getDefaultCellFocus() const; + + void setDataSource(RecyclingGridDataSource* source); + + RecyclingGridDataSource* getDataSource() const; + + void showSkeleton(unsigned int num = 30); + + void refresh(); + + void setRefreshAction(const std::function& event); + + // 重新加载数据 + void reloadData(); + + void notifyDataChanged(); + + /// 获取当前指定索引数据所在的item指针 + ///(注意,因为是循环使用列表项的,所以此指针只能在获取时刻在主线程内使用) + RecyclingGridItem* getGridItemByIndex(size_t index); + + std::vector& getGridItems(); + + void clearData(); + + void setEmpty(std::string msg = ""); + + void setError(std::string error = ""); + + void selectRowAt(size_t index, bool animated); + + // 计算从start元素的顶点到index(不包含index)元素顶点的距离 + float getHeightByCellIndex(size_t index, size_t start = 0); + + View* getNextCellFocus(brls::FocusDirection direction, View* currentView); + + void forceRequestNextPage(); + + void onLayout() override; + + /// 当前数据总数量 + size_t getItemCount(); + + /// 当前数据总行数 + size_t getRowCount(); + + /// 导航到页面尾部时触发回调函数 + void onNextPage(const std::function& callback = nullptr); + + void setPadding(float padding) override; + void setPadding(float top, float right, float bottom, float left) override; + void setPaddingTop(float top) override; + void setPaddingRight(float right) override; + void setPaddingBottom(float bottom) override; + void setPaddingLeft(float left) override; + + void setPaddingRightPercentage(float right); + void setPaddingLeftPercentage(float left); + + float getPaddingLeft(); + float getPaddingRight(); + + // 获取一个列表项组件 + // 如果缓存列表中存在就从中取出一个 + // 如果缓存列表为空则生成一个新的 + RecyclingGridItem* dequeueReusableCell(std::string identifier); + + brls::View* getDefaultFocus() override; + + ~RecyclingGrid() override; + + static View* create(); + + /// 元素间距 + float estimatedRowSpace = 20; + + /// 默认行高(元素实际高度 = 默认行高 - 元素间隔) + float estimatedRowHeight = 240; + + /// 列数 + int spanCount = 3; + + /// 预取的行数 + int preFetchLine = 1; + + /// 瀑布流模式,每一项高度不固定(仅在spanCount为1时可用) + bool isFlowMode = false; + +private: + RecyclingGridDataSource* dataSource = nullptr; + bool layouted = false; + float oldWidth = -1; + + bool requestNextPage = false; + // true表示正在请求下一页,此时不会再次触发下一页请求 + // 数据为空时不请求下一页,因为有些时候首页和下一页请求的内容或方式不同 + // 当列表元素有变动时(添加或修改数据源,会重置为false,这是将允许请求下一页) + + uint32_t visibleMin, visibleMax; + size_t defaultCellFocus = 0; + + float paddingTop = 0; + float paddingRight = 0; + float paddingBottom = 0; + float paddingLeft = 0; + + bool paddingPercentage = false; + + std::function nextPageCallback = nullptr; + std::function refreshAction = nullptr; + + RecyclingGridContentBox* contentBox = nullptr; + brls::Image* hintImage; + brls::Label* hintLabel; + brls::Button* refreshButton; + brls::Rect renderedFrame; + std::vector cellHeightCache; + std::map*> queueMap; + std::map> + allocationMap; + + //检查宽度是否有变化 + bool checkWidth(); + + // 回收列表项 + void queueReusableCell(RecyclingGridItem* cell); + + void itemsRecyclingLoop(); + + /** + * 在指定位置添加一个列表项 + * 内部更新 renderedFrame 的值,假设有一个每一项都绘制的超长列表,renderedFrame 的 y 表示当前截取绘制的顶部坐标,height 表示当前绘制的高度 + * 当添加一个列表项时,renderedFrame 的 height 增加一项的高度(注意,只在每行的第一个列表项添加时才更新列表项的高度) + * @param index 指定的位置 + * @param downSide 是向下添加还是向上添加,当向上添加时 将 renderedFrame 的 y 减去当前列表项的高度。(y 的值只在向上添加或移除时候改变) + */ + void addCellAt(size_t index, bool downSide); + + void removeCell(brls::View *view); +}; + +class RecyclingGridContentBox : public brls::Box { +public: + RecyclingGridContentBox(RecyclingGrid* recycler); + brls::View* getNextFocus(brls::FocusDirection direction, + brls::View* currentView) override; + +private: + RecyclingGrid* recycler; +}; + +class SkeletonCell : public RecyclingGridItem { +public: + SkeletonCell(); + + static RecyclingGridItem* create(); + + void draw(NVGcontext* vg, float x, float y, float width, float height, + brls::Style style, brls::FrameContext* ctx) override; + +private: + NVGcolor background = brls::Application::getTheme()["color/grey_3"]; +}; \ No newline at end of file diff --git a/resources/xml/tabs/mods.xml b/resources/xml/tabs/mods.xml index bedb9c8..190357d 100644 --- a/resources/xml/tabs/mods.xml +++ b/resources/xml/tabs/mods.xml @@ -3,7 +3,7 @@ height="auto" axis="column"> - mod.getImagesUrl().size(); - - - + size_t imageCount = this->mod.getImagesUrl().size(); std::vector spinnerViews; - // add spinners to the boxes std::promise spinnersAddedPromise; auto spinnersAddedFuture = spinnersAddedPromise.get_future(); - brls::sync([this, &spinnerViews, &spinnersAddedPromise, &imageCount]() { - brls::Logger::debug("Adding spinners..."); - brls::Logger::debug("Number of image URLs: {}", imageCount); - - // create boxes for the images - for (auto i = 0; i < imageCount; i += 7) { - if (shouldStopThread()) { - brls::Logger::debug("Thread stopped while creating boxes."); - return; + // Créer les composants UI dans le thread principal + brls::sync([this, &spinnerViews, &spinnersAddedPromise, imageCount]() { + try { + brls::Logger::debug("Adding spinners..."); + brls::Logger::debug("Number of image URLs: {}", imageCount); + + // Créer les boîtes horizontales + size_t boxCount = (imageCount + 6) / 7; + for (size_t i = 0; i < boxCount; ++i) { + if (shouldStopThread()) { + brls::Logger::debug("Thread stopped while creating boxes."); + return; + } + + auto box = new brls::Box(); + box->setWidth(bigImageWidth); + box->setHeight(bigImageWidth / 8 * 9 / 16); + box->setAxis(brls::Axis::ROW); + box->setJustifyContent(brls::JustifyContent::FLEX_START); + box->setAlignItems(brls::AlignItems::FLEX_START); + box->setWireframeEnabled(true); + screenshot_box->addView(box); + smallScreenshotsBoxs.push_back(box); } - auto box = new brls::Box(); - box->setWidth(bigImageWidth); - box->setHeight(bigImageWidth/8 * 9/16); - box->setAxis(brls::Axis::ROW); - box->setJustifyContent(brls::JustifyContent::FLEX_START); - box->setAlignItems(brls::AlignItems::FLEX_START); - box->setWireframeEnabled(true); - screenshot_box->addView(box); - smallScreenshotsBoxs.push_back(box); - } - - for (size_t i = 0; i < imageCount; i++) { - if (shouldStopThread()) { - brls::Logger::debug("Thread stopped while adding spinners."); - return; + // Ajouter les spinners dans les boîtes + for (size_t i = 0; i < imageCount; ++i) { + if (shouldStopThread()) { + brls::Logger::debug("Thread stopped while adding spinners."); + return; + } + + auto spinner = new SpinnerImageView( + bigImageWidth / 8, + bigImageWidth / 8 * 9 / 16, + bigImageWidth / 8 / 7 / 2 + ); + size_t boxIndex = i / 7; + smallScreenshotsBoxs[boxIndex]->addView(spinner); + spinnerViews.push_back(spinner); } - auto spinnerImageView = new SpinnerImageView(bigImageWidth/8, bigImageWidth/8 * 9/16, bigImageWidth/8/7/2); - this->smallScreenshotsBoxs[abs(i / 7)]->addView(spinnerImageView); - - spinnerViews.push_back(spinnerImageView); - } + // Navigation entre les spinners + for (size_t i = 0; i < spinnerViews.size(); ++i) { + if (shouldStopThread()) return; - // set custom navidation route - for (size_t i = 0; i < spinnerViews.size(); i++) { - if (shouldStopThread()) { - brls::Logger::debug("Thread stopped while setting navigation route."); - return; - } - if (i < 7) { - spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::DOWN, spinnerViews[i + 7]); - } else if (i >= 7 && i < spinnerViews.size() - 7) { - spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::UP, spinnerViews[i - 7]); - spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::DOWN, spinnerViews[i + 7]); - } else { - spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::UP, spinnerViews[i - 7]); + if (i + 7 < spinnerViews.size()) + spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::DOWN, spinnerViews[i + 7]); + if (i >= 7) + spinnerViews[i]->setCustomNavigationRoute(brls::FocusDirection::UP, spinnerViews[i - 7]); } - } - big_image_box->addView(bigSpinImg); - brls::Logger::debug("Spinners added."); + big_image_box->addView(bigSpinImg); + brls::Logger::debug("Spinners added."); - #ifndef NDEBUG - cfg::Config config; - if (config.getWireframe()) { - this->setWireframeEnabled(true); - for(auto& view : this->getChildren()) { - view->setWireframeEnabled(true); - } - for(auto& view : this->smallScreenshotsBoxs) { - view->setWireframeEnabled(true); - } - for(auto& view : this->scrolling->getChildren()) { - view->setWireframeEnabled(true); + #ifndef NDEBUG + cfg::Config config; + if (config.getWireframe()) { + this->setWireframeEnabled(true); + for (auto* view : this->getChildren()) view->setWireframeEnabled(true); + for (auto* view : this->smallScreenshotsBoxs) view->setWireframeEnabled(true); + for (auto* view : this->scrolling->getChildren()) view->setWireframeEnabled(true); } - } - #endif - + #endif - spinnersAddedPromise.set_value(); + spinnersAddedPromise.set_value(); + } catch (const std::exception& e) { + brls::Logger::error("Exception while adding spinners: {}", e.what()); + spinnersAddedPromise.set_value(); + } }); - // wait for the spinners to be added before downloading images spinnersAddedFuture.wait(); - brls::Logger::debug("SpinnerViews size: {}", spinnerViews.size()); - - for (size_t i = 0; i < spinnerViews.size(); i++) { + // Chargement des images + for (size_t i = 0; i < spinnerViews.size(); ++i) { if (shouldStopThread()) { brls::Logger::debug("Thread stopped during image download."); return; } - std::vector buffer; - - brls::Logger::debug("Downloading image {}", i); - buffer = this->mod.downloadImage(i); - brls::Logger::debug("Downloaded image {} with size {}", i, buffer.size()); - - if (shouldStopThread()) { - brls::Logger::debug("Thread stopped after downloading image {}.", i); - return; + try { + std::vector buffer = this->mod.downloadImage(i); + brls::Logger::debug("Downloaded image {} with size {}", i, buffer.size()); + + if (shouldStopThread()) return; + + // Mettre à jour l'UI dans le thread principal + brls::sync([this, spinnerView = spinnerViews[i], buffer = std::move(buffer), i]() mutable { + if (shouldStopThread()) return; + + if (i == 0) { + bigSpinImg->setImage(buffer); + loadButtons(); + } + + spinnerView->setImage(buffer); + spinnerView->registerClickAction(brls::ActionListener([this, buffer](brls::View*) { + this->bigSpinImg->setImage(buffer); + return true; + })); + }); + } catch (const std::exception& e) { + brls::Logger::error("Failed to download or set image {}: {}", i, e.what()); } - - brls::sync([this, spinnerView = spinnerViews[i], buffer, i]() mutable { - if (shouldStopThread()) { - brls::Logger::debug("Thread stopped while updating UI for image {}.", i); - return; - } - - // if it's the first image, set it to the big image - if (i == 0) { - bigSpinImg->setImage(buffer); - loadButtons(); - } - - brls::Logger::debug("Replacing spinner with image {}", i); - spinnerView->setImage(buffer); - spinnerView->registerClickAction(brls::ActionListener([this, buffer](brls::View* view) { - this->bigSpinImg->setImage(buffer); - return true; - })); - }); } } + bool ModPreview::shouldStopThread() { std::unique_lock lock(threadMutex); return stopThreadFlag || isExiting; diff --git a/source/views/mods_list.cpp b/source/views/mods_list.cpp index 345fe38..42686fc 100644 --- a/source/views/mods_list.cpp +++ b/source/views/mods_list.cpp @@ -38,22 +38,9 @@ CategorieCell* CategorieCell::create() return new CategorieCell(); } -brls::RecyclerCell* ModData::cellForRow(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) -{ - auto cell = (ModCell*)recycler->dequeueReusableCell("Cell"); - brls::Logger::debug("Mod name : {}", modList->getMods()[indexPath.row].getName()); - cell->label->setText(modList->getMods()[indexPath.row].getName()); - cell->subtitle->setText(fmt::format("{} : {}", "menu/mods/author"_i18n, modList->getMods()[indexPath.row].getAuthor())); - return cell; -} -void ModData::didSelectRowAt(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) -{ - brls::Logger::debug("Mod name : {}", modList->getMods()[indexPath.row].getName()); - recycler->present(new ModPreview(modList->getMods()[indexPath.row], bannerBuffer)); - - //recycler->present(new ModPreview(modList->getMods()[indexPath.row])); -} + + ModData::ModData(Game game): game(game) { @@ -65,19 +52,61 @@ ModData::ModData(Game game): game(game) } -int ModData::numberOfSections(brls::RecyclerFrame* recycler) +// brls::RecyclerCell* ModData::cellForRow(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) +// { +// auto cell = (ModCell*)recycler->dequeueReusableCell("Cell"); +// brls::Logger::debug("Mod name : {}", modList->getMods()[indexPath.row].getName()); +// cell->label->setText(modList->getMods()[indexPath.row].getName()); +// cell->subtitle->setText(fmt::format("{} : {}", "menu/mods/author"_i18n, modList->getMods()[indexPath.row].getAuthor())); +// return cell; +// } + +// void ModData::didSelectRowAt(brls::RecyclerFrame* recycler, brls::IndexPath indexPath) +// { +// brls::Logger::debug("Mod name : {}", modList->getMods()[indexPath.row].getName()); +// recycler->present(new ModPreview(modList->getMods()[indexPath.row], bannerBuffer)); + +// //recycler->present(new ModPreview(modList->getMods()[indexPath.row])); +// } + +// int ModData::numberOfSections(brls::RecyclerFrame* recycler) +// { +// return 1; +// } + +// int ModData::numberOfRows(brls::RecyclerFrame* recycler, int section) +// { +// return modList->getMods().size(); +// } + +// std::string ModData::titleForHeader(brls::RecyclerFrame* recycler, int section) +// { +// return ""; +// } + +RecyclingGridItem* ModData::cellForRow(RecyclingGrid* recycler, size_t index) { - return 1; + auto cell = (ModCell*)recycler->dequeueReusableCell("Cell"); + brls::Logger::debug("Mod name : {}", modList->getMods()[index].getName()); + cell->label->setText(modList->getMods()[index].getName()); + cell->subtitle->setText(fmt::format("{} : {}", "menu/mods/author"_i18n, modList->getMods()[index].getAuthor())); + return cell; } -int ModData::numberOfRows(brls::RecyclerFrame* recycler, int section) +size_t ModData::getItemCount() { return modList->getMods().size(); } -std::string ModData::titleForHeader(brls::RecyclerFrame* recycler, int section) +void ModData::onItemSelected(RecyclingGrid* recycler, size_t index) { - return ""; + brls::Logger::debug("Mod name : {}", modList->getMods()[index].getName()); + recycler->present(new ModPreview(modList->getMods()[index], bannerBuffer)); +} + +void ModData::clearData() +{ + modList->getMods().clear(); } ModListTab::ModListTab(Game& game) { @@ -139,7 +168,7 @@ ModListTab::ModListTab(Game& game) { recycler->estimatedRowHeight = 100; recycler->registerCell("Cell", []() { return ModCell::create();}); - recycler->setDataSource(modData.get(), false); + recycler->setDataSource(modData.get()); #ifndef NDEBUG cfg::Config config; @@ -164,4 +193,10 @@ ModListTab::ModListTab(Game& game) { /*brls::View* ModListTab::create() { return new ModListTab(); -}*/ \ No newline at end of file +}*/ + +ModListTab::~ModListTab() { + brls::Logger::debug("ModListTab destructor"); + modData->clearData(); + modData.reset(); +} \ No newline at end of file diff --git a/source/views/recycling_grip.cpp b/source/views/recycling_grip.cpp new file mode 100644 index 0000000..ce8b6bb --- /dev/null +++ b/source/views/recycling_grip.cpp @@ -0,0 +1,763 @@ +// +// Created by fang on 2022/6/15. +// + +#include +#include +#include "views/recycling_grid.hpp" + +/// RecyclingGridItem + +RecyclingGridItem::RecyclingGridItem() { + this->setFocusable(true); + this->registerClickAction([this](View* view) { + auto* recycler = dynamic_cast(getParent()->getParent()); + if (recycler) recycler->getDataSource()->onItemSelected(recycler, index); + return true; + }); + this->addGestureRecognizer(new brls::TapGestureRecognizer(this)); +} + +size_t RecyclingGridItem::getIndex() const { return this->index; } + +void RecyclingGridItem::setIndex(size_t value) { this->index = value; } + +RecyclingGridItem::~RecyclingGridItem() = default; + +/// Skeleton cell + +SkeletonCell::SkeletonCell() { this->setFocusable(false); } + +RecyclingGridItem* SkeletonCell::create() { return new SkeletonCell(); } + +void SkeletonCell::draw(NVGcontext* vg, float x, float y, float width, float height, brls::Style style, + brls::FrameContext* ctx) { + brls::Time curTime = brls::getCPUTimeUsec() / 1000; + float p = (curTime % 1000) * 1.0 / 1000; + p = fabs(0.5 - p) + 0.25; + + NVGcolor end = background; + end.a = p; + + NVGpaint paint = nvgLinearGradient(vg, x, y, x + width, y + height, a(background), a(end)); + nvgBeginPath(vg); + nvgFillPaint(vg, paint); + nvgRoundedRect(vg, x, y, width, height, 6); + nvgFill(vg); +} + +/// Skeleton DataSource + +class DataSourceSkeleton : public RecyclingGridDataSource { +public: + DataSourceSkeleton(unsigned int n) : num(n) {} + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) { + SkeletonCell* item = (SkeletonCell*)recycler->dequeueReusableCell("Skeleton"); + item->setHeight(recycler->estimatedRowHeight); + return item; + } + + size_t getItemCount() { return this->num; } + + void clearData() { this->num = 0; } + +private: + unsigned int num; +}; + +/// RecyclingGrid + +RecyclingGrid::RecyclingGrid() { + brls::Logger::debug("View RecyclingGrid: create"); + + // Create hint views + this->hintImage = new brls::Image(); + this->hintImage->detach(); + //this->hintImage->setImageFromRes("pictures/empty.png"); + this->hintLabel = new brls::Label(); + this->hintLabel->detach(); + this->hintLabel->setFontSize(14); + this->hintLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER); + + // Create Refresh button + this->refreshButton = new brls::Button(); + this->refreshButton->detach(); + this->refreshButton->setVisibility(brls::Visibility::GONE); + this->refreshButton->registerClickAction([this](...) { + if (this->refreshAction) this->refreshAction(); + return true; + }); + // 使用 Box 的 addView,添加一个 detached button + brls::Box::addView(this->refreshButton); + + this->setFocusable(false); + + this->setScrollingBehavior(brls::ScrollingBehavior::CENTERED); + // Create content box + this->contentBox = new RecyclingGridContentBox(this); + this->setContentView(this->contentBox); + + this->registerFloatXMLAttribute("itemHeight", [this](float value) { + this->estimatedRowHeight = value; + this->reloadData(); + }); + + this->registerFloatXMLAttribute("spanCount", [this](float value) { + if (value != 1) { + isFlowMode = false; + } + this->spanCount = value; + this->reloadData(); + }); + + this->registerFloatXMLAttribute("itemSpace", [this](float value) { + this->estimatedRowSpace = value; + this->reloadData(); + }); + + this->registerFloatXMLAttribute("preFetchLine", [this](float value) { + this->preFetchLine = value; + this->reloadData(); + }); + + this->registerBoolXMLAttribute("flowMode", [this](bool value) { + this->spanCount = 1; + this->isFlowMode = value; + this->reloadData(); + }); + + this->registerPercentageXMLAttribute("paddingRight", + [this](float percentage) { this->setPaddingRightPercentage(percentage); }); + + this->registerPercentageXMLAttribute("paddingLeft", + [this](float percentage) { this->setPaddingLeftPercentage(percentage); }); + + this->registerCell("Skeleton", []() { return SkeletonCell::create(); }); + this->showSkeleton(); +} + +RecyclingGrid::~RecyclingGrid() { + brls::Logger::debug("View RecyclingGridActivity: delete"); + if (this->hintImage) this->hintImage->freeView(); + this->hintImage = nullptr; + if (this->hintLabel) this->hintLabel->freeView(); + this->hintLabel = nullptr; + //delete this->dataSource; + for (const auto& it : queueMap) { + for (auto item : *it.second) { + item->setParent(nullptr); + if (item->isPtrLocked()) + item->freeView(); + else + delete item; + } + delete it.second; + } +} + +void RecyclingGrid::draw(NVGcontext* vg, float x, float y, float width, float height, brls::Style style, + brls::FrameContext* ctx) { + // 触摸或鼠标滑动时会导致屏幕元素位置变更 + // 简单地在draw函数中调用itemsRecyclingLoop 实现动态的增删元素 + // todo:只在滑动过程中调用 itemsRecyclingLoop 以节省静止时的计算消耗 + itemsRecyclingLoop(); + + ScrollingFrame::draw(vg, x, y, width, height, style, ctx); + + if (!this->dataSource || this->dataSource->getItemCount() == 0) { + if (!this->hintImage) return; + float w1 = hintImage->getWidth(), w2 = hintLabel->getWidth(); + float h1 = hintImage->getHeight(), h2 = hintLabel->getHeight(); + this->hintImage->setAlpha(this->getAlpha()); + this->hintImage->draw(vg, x + (width - w1) / 2, y + (height - h1) / 2, w1, h1, style, ctx); + this->hintLabel->setAlpha(this->getAlpha()); + this->hintLabel->draw(vg, x + (width - w2) / 2, y + (height + h1) / 2, w2, h2, style, ctx); + } +} + +void RecyclingGrid::registerCell(std::string identifier, std::function allocation) { + queueMap.insert(std::make_pair(identifier, new std::vector())); + allocationMap.insert(std::make_pair(identifier, allocation)); +} + +void RecyclingGrid::addCellAt(size_t index, bool downSide) { + RecyclingGridItem* cell; + //获取到一个填充好数据的cell + cell = dataSource->cellForRow(this, index); + + float cellHeight = estimatedRowHeight; + float cellWidth = (renderedFrame.getWidth() - getPaddingLeft() - getPaddingRight()) / spanCount - + cell->getMarginLeft() - cell->getMarginRight(); + float cellX = renderedFrame.getMinX() + getPaddingLeft(); + + if (isFlowMode) { + // 必须在 getHeight 前设置宽度,否则会影响到cell自定义高度的判定 + cell->setWidth(cellWidth); + if (cellHeightCache[index] == -1) { + // 没有预定义cell的高度,使用cell默认的高度 + cellHeight = cell->getHeight(); + + if (cellHeight > estimatedRowHeight) { + cellHeight = estimatedRowHeight; + } + cellHeightCache[index] = cellHeight; + } else { + // dataSource 中指定了cell的高度,使用预定义的值 + cellHeight = cellHeightCache[index]; + } + + brls::Logger::verbose("Add cell at: y {} height {}", getHeightByCellIndex(index) + paddingTop, cellHeight); + } else { + cell->setWidth(cellWidth - estimatedRowSpace); + cellX += (renderedFrame.getWidth() - getPaddingLeft() - getPaddingRight()) / spanCount * (index % spanCount); + } + + cell->setHeight(cellHeight); + cell->setDetachedPositionX(cellX); + cell->setDetachedPositionY(getHeightByCellIndex(index) + paddingTop); + cell->setIndex(index); + + this->contentBox->getChildren().insert(this->contentBox->getChildren().end(), cell); + + // Allocate and set parent userdata + size_t* userdata = (size_t*)malloc(sizeof(size_t)); + *userdata = index; + + cell->setParent(this->contentBox, userdata); + + // Layout and events + this->contentBox->invalidate(); + cell->View::willAppear(); + + if (index < visibleMin) visibleMin = index; + + if (index > visibleMax) visibleMax = index; + + // 只有元素出现在首列时才需要考虑修改 renderedFrame + if (index % spanCount == 0) { + if (!downSide) renderedFrame.origin.y -= cellHeight + estimatedRowSpace; + + renderedFrame.size.height += cellHeight + estimatedRowSpace; + } + + // 瀑布流模式需要不断修正高度 + if (isFlowMode) + contentBox->setHeight(getHeightByCellIndex(this->dataSource->getItemCount()) + paddingTop + paddingBottom); + + brls::Logger::verbose("Cell #{} - added", index); +} + +void RecyclingGrid::removeCell(brls::View* view) { + if (!view) return; + + // Find the index of the view + size_t index; + bool found = false; + auto& children = this->contentBox->getChildren(); + + for (size_t i = 0; i < children.size(); i++) { + View* child = children[i]; + + if (child == view) { + found = true; + index = i; + break; + } + } + + if (!found) return; + + // Remove it + children.erase(children.begin() + index); + + view->willDisappear(true); + + this->invalidate(); +} + +void RecyclingGrid::setDataSource(RecyclingGridDataSource* source) { + if (this->dataSource) delete this->dataSource; + + // 允许自动加载下一页 + this->requestNextPage = false; + this->dataSource = source; + if (layouted) reloadData(); +} + +void RecyclingGrid::reloadData() { + if (!layouted) return; + + // 将所有节点从屏幕上移除放入重复利用的列表中 + auto &children = this->contentBox->getChildren(); + for (auto const& child : children) { + queueReusableCell((RecyclingGridItem*)child); + child->willDisappear(true); + } + children.clear(); + + visibleMin = UINT_MAX; + visibleMax = 0; + + renderedFrame = brls::Rect(); + renderedFrame.size.width = getWidth(); + if (renderedFrame.size.width != renderedFrame.size.width) { + // 当列表在展示骨架屏后被隐藏,这时获取到 width 的值为 NAN + // 使用历史宽度值避免后续计算错误 + renderedFrame.size.width = oldWidth; + } + + setContentOffsetY(0, false); + if (dataSource == nullptr) return; + if (dataSource->getItemCount() <= 0) { + contentBox->setHeight(0); + return; + } + size_t cellFocusIndex = this->defaultCellFocus; + if (cellFocusIndex >= dataSource->getItemCount()) cellFocusIndex = dataSource->getItemCount() - 1; + + // 设置列表的高度(真实高度,非显示的高度) + if (!isFlowMode || spanCount != 1) { + // 设置了固定的高度 + contentBox->setHeight((estimatedRowHeight + estimatedRowSpace) * (float)getRowCount() + paddingTop + + paddingBottom); + // 添加当前焦点 cell 所在行的第一项到屏幕,其余项通过 selectRowAt 内的 itemsRecyclingLoop 自动添加 + // 这里添加首项是因为添加首项时会变更 renderedFrame 的 height 值,包括 itemsRecyclingLoop 内的计算也都是以首项为基准进行的 + // 原则上这里的 addCellAt 任意添加一项即可(比如添加第零项),但最好能添加到 cellFocusIndex 附近,这有助于提升首屏性能 + size_t lineHeadIndex = cellFocusIndex / spanCount * spanCount; + // 更新 renderedFrame 数据,设置y的值,伪装成已经移除了 lineHeadIndex 项之前的列表项 + // y 值表示当前列表渲染的顶部,低于 y 值高度的列表项不会被渲染 + renderedFrame.origin.y = getHeightByCellIndex(lineHeadIndex); + + // 添加 lineHeadIndex 项到需要渲染的列表项中,因为 lineHeadIndex 为一行的首项,在执行 addCellAt 之后会更新 renderedFrame 数据 + // renderedFrame 的 height 调整为第 lineHeadIndex 项的高度 + this->addCellAt(lineHeadIndex, true); + } else { + // 获取每个cell的高度并缓存起来 + cellHeightCache.clear(); + for (size_t section = 0; section < dataSource->getItemCount(); section++) { + float height = dataSource->heightForRow(this, section); + cellHeightCache.push_back(height); + } + contentBox->setHeight(getHeightByCellIndex(dataSource->getItemCount()) + paddingTop + paddingBottom); + // 流式布局无法准确确定焦点cell的位置,因此暂时只添加第一项,在 itemsRecyclingLoop 中会逐渐添加到 cellFocusIndex,再按需删除 + // 当 cellFocusIndex 过于大时,会导致添加的元素过多,因此需要考虑优化 + this->addCellAt(0, true); + } + + // 在前面的操作中,列表增加了一项,通过 selectRowAt 再精确地显示出具体选中项 + selectRowAt(cellFocusIndex, false); +} + +void RecyclingGrid::notifyDataChanged() { + // todo: 目前仅能处理data在原本的基础上增加的情况,需要考虑data减少或更换时的情况 + if (!layouted) return; + + if (dataSource) { + if (isFlowMode) { + for (size_t i = cellHeightCache.size(); i < dataSource->getItemCount(); i++) { + float height = dataSource->heightForRow(this, i); + cellHeightCache.push_back(height); + } + contentBox->setHeight(getHeightByCellIndex(this->dataSource->getItemCount()) + paddingTop + paddingBottom); + } else { + contentBox->setHeight((estimatedRowHeight + estimatedRowSpace) * this->getRowCount() + paddingTop + + paddingBottom); + } + } + // 数据增多后重新允许加载下一页 + requestNextPage = false; +} + +RecyclingGridItem* RecyclingGrid::getGridItemByIndex(size_t index) { + for (brls::View* i : contentBox->getChildren()) { + RecyclingGridItem* v = dynamic_cast(i); + if (!v) continue; + if (v->getIndex() == index) return v; + } + // 当前索引数据没有绑定列表项 + return nullptr; +} + +std::vector& RecyclingGrid::getGridItems() { + return (std::vector&)contentBox->getChildren(); +} + +void RecyclingGrid::clearData() { + if (dataSource) { + dataSource->clearData(); + this->reloadData(); + } +} + +void RecyclingGrid::setEmpty(std::string msg) { + this->hintImage->setImageFromRes("pictures/empty.png"); + this->hintLabel->setText(msg); + this->clearData(); +} + +void RecyclingGrid::setError(std::string error) { + this->hintImage->setImageFromRes("pictures/net_error.png"); + this->hintLabel->setText(error); + this->clearData(); +} + +void RecyclingGrid::setDefaultCellFocus(size_t index) { this->defaultCellFocus = index; } + +size_t RecyclingGrid::getDefaultCellFocus() const { return this->defaultCellFocus; } + +size_t RecyclingGrid::getItemCount() { return this->dataSource->getItemCount(); } + +size_t RecyclingGrid::getRowCount() { return (this->dataSource->getItemCount() - 1) / this->spanCount + 1; } + +void RecyclingGrid::onNextPage(const std::function& callback) { this->nextPageCallback = callback; } + +void RecyclingGrid::itemsRecyclingLoop() { + if (!dataSource) return; + + brls::Rect visibleFrame = getVisibleFrame(); + + // 上方元素自动销毁 + while (true) { + RecyclingGridItem* minCell = nullptr; + for (auto it : contentBox->getChildren()) + if (*((size_t*)it->getParentUserData()) == visibleMin) minCell = (RecyclingGridItem*)it; + + // 当第一个cell的顶部 与 组件顶部的距离大于 preFetchLine 行元素的距离时结束 + if (!minCell || (minCell->getDetachedPosition().y + + getHeightByCellIndex(visibleMin + (preFetchLine + 1) * spanCount, visibleMin) >= + visibleFrame.getMinY())) + break; + + float cellHeight = estimatedRowHeight; + if (isFlowMode) cellHeight = cellHeightCache[visibleMin]; + + renderedFrame.origin.y += minCell->getIndex() % spanCount == 0 ? cellHeight + estimatedRowSpace : 0; + renderedFrame.size.height -= minCell->getIndex() % spanCount == 0 ? cellHeight + estimatedRowSpace : 0; + + queueReusableCell(minCell); + this->removeCell(minCell); + + brls::Logger::verbose("Cell #{} - destroyed", visibleMin); + + visibleMin++; + } + + // 下方元素自动销毁 + while (true) { + RecyclingGridItem* maxCell = nullptr; + for (auto it : contentBox->getChildren()) + if (*((size_t*)it->getParentUserData()) == visibleMax) maxCell = (RecyclingGridItem*)it; + + // 当最后一个cell的顶部 与 组件底部间的距离 小于 preFetchLine 行元素的距离时结束 + if (!maxCell || (maxCell->getDetachedPosition().y - + getHeightByCellIndex(visibleMax, visibleMax - preFetchLine * spanCount) <= + visibleFrame.getMaxY())) + break; + if (visibleMax == 0) { + break; + } + + float cellHeight = estimatedRowHeight; + if (isFlowMode) cellHeight = cellHeightCache[visibleMax]; + + renderedFrame.size.height -= maxCell->getIndex() % spanCount == 0 ? cellHeight + estimatedRowSpace : 0; + + queueReusableCell(maxCell); + this->removeCell(maxCell); + + brls::Logger::verbose("Cell #{} - destroyed", visibleMax); + + visibleMax--; + } + + // 上方元素自动添加 + while (visibleMin - 1 < dataSource->getItemCount()) { + if ((visibleMin) % spanCount == 0) + // 当 renderedFrame 顶部 与 组件顶部的距离小于 preFetchLine 行cell的距离时结束 + if (renderedFrame.getMinY() + getHeightByCellIndex(visibleMin + preFetchLine * spanCount, visibleMin) < + visibleFrame.getMinY() - paddingTop) { + break; + } + addCellAt(visibleMin - 1, false); + } + + // 下方元素自动添加 + while (visibleMax + 1 < dataSource->getItemCount()) { + // 当即将被添加的元素为新一行的开始时结束,否则填充满一整行 + if ((visibleMax + 1) % spanCount == 0) + // 如果 renderedFrame 底部 与 组件底部 距离超过了preFetchLine 行cell的距离时结束 + if (renderedFrame.getMaxY() - + getHeightByCellIndex(visibleMax + 1, visibleMax + 1 - preFetchLine * spanCount) > + visibleFrame.getMaxY() - paddingBottom) { + requestNextPage = false; // 允许加载下一页 + break; + } + addCellAt(visibleMax + 1, true); + } + + if (visibleMax + 1 >= this->getItemCount()) { + // 只有当 requestNextPage 为false时,才可以请求下一页,避免多次重复请求 + if (!requestNextPage && nextPageCallback) { + // 有数据、不是骨架屏数据、数据不为空 + if (dataSource && !dynamic_cast(dataSource) && dataSource->getItemCount() > 0) { + brls::Logger::debug("RecyclingGrid request next page"); + requestNextPage = true; + this->nextPageCallback(); + } + } + } +} + +RecyclingGridDataSource* RecyclingGrid::getDataSource() const { return this->dataSource; } + +void RecyclingGrid::showSkeleton(unsigned int num) { this->setDataSource(new DataSourceSkeleton(num)); } + +void RecyclingGrid::refresh() { + if (this->refreshAction) this->refreshAction(); +} + +void RecyclingGrid::setRefreshAction(const std::function& event) { + this->refreshAction = event; + this->refreshButton->setVisibility(brls::Visibility::VISIBLE); +} + +void RecyclingGrid::selectRowAt(size_t index, bool animated) { + this->setContentOffsetY(getHeightByCellIndex(index), animated); + this->itemsRecyclingLoop(); + + for (View* view : contentBox->getChildren()) { + if (*((size_t*)view->getParentUserData()) == index) { + contentBox->setLastFocusedView(view); + break; + } + } +} + +float RecyclingGrid::getHeightByCellIndex(size_t index, size_t start) { + if (index <= start) return 0; + if (!isFlowMode) return (estimatedRowHeight + estimatedRowSpace) * (size_t)((index - start) / spanCount); + + if (cellHeightCache.size() == 0) { + brls::Logger::error("cellHeightCache.size() cannot be zero in flow mode {} {}", start, index); + return 0; + } + + if (start < 0) start = 0; + if (index > this->cellHeightCache.size()) index = this->cellHeightCache.size(); + + float res = 0; + for (size_t i = start; i < index && i < cellHeightCache.size(); i++) { + if (cellHeightCache[i] != -1) + res += cellHeightCache[i] + estimatedRowSpace; + else + res += estimatedRowHeight + estimatedRowSpace; + } + return res; +} + +void RecyclingGrid::forceRequestNextPage() { this->requestNextPage = false; } + +brls::View* RecyclingGrid::getNextCellFocus(brls::FocusDirection direction, brls::View* currentView) { + void* parentUserData = currentView->getParentUserData(); + + // Allow up and down when axis is ROW + if ((this->contentBox->getAxis() == brls::Axis::ROW && direction != brls::FocusDirection::LEFT && + direction != brls::FocusDirection::RIGHT)) { + int row_offset = spanCount; + if (direction == brls::FocusDirection::UP) row_offset = -spanCount; + View* row_currentFocus = nullptr; + size_t row_currentFocusIndex = *((size_t*)parentUserData) + row_offset; + + if (row_currentFocusIndex >= this->dataSource->getItemCount()) { + row_currentFocusIndex -= *((size_t*)parentUserData) % spanCount; + } + + while (!row_currentFocus && row_currentFocusIndex >= 0 && + row_currentFocusIndex < this->dataSource->getItemCount()) { + for (auto it : this->contentBox->getChildren()) { + if (*((size_t*)it->getParentUserData()) == row_currentFocusIndex) { + row_currentFocus = it->getDefaultFocus(); + break; + } + } + row_currentFocusIndex += row_offset; + } + if (row_currentFocus) { + // 按键(上或下)可以导航过去的情况 + itemsRecyclingLoop(); + + return row_currentFocus; + } + } + + if (this->contentBox->getAxis() == brls::Axis::ROW) { + int position = *((size_t*)parentUserData) % spanCount; + if ((direction == brls::FocusDirection::LEFT && position == 0) || + (direction == brls::FocusDirection::RIGHT && position == (spanCount - 1))) { + View* next = getParentNavigationDecision(this, nullptr, direction); + if (!next && hasParent()) next = getParent()->getNextFocus(direction, this); + return next; + } + } + + // Return nullptr immediately if focus direction mismatches the box axis (clang-format refuses to split it in multiple lines...) + if ((this->contentBox->getAxis() == brls::Axis::ROW && direction != brls::FocusDirection::LEFT && + direction != brls::FocusDirection::RIGHT) || + (this->contentBox->getAxis() == brls::Axis::COLUMN && direction != brls::FocusDirection::UP && + direction != brls::FocusDirection::DOWN)) { + View* next = getParentNavigationDecision(this, nullptr, direction); + if (!next && hasParent()) next = getParent()->getNextFocus(direction, this); + return next; + } + + // Traverse the children + size_t offset = 1; // which way we are going in the children list + + if ((this->contentBox->getAxis() == brls::Axis::ROW && direction == brls::FocusDirection::LEFT) || + (this->contentBox->getAxis() == brls::Axis::COLUMN && direction == brls::FocusDirection::UP)) { + offset = -1; + } + + size_t currentFocusIndex = *((size_t*)parentUserData) + offset; + View* currentFocus = nullptr; + + while (!currentFocus && currentFocusIndex >= 0 && currentFocusIndex < this->dataSource->getItemCount()) { + for (auto it : this->contentBox->getChildren()) { + if (*((size_t*)it->getParentUserData()) == currentFocusIndex) { + currentFocus = it->getDefaultFocus(); + break; + } + } + currentFocusIndex += offset; + } + + currentFocus = getParentNavigationDecision(this, currentFocus, direction); + if (!currentFocus && hasParent()) currentFocus = getParent()->getNextFocus(direction, this); + return currentFocus; +} + +void RecyclingGrid::onLayout() { + ScrollingFrame::onLayout(); + auto rect = this->getFrame(); + float width = rect.getWidth(); + // check NAN + if (width != width) return; + + if (!this->contentBox) return; + this->contentBox->setWidth(width); + if (checkWidth()) { + brls::Logger::debug("RecyclingGrid::onLayout reloadData()"); + layouted = true; + reloadData(); + } + this->refreshButton->setDetachedPosition(rect.getWidth() - 80, rect.getHeight() - 80); +} + +bool RecyclingGrid::checkWidth() { + float width = getWidth(); + if (oldWidth == -1) { + oldWidth = width; + } + if ((int)oldWidth != (int)width && width != 0) { + brls::Logger::debug("RecyclingGrid::checkWidth from {} to {}", oldWidth, width); + oldWidth = width; + return true; + } + oldWidth = width; + return false; +} + +void RecyclingGrid::queueReusableCell(RecyclingGridItem* cell) { + queueMap.at(cell->reuseIdentifier)->push_back(cell); + cell->cacheForReuse(); +} + +void RecyclingGrid::setPadding(float padding) { this->setPadding(padding, padding, padding, padding); } + +void RecyclingGrid::setPadding(float top, float right, float bottom, float left) { + paddingPercentage = false; + paddingTop = top; + paddingRight = right; + paddingBottom = bottom; + paddingLeft = left; + + this->reloadData(); +} + +void RecyclingGrid::setPaddingTop(float top) { + paddingTop = top; + this->reloadData(); +} + +void RecyclingGrid::setPaddingRight(float right) { + paddingPercentage = false; + paddingRight = right; + this->reloadData(); +} + +void RecyclingGrid::setPaddingBottom(float bottom) { + paddingBottom = bottom; + this->reloadData(); +} + +void RecyclingGrid::setPaddingLeft(float left) { + paddingPercentage = false; + paddingLeft = left; + this->reloadData(); +} + +void RecyclingGrid::setPaddingRightPercentage(float right) { + paddingPercentage = true; + paddingRight = right / 100.0f; +} + +void RecyclingGrid::setPaddingLeftPercentage(float left) { + paddingPercentage = true; + paddingLeft = left / 100.0f; +} + +float RecyclingGrid::getPaddingLeft() { + return paddingPercentage ? renderedFrame.getWidth() * paddingLeft : paddingLeft; +} + +float RecyclingGrid::getPaddingRight() { + return paddingPercentage ? renderedFrame.getWidth() * paddingRight : paddingRight; +} + +brls::View* RecyclingGrid::getDefaultFocus() { + if (this->dataSource && this->dataSource->getItemCount() > 0) return ScrollingFrame::getDefaultFocus(); + return nullptr; +} + +brls::View* RecyclingGrid::create() { return new RecyclingGrid(); } + +RecyclingGridItem* RecyclingGrid::dequeueReusableCell(std::string identifier) { + brls::Logger::verbose("RecyclingGrid::dequeueReusableCell: {}", identifier); + RecyclingGridItem* cell = nullptr; + auto it = queueMap.find(identifier); + + if (it != queueMap.end()) { + std::vector* vector = it->second; + if (!vector->empty()) { + cell = vector->back(); + vector->pop_back(); + } else { + cell = allocationMap.at(identifier)(); + cell->reuseIdentifier = identifier; + cell->detach(); + } + } + + cell->setHeight(brls::View::AUTO); + if (cell) cell->prepareForReuse(); + + return cell; +} + +/// RecyclingGridContentBox + +RecyclingGridContentBox::RecyclingGridContentBox(RecyclingGrid* recycler) : Box(brls::Axis::ROW), recycler(recycler) {} + +brls::View* RecyclingGridContentBox::getNextFocus(brls::FocusDirection direction, brls::View* currentView) { + return this->recycler->getNextCellFocus(direction, currentView); +} \ No newline at end of file diff --git a/source/views/settings_tab.cpp b/source/views/settings_tab.cpp index 6263b6c..f72a7eb 100644 --- a/source/views/settings_tab.cpp +++ b/source/views/settings_tab.cpp @@ -47,7 +47,7 @@ SettingsTab::SettingsTab() { settings_box->addView(debug_cell); auto wireframe_cell = new brls::BooleanCell(); - wireframe_cell->init("menu/settings_tab/debug"_i18n, brls::Application::isDebuggingViewEnabled(), [](bool value){ + wireframe_cell->init("menu/settings_tab/wireframe"_i18n, config.getWireframe(), [](bool value){ brls::sync([value](){ brls::Logger::info("{} wireframe", value ? "Enable" : "Disable"); });