#include "app/ModuleBrowser.hpp" #include "widget/OpaqueWidget.hpp" #include "widget/OverlayWidget.hpp" #include "widget/TransparentWidget.hpp" #include "widget/ZoomWidget.hpp" #include "ui/ScrollWidget.hpp" #include "ui/SequentialLayout.hpp" #include "ui/MarginLayout.hpp" #include "ui/Label.hpp" #include "ui/TextField.hpp" #include "ui/MenuOverlay.hpp" #include "ui/List.hpp" #include "ui/MenuItem.hpp" #include "ui/Button.hpp" #include "ui/RadioButton.hpp" #include "ui/ChoiceButton.hpp" #include "app/ModuleWidget.hpp" #include "app/Scene.hpp" #include "plugin.hpp" #include "app.hpp" #include "plugin/Model.hpp" #include "string.hpp" #include "history.hpp" #include "settings.hpp" #include #include namespace rack { namespace app { static float modelScore(plugin::Model *model, const std::string &search) { if (search.empty()) return true; std::string s; s += model->plugin->slug; s += " "; s += model->plugin->author; s += " "; s += model->plugin->name; s += " "; s += model->slug; s += " "; s += model->name; // for (const std::string &tag : model->tags) { // s += " "; // s += tag; // } float score = string::fuzzyScore(s, search); return score; } struct BrowserOverlay : widget::OverlayWidget { void step() override { box = parent->box.zeroPos(); // Only step if visible, since there are potentially thousands of descendants that don't need to be stepped. if (visible) OverlayWidget::step(); } void onButton(const widget::ButtonEvent &e) override { OverlayWidget::onButton(e); if (e.getConsumed() != this) return; if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { hide(); } } }; struct InfoBox : widget::Widget { void setModel(plugin::Model *model) { math::Vec pos; // Name label ui::Label *nameLabel = new ui::Label; // nameLabel->box.size.x = box.size.x; nameLabel->box.pos = pos; nameLabel->text = model->name; addChild(nameLabel); pos = nameLabel->box.getBottomLeft(); // Plugin label ui::Label *pluginLabel = new ui::Label; // pluginLabel->box.size.x = box.size.x; pluginLabel->box.pos = pos; pluginLabel->text = model->plugin->name; addChild(pluginLabel); pos = pluginLabel->box.getBottomLeft(); ui::Label *descriptionLabel = new ui::Label; descriptionLabel->box.size.x = box.size.x; descriptionLabel->box.pos = pos; descriptionLabel->text = model->description; addChild(descriptionLabel); pos = descriptionLabel->box.getBottomLeft(); // for (const std::string &tag : model->tags) { // ui::Button *tagButton = new ui::Button; // tagButton->box.size.x = box.size.x; // tagButton->box.pos = pos; // tagButton->text = tag; // addChild(tagButton); // pos = tagButton->box.getTopLeft(); // } } void draw(const DrawArgs &args) override { nvgBeginPath(args.vg); nvgRect(args.vg, 0, 0, box.size.x, box.size.y); nvgFillColor(args.vg, nvgRGBAf(1, 1, 1, 0.5)); nvgFill(args.vg); Widget::draw(args); } }; struct ModelFavoriteQuantity : ui::Quantity { plugin::Model *model; std::string getLabel() override {return "★";} void setValue(float value) override { if (value) { settings.favoriteModels.insert(model); } else { auto it = settings.favoriteModels.find(model); if (it != settings.favoriteModels.end()) settings.favoriteModels.erase(it); } } float getValue() override { auto it = settings.favoriteModels.find(model); return (it != settings.favoriteModels.end()); } }; static const float MODEL_BOX_ZOOM = 0.5f; struct ModelBox : widget::OpaqueWidget { plugin::Model *model; InfoBox *infoBox; widget::Widget *previewWidget; ui::RadioButton *favoriteButton; /** Lazily created */ widget::FramebufferWidget *previewFb = NULL; /** Number of frames since draw() has been called */ int visibleFrames = 0; ModelBox() { // Approximate size as 10HP before we know the actual size. // We need a nonzero size, otherwise the parent widget will consider it not in the draw bounds, so its preview will not be lazily created. box.size.x = 10 * RACK_GRID_WIDTH * MODEL_BOX_ZOOM; box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; box.size = box.size.ceil(); } void setModel(plugin::Model *model) { this->model = model; previewWidget = new widget::TransparentWidget; previewWidget->box.size.y = std::ceil(RACK_GRID_HEIGHT * MODEL_BOX_ZOOM); addChild(previewWidget); infoBox = new InfoBox; infoBox->box.size = math::Vec(100, 100); // infoBox->setModel(model); infoBox->hide(); addChild(infoBox); // Favorite button favoriteButton = new ui::RadioButton; ModelFavoriteQuantity *favoriteQuantity = new ModelFavoriteQuantity; favoriteQuantity->model = model; favoriteButton->quantity = favoriteQuantity; favoriteButton->box.pos.y = box.size.y; box.size.y += favoriteButton->box.size.y; addChild(favoriteButton); } void createPreview() { previewFb = new widget::FramebufferWidget; if (math::isNear(APP->window->pixelRatio, 1.0)) { // Small details draw poorly at low DPI, so oversample when drawing to the framebuffer previewFb->oversample = 2.0; } previewWidget->addChild(previewFb); widget::ZoomWidget *zoomWidget = new widget::ZoomWidget; zoomWidget->setZoom(MODEL_BOX_ZOOM); previewFb->addChild(zoomWidget); ModuleWidget *moduleWidget = model->createModuleWidgetNull(); zoomWidget->addChild(moduleWidget); zoomWidget->box.size.x = moduleWidget->box.size.x * MODEL_BOX_ZOOM; zoomWidget->box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; previewWidget->box.size.x = std::ceil(zoomWidget->box.size.x); infoBox->box.size = previewWidget->box.size; favoriteButton->box.size.x = previewWidget->box.size.x; box.size.x = previewWidget->box.size.x; } void deletePreview() { assert(previewFb); previewWidget->removeChild(previewFb); delete previewFb; previewFb = NULL; } void step() override { if (previewFb && ++visibleFrames >= 60) { deletePreview(); } OpaqueWidget::step(); } void draw(const DrawArgs &args) override { visibleFrames = 0; // Lazily create preview when drawn if (!previewFb) { createPreview(); } // Draw shadow nvgBeginPath(args.vg); float r = 10; // Blur radius float c = 10; // Corner radius nvgRect(args.vg, -r, -r, box.size.x + 2*r, box.size.y + 2*r); NVGcolor shadowColor = nvgRGBAf(0, 0, 0, 0.5); NVGcolor transparentColor = nvgRGBAf(0, 0, 0, 0); nvgFillPaint(args.vg, nvgBoxGradient(args.vg, 0, 0, box.size.x, box.size.y, c, r, shadowColor, transparentColor)); nvgFill(args.vg); OpaqueWidget::draw(args); } void onButton(const widget::ButtonEvent &e) override; void onEnter(const widget::EnterEvent &e) override { e.consume(this); infoBox->show(); } void onLeave(const widget::LeaveEvent &e) override { infoBox->hide(); } }; 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 APP->event->setSelected(this); TextField::step(); } void onSelectKey(const widget::SelectKeyEvent &e) override { if (e.action == GLFW_PRESS) { if (e.key == GLFW_KEY_ESCAPE) { BrowserOverlay *overlay = getAncestorOfType(); overlay->hide(); e.consume(this); } } if (!e.getConsumed()) ui::TextField::onSelectKey(e); } void onChange(const widget::ChangeEvent &e) override; void onHide(const widget::HideEvent &e) override { APP->event->setSelected(NULL); ui::TextField::onHide(e); } void onShow(const widget::ShowEvent &e) override { selectAll(); TextField::onShow(e); } }; struct ShowFavoritesQuantity : ui::Quantity { widget::Widget *widget; std::string getLabel() override { int favoritesLen = settings.favoriteModels.size(); return string::f("Only show favorites (%d)", favoritesLen); } void setValue(float value) override; float getValue() override; }; struct BrowserSidebar : widget::Widget { BrowserSearchField *searchField; ui::RadioButton *favoriteButton; ui::Label *authorLabel; ui::List *authorList; ui::ScrollWidget *authorScroll; ui::Label *tagLabel; ui::List *tagList; ui::ScrollWidget *tagScroll; BrowserSidebar() { searchField = new BrowserSearchField; addChild(searchField); favoriteButton = new ui::RadioButton; ShowFavoritesQuantity *favoriteQuantity = new ShowFavoritesQuantity; favoriteQuantity->widget = favoriteButton; favoriteButton->quantity = favoriteQuantity; addChild(favoriteButton); authorLabel = new ui::Label; // authorLabel->fontSize = 16; authorLabel->color = nvgRGB(0x80, 0x80, 0x80); authorLabel->text = "Authors"; addChild(authorLabel); // Plugin list authorScroll = new ui::ScrollWidget; addChild(authorScroll); authorList = new ui::List; authorScroll->container->addChild(authorList); std::set authorNames; for (plugin::Plugin *plugin : plugin::plugins) { authorNames.insert(plugin->author); } for (const std::string &authorName : authorNames) { AuthorItem *item = new AuthorItem; item->text = authorName; authorList->addChild(item); } tagLabel = new ui::Label; // tagLabel->fontSize = 16; tagLabel->color = nvgRGB(0x80, 0x80, 0x80); tagLabel->text = "Tags"; addChild(tagLabel); // Tag list tagScroll = new ui::ScrollWidget; addChild(tagScroll); tagList = new ui::List; tagScroll->container->addChild(tagList); for (const std::string &tag : plugin::allowedTags) { TagItem *item = new TagItem; item->text = tag; tagList->addChild(item); } } void step() override { searchField->box.size.x = box.size.x; favoriteButton->box.pos = searchField->box.getBottomLeft(); favoriteButton->box.size.x = box.size.x; float listHeight = (box.size.y - favoriteButton->box.getBottom()) / 2; listHeight = std::floor(listHeight); authorLabel->box.pos = favoriteButton->box.getBottomLeft(); authorLabel->box.size.x = box.size.x; authorScroll->box.pos = authorLabel->box.getBottomLeft(); authorScroll->box.size.y = listHeight - authorLabel->box.size.y; 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 - tagLabel->box.size.y; tagScroll->box.size.x = box.size.x; tagList->box.size.x = tagScroll->box.size.x; Widget::step(); } }; struct ModuleBrowser : widget::OpaqueWidget { BrowserSidebar *sidebar; ui::ScrollWidget *modelScroll; ui::Label *modelLabel; ui::MarginLayout *modelMargin; ui::SequentialLayout *modelContainer; std::string search; std::string author; std::string tag; bool favorites = false; ModuleBrowser() { sidebar = new BrowserSidebar; sidebar->box.size.x = 200; addChild(sidebar); modelScroll = new ui::ScrollWidget; addChild(modelScroll); modelLabel = new ui::Label; // modelLabel->fontSize = 16; modelLabel->box.pos = math::Vec(10, 10); modelScroll->container->addChild(modelLabel); modelMargin = new ui::MarginLayout; modelMargin->box.pos = modelLabel->box.getBottomLeft(); modelMargin->margin = math::Vec(10, 10); modelScroll->container->addChild(modelMargin); modelContainer = new ui::SequentialLayout; 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; moduleBox->setModel(model); modelContainer->addChild(moduleBox); } } refresh(); } void step() override { box = parent->box.zeroPos().grow(math::Vec(-70, -70)); sidebar->box.size.y = box.size.y; modelScroll->box.pos.x = sidebar->box.size.x; modelScroll->box.size.x = box.size.x - sidebar->box.size.x; modelScroll->box.size.y = box.size.y; modelMargin->box.size.x = modelScroll->box.size.x; modelMargin->box.size.y = modelContainer->getChildrenBoundingBox().size.y + 2 * modelMargin->margin.y; OpaqueWidget::step(); } void draw(const DrawArgs &args) override { bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0); Widget::draw(args); } void refresh() { // Reset scroll position modelScroll->offset = math::Vec(); // Show all or only favorites for (Widget *w : modelContainer->children) { if (favorites) { ModelBox *m = dynamic_cast(w); auto it = settings.favoriteModels.find(m->model); w->visible = (it != settings.favoriteModels.end()); } else { w->visible = true; } } if (search.empty()) { // Sort by plugin name and then module name modelContainer->children.sort([&](Widget *w1, Widget *w2) { ModelBox *m1 = dynamic_cast(w1); ModelBox *m2 = dynamic_cast(w2); if (m1->model->plugin->name != m2->model->plugin->name) return m1->model->plugin->name < m2->model->plugin->name; return m1->model->name < m2->model->name; }); } else { std::map scores; // Compute scores and filter visibility for (Widget *w : modelContainer->children) { ModelBox *m = dynamic_cast(w); assert(m); float score = 0.f; if (m->visible) { score = modelScore(m->model, search); m->visible = (score > 0); } scores[m] = score; } // Sort by score modelContainer->children.sort([&](Widget *w1, Widget *w2) { return scores[w1] > scores[w2]; }); } // Filter ModelBoxes by author if (!author.empty()) { for (Widget *w : modelContainer->children) { if (!w->visible) continue; ModelBox *m = dynamic_cast(w); assert(m); if (m->model->plugin->author != author) m->visible = false; } } // Filter ModelBoxes by tag if (!tag.empty()) { for (Widget *w : modelContainer->children) { if (!w->visible) continue; ModelBox *m = dynamic_cast(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; } } std::set enabledAuthors; std::set enabledTags; // Get list of enabled authors and tags for sidebar for (Widget *w : modelContainer->children) { ModelBox *m = dynamic_cast(w); assert(m); if (!m->visible) continue; enabledAuthors.insert(m->model->plugin->author); for (const std::string &tag : m->model->tags) { enabledTags.insert(tag); } } // Count models int modelsLen = 0; for (Widget *w : modelContainer->children) { if (w->visible) modelsLen++; } modelLabel->text = string::f("Modules (%d)", modelsLen); // Enable author and tag items that are available in visible ModelBoxes int authorsLen = 0; for (Widget *w : sidebar->authorList->children) { AuthorItem *item = dynamic_cast(w); assert(item); auto it = enabledAuthors.find(item->text); item->disabled = (it == enabledAuthors.end()); if (!item->disabled) authorsLen++; } sidebar->authorLabel->text = string::f("Authors (%d)", authorsLen); int tagsLen = 0; for (Widget *w : sidebar->tagList->children) { TagItem *item = dynamic_cast(w); assert(item); auto it = enabledTags.find(item->text); item->disabled = (it == enabledTags.end()); if (!item->disabled) tagsLen++; } sidebar->tagLabel->text = string::f("Tags (%d)", tagsLen); } }; // Implementations to resolve dependencies inline void ModelBox::onButton(const widget::ButtonEvent &e) { OpaqueWidget::onButton(e); if (e.getConsumed() != this) return; if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { // Create module ModuleWidget *moduleWidget = model->createModuleWidget(); assert(moduleWidget); APP->scene->rack->addModuleAtMouse(moduleWidget); // Pretend the moduleWidget was clicked so it can be dragged in the RackWidget e.consume(moduleWidget); // Hide Module Browser BrowserOverlay *overlay = getAncestorOfType(); overlay->hide(); // Push ModuleAdd history action history::ModuleAdd *h = new history::ModuleAdd; h->name = "create module"; h->setModule(moduleWidget); APP->history->push(h); } } inline void AuthorItem::onAction(const widget::ActionEvent &e) { ModuleBrowser *browser = getAncestorOfType(); if (browser->author == text) browser->author = ""; else browser->author = text; browser->refresh(); } inline void TagItem::onAction(const widget::ActionEvent &e) { ModuleBrowser *browser = getAncestorOfType(); if (browser->tag == text) browser->tag = ""; else browser->tag = text; browser->refresh(); } inline void BrowserSearchField::onChange(const widget::ChangeEvent &e) { ModuleBrowser *browser = getAncestorOfType(); browser->search = string::trim(text); browser->refresh(); } inline void ShowFavoritesQuantity::setValue(float value) { ModuleBrowser *browser = widget->getAncestorOfType(); browser->favorites = (bool) value; browser->refresh(); } inline float ShowFavoritesQuantity::getValue() { ModuleBrowser *browser = widget->getAncestorOfType(); return browser->favorites; } // Global functions widget::Widget *moduleBrowserCreate() { BrowserOverlay *overlay = new BrowserOverlay; ModuleBrowser *browser = new ModuleBrowser; overlay->addChild(browser); return overlay; } } // namespace app } // namespace rack