| @@ -232,6 +232,16 @@ struct ModelBox : widget::OpaqueWidget { | |||
| }; | |||
| struct AuthorItem : ui::MenuItem { | |||
| void onAction(const widget::ActionEvent &e) override; | |||
| }; | |||
| struct TagItem : ui::MenuItem { | |||
| void onAction(const widget::ActionEvent &e) override; | |||
| }; | |||
| struct BrowserSearchField : ui::TextField { | |||
| void step() override { | |||
| // Steal focus when step is called | |||
| @@ -268,8 +278,10 @@ struct BrowserSearchField : ui::TextField { | |||
| struct BrowserSidebar : widget::Widget { | |||
| BrowserSearchField *searchField; | |||
| ui::List *pluginList; | |||
| ui::ScrollWidget *pluginScroll; | |||
| ui::Label *authorLabel; | |||
| ui::List *authorList; | |||
| ui::ScrollWidget *authorScroll; | |||
| ui::Label *tagLabel; | |||
| ui::List *tagList; | |||
| ui::ScrollWidget *tagScroll; | |||
| @@ -277,24 +289,34 @@ struct BrowserSidebar : widget::Widget { | |||
| searchField = new BrowserSearchField; | |||
| addChild(searchField); | |||
| authorLabel = new ui::Label; | |||
| authorLabel->color = nvgRGB(0x80, 0x80, 0x80); | |||
| authorLabel->text = "Authors"; | |||
| addChild(authorLabel); | |||
| // Plugin list | |||
| pluginScroll = new ui::ScrollWidget; | |||
| addChild(pluginScroll); | |||
| authorScroll = new ui::ScrollWidget; | |||
| addChild(authorScroll); | |||
| pluginList = new ui::List; | |||
| pluginScroll->container->addChild(pluginList); | |||
| authorList = new ui::List; | |||
| authorScroll->container->addChild(authorList); | |||
| std::set<std::string, string::CaseInsensitiveCompare> pluginNames; | |||
| std::set<std::string, string::CaseInsensitiveCompare> authorNames; | |||
| for (plugin::Plugin *plugin : plugin::plugins) { | |||
| pluginNames.insert(plugin->name); | |||
| authorNames.insert(plugin->author); | |||
| } | |||
| for (const std::string &pluginName : pluginNames) { | |||
| ui::MenuItem *item = new ui::MenuItem; | |||
| item->text = pluginName; | |||
| pluginList->addChild(item); | |||
| for (const std::string &authorName : authorNames) { | |||
| AuthorItem *item = new AuthorItem; | |||
| item->text = authorName; | |||
| authorList->addChild(item); | |||
| } | |||
| tagLabel = new ui::Label; | |||
| tagLabel->color = nvgRGB(0x80, 0x80, 0x80); | |||
| tagLabel->text = "Tags"; | |||
| addChild(tagLabel); | |||
| // Tag list | |||
| tagScroll = new ui::ScrollWidget; | |||
| addChild(tagScroll); | |||
| @@ -303,22 +325,32 @@ struct BrowserSidebar : widget::Widget { | |||
| tagScroll->container->addChild(tagList); | |||
| for (const std::string &tag : plugin::allowedTags) { | |||
| ui::MenuItem *item = new ui::MenuItem; | |||
| TagItem *item = new TagItem; | |||
| item->text = tag; | |||
| tagList->addChild(item); | |||
| } | |||
| } | |||
| void step() override { | |||
| float listHeight = (box.size.y - searchField->box.size.y) / 2 - authorLabel->box.size.y; | |||
| listHeight = std::floor(listHeight); | |||
| searchField->box.size.x = box.size.x; | |||
| pluginScroll->box.pos = searchField->box.getBottomLeft(); | |||
| pluginScroll->box.size.y = (box.size.y - searchField->box.size.y) / 2; | |||
| pluginScroll->box.size.x = box.size.x; | |||
| pluginList->box.size.x = pluginScroll->box.size.x; | |||
| tagScroll->box.pos = pluginScroll->box.getBottomLeft().floor(); | |||
| tagScroll->box.size.y = (box.size.y - searchField->box.size.y) / 2; | |||
| authorLabel->box.pos = searchField->box.getBottomLeft(); | |||
| authorLabel->box.size.x = box.size.x; | |||
| authorScroll->box.pos = authorLabel->box.getBottomLeft(); | |||
| authorScroll->box.size.y = listHeight; | |||
| authorScroll->box.size.x = box.size.x; | |||
| authorList->box.size.x = authorScroll->box.size.x; | |||
| tagLabel->box.pos = authorScroll->box.getBottomLeft(); | |||
| tagLabel->box.size.x = box.size.x; | |||
| tagScroll->box.pos = tagLabel->box.getBottomLeft(); | |||
| tagScroll->box.size.y = listHeight; | |||
| tagScroll->box.size.x = box.size.x; | |||
| tagList->box.size.x = tagScroll->box.size.x; | |||
| Widget::step(); | |||
| } | |||
| }; | |||
| @@ -330,6 +362,10 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| ui::MarginLayout *modelMargin; | |||
| ui::SequentialLayout *modelContainer; | |||
| std::string search; | |||
| std::string author; | |||
| std::string tag; | |||
| ModuleBrowser() { | |||
| sidebar = new BrowserSidebar; | |||
| sidebar->box.size.x = 200; | |||
| @@ -346,6 +382,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| modelContainer->spacing = math::Vec(10, 10); | |||
| modelMargin->addChild(modelContainer); | |||
| // Add ModelBoxes for each Model | |||
| for (plugin::Plugin *plugin : plugin::plugins) { | |||
| for (plugin::Model *model : plugin->models) { | |||
| ModelBox *moduleBox = new ModelBox; | |||
| @@ -354,11 +391,11 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| } | |||
| } | |||
| setSearch(""); | |||
| refreshModels(); | |||
| } | |||
| void step() override { | |||
| box = parent->box.zeroPos().grow(math::Vec(-50, -50)); | |||
| box = parent->box.zeroPos().grow(math::Vec(-70, -70)); | |||
| sidebar->box.size.y = box.size.y; | |||
| @@ -376,16 +413,16 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| Widget::draw(args); | |||
| } | |||
| void setSearch(const std::string &search) { | |||
| std::string searchTrimmed = string::trim(search); | |||
| void refreshModels() { | |||
| // Reset scroll position | |||
| modelScroll->offset = math::Vec(); | |||
| if (searchTrimmed.empty()) { | |||
| if (search.empty()) { | |||
| // Make all ModelBoxes visible | |||
| for (Widget *w : modelContainer->children) { | |||
| w->visible = true; | |||
| } | |||
| // If no search query, sort by plugin name and module name | |||
| // Sort by plugin name and then module name | |||
| modelContainer->children.sort([&](Widget *w1, Widget *w2) { | |||
| ModelBox *m1 = dynamic_cast<ModelBox*>(w1); | |||
| ModelBox *m2 = dynamic_cast<ModelBox*>(w2); | |||
| @@ -396,11 +433,11 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| } | |||
| else { | |||
| std::map<Widget*, float> scores; | |||
| // Compute scores and set visibility | |||
| // Compute scores and filter visibility | |||
| for (Widget *w : modelContainer->children) { | |||
| ModelBox *m = dynamic_cast<ModelBox*>(w); | |||
| assert(m); | |||
| float score = modelScore(m->model, searchTrimmed); | |||
| float score = modelScore(m->model, search); | |||
| scores[m] = score; | |||
| m->visible = (score > 0); | |||
| } | |||
| @@ -409,6 +446,37 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
| return scores[w1] > scores[w2]; | |||
| }); | |||
| } | |||
| // Filter authors | |||
| if (!author.empty()) { | |||
| for (Widget *w : modelContainer->children) { | |||
| if (!w->visible) | |||
| continue; | |||
| ModelBox *m = dynamic_cast<ModelBox*>(w); | |||
| assert(m); | |||
| if (m->model->plugin->author != author) | |||
| m->visible = false; | |||
| } | |||
| } | |||
| // Filter tags | |||
| if (!tag.empty()) { | |||
| for (Widget *w : modelContainer->children) { | |||
| if (!w->visible) | |||
| continue; | |||
| ModelBox *m = dynamic_cast<ModelBox*>(w); | |||
| assert(m); | |||
| bool found = false; | |||
| for (const std::string &tag : m->model->tags) { | |||
| if (tag == this->tag) { | |||
| found = true; | |||
| break; | |||
| } | |||
| } | |||
| if (!found) | |||
| m->visible = false; | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @@ -442,12 +510,33 @@ inline void ModelBox::onButton(const widget::ButtonEvent &e) { | |||
| } | |||
| } | |||
| inline void BrowserSearchField::onChange(const widget::ChangeEvent &e) { | |||
| inline void AuthorItem::onAction(const widget::ActionEvent &e) { | |||
| ModuleBrowser *browser = getAncestorOfType<ModuleBrowser>(); | |||
| browser->setSearch(text); | |||
| if (browser->author == text) | |||
| browser->author = ""; | |||
| else | |||
| browser->author = text; | |||
| browser->refreshModels(); | |||
| } | |||
| inline void TagItem::onAction(const widget::ActionEvent &e) { | |||
| ModuleBrowser *browser = getAncestorOfType<ModuleBrowser>(); | |||
| if (browser->tag == text) | |||
| browser->tag = ""; | |||
| else | |||
| browser->tag = text; | |||
| browser->refreshModels(); | |||
| } | |||
| inline void BrowserSearchField::onChange(const widget::ChangeEvent &e) { | |||
| ModuleBrowser *browser = getAncestorOfType<ModuleBrowser>(); | |||
| browser->search = string::trim(text); | |||
| browser->refreshModels(); | |||
| } | |||
| // Global functions | |||