#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rack { namespace app { namespace browser { static fuzzysearch::Database modelDb; static bool modelDbInitialized = false; static void fuzzySearchInit() { if (modelDbInitialized) return; modelDb.setWeights({1.f, 1.f, 0.1f, 1.f, 0.5f, 0.5f}); modelDb.setThreshold(0.25f); // Iterate plugins for (plugin::Plugin* plugin : plugin::plugins) { // Iterate model in plugin for (plugin::Model* model : plugin->models) { // Get search fields for model std::string tagStr; for (int tagId : model->tagIds) { // Add all aliases of a tag for (const std::string& tagAlias : tag::tagAliases[tagId]) { tagStr += tagAlias; tagStr += ", "; } } std::vector fields = { model->plugin->brand, model->plugin->name, model->plugin->description, model->name, model->description, tagStr, }; // DEBUG("%s; %s; %s; %s; %s; %s", fields[0].c_str(), fields[1].c_str(), fields[2].c_str(), fields[3].c_str(), fields[4].c_str(), fields[5].c_str()); modelDb.addEntry(model, fields); } } modelDbInitialized = true; } static ModuleWidget* chooseModel(plugin::Model* model) { // Record usage settings::ModuleInfo& mi = settings::moduleInfos[model->plugin->slug][model->slug]; mi.added++; mi.lastAdded = system::getUnixTime(); // Create Module and ModuleWidget engine::Module* module = model->createModule(); APP->engine->addModule(module); ModuleWidget* moduleWidget = model->createModuleWidget(module); APP->scene->rack->addModuleAtMouse(moduleWidget); // Load template preset moduleWidget->loadTemplate(); // history::ModuleAdd history::ModuleAdd* h = new history::ModuleAdd; h->name = "create module"; // This serializes the module so redoing returns to the current state. h->setModule(moduleWidget); APP->history->push(h); // Hide Module Browser APP->scene->browser->hide(); return moduleWidget; } // Widgets struct Browser; struct BrowserOverlay : ui::MenuOverlay { void step() override { // Only step if visible, since there are potentially thousands of descendants that don't need to be stepped. if (isVisible()) MenuOverlay::step(); } void onAction(const ActionEvent& e) override { // Hide instead of requestDelete() hide(); } }; struct ModelBox : widget::OpaqueWidget { plugin::Model* model; ui::Tooltip* tooltip = NULL; // Lazily created widgets widget::Widget* previewWidget = NULL; widget::ZoomWidget* zoomWidget = NULL; widget::FramebufferWidget* fb = NULL; ModuleWidget* moduleWidget = NULL; ModelBox() { updateZoom(); } void setModel(plugin::Model* model) { this->model = model; } void updateZoom() { float zoom = std::pow(2.f, settings::browserZoom); if (previewWidget) { fb->setDirty(); zoomWidget->setZoom(zoom); box.size.x = moduleWidget->box.size.x * zoom; } else { // Approximate size as 12HP before we know the actual size. // We need a nonzero size, otherwise too many ModelBoxes will lazily render in the same frame. box.size.x = 12 * RACK_GRID_WIDTH * zoom; } box.size.y = RACK_GRID_HEIGHT * zoom; box.size = box.size.ceil(); } void createPreview() { if (previewWidget) return; previewWidget = new widget::TransparentWidget; addChild(previewWidget); zoomWidget = new widget::ZoomWidget; previewWidget->addChild(zoomWidget); fb = 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 fb->oversample = 2.0; } zoomWidget->addChild(fb); moduleWidget = model->createModuleWidget(NULL); fb->addChild(moduleWidget); updateZoom(); } void draw(const DrawArgs& args) override { // Lazily create preview when drawn createPreview(); const settings::ModuleInfo* mi = settings::getModuleInfo(model->plugin->slug, model->slug); // Draw shadow nvgBeginPath(args.vg); float r = 10; // Blur radius float c = 5; // 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); if (mi && mi->favorite) shadowColor = nvgRGBAf(2, 2, 2, 1.0); nvgFillPaint(args.vg, nvgBoxGradient(args.vg, 0, 0, box.size.x, box.size.y, c, r, shadowColor, color::BLACK_TRANSPARENT)); nvgFill(args.vg); // To avoid blinding the user when rack brightness is low, draw framebuffer with the same brightness. float b = settings::rackBrightness; nvgGlobalTint(args.vg, nvgRGBAf(b, b, b, 1)); OpaqueWidget::draw(args); } void step() override { OpaqueWidget::step(); } void setTooltip(ui::Tooltip* tooltip) { if (this->tooltip) { this->tooltip->requestDelete(); this->tooltip = NULL; } if (tooltip) { APP->scene->addChild(tooltip); this->tooltip = tooltip; } } void onButton(const ButtonEvent& e) override { if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0) { ModuleWidget* mw = chooseModel(model); // Pretend the moduleWidget was clicked so it can be dragged in the RackWidget e.consume(mw); // Set the drag position at the center of the module mw->dragOffset() = mw->box.size.div(2); // Disable dragging temporarily until the mouse has moved a bit. mw->dragEnabled() = false; } // Toggle favorite if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { model->setFavorite(!model->isFavorite()); e.consume(this); } // Open context menu on right-click if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) { createContextMenu(); e.consume(this); } } void onHoverKey(const HoverKeyEvent& e) override { if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) { if (e.key == GLFW_KEY_F1 && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { system::openBrowser(model->getManualUrl()); e.consume(this); } } if (e.isConsumed()) return; OpaqueWidget::onHoverKey(e); } ui::Tooltip* createTooltip() { std::string text; text += model->name; text += "\n"; text += model->plugin->brand; // Description if (model->description != "") { text += "\n" + model->description; } // Tags text += "\n\nTags: "; std::vector tags; for (int tagId : model->tagIds) { tags.push_back(tag::getTag(tagId)); } text += string::join(tags, ", "); ui::Tooltip* tooltip = new ui::Tooltip; tooltip->text = text; return tooltip; } void onEnter(const EnterEvent& e) override { setTooltip(createTooltip()); } void onLeave(const LeaveEvent& e) override { setTooltip(NULL); } void onHide(const HideEvent& e) override { // Hide tooltip setTooltip(NULL); OpaqueWidget::onHide(e); } void createContextMenu() { ui::Menu* menu = createMenu(); menu->addChild(createMenuLabel(model->name)); menu->addChild(createMenuLabel(model->plugin->brand)); model->appendContextMenu(menu); } }; struct BrowserSearchField : ui::TextField { Browser* browser; void step() override { // Steal focus when step is called APP->event->setSelectedWidget(this); TextField::step(); } void onSelectKey(const SelectKeyEvent& e) override; void onChange(const ChangeEvent& e) override; void onAction(const ActionEvent& e) override; void onHide(const HideEvent& e) override { APP->event->setSelectedWidget(NULL); ui::TextField::onHide(e); } void onShow(const ShowEvent& e) override { selectAll(); TextField::onShow(e); } }; struct FavoriteQuantity : Quantity { Browser* browser; void setValue(float value) override; float getValue() override; }; struct ClearButton : ui::Button { Browser* browser; void onAction(const ActionEvent& e) override; }; struct BrandItem : ui::MenuItem { Browser* browser; std::string brand; void onAction(const ActionEvent& e) override; void step() override; }; struct BrandButton : ui::ChoiceButton { Browser* browser; void onAction(const ActionEvent& e) override; void step() override; }; struct TagItem : ui::MenuItem { Browser* browser; int tagId; void onAction(const ActionEvent& e) override; void step() override; }; struct TagButton : ui::ChoiceButton { Browser* browser; void onAction(const ActionEvent& e) override; void step() override; }; static const std::string sortNames[] = { "Last updated", "Last used", "Most used", "Brand", "Module name", "Random", }; struct SortItem : ui::MenuItem { Browser* browser; settings::BrowserSort sort; void onAction(const ActionEvent& e) override; void step() override { rightText = CHECKMARK(settings::browserSort == sort); MenuItem::step(); } }; struct SortButton : ui::ChoiceButton { Browser* browser; void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->box.size.x = box.size.x; for (int sortId = 0; sortId <= settings::BROWSER_SORT_RANDOM; sortId++) { SortItem* sortItem = new SortItem; sortItem->text = sortNames[sortId]; sortItem->sort = (settings::BrowserSort) sortId; sortItem->browser = browser; menu->addChild(sortItem); } } void step() override { text = "Sort: "; text += sortNames[settings::browserSort]; ChoiceButton::step(); } }; struct ZoomItem : ui::MenuItem { Browser* browser; float zoom; void onAction(const ActionEvent& e) override; void step() override { rightText = CHECKMARK(settings::browserZoom == zoom); MenuItem::step(); } }; struct ZoomButton : ui::ChoiceButton { Browser* browser; void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->box.size.x = box.size.x; for (float zoom = 1.f; zoom >= -2.f; zoom -= 0.5f) { ZoomItem* sortItem = new ZoomItem; sortItem->text = string::f("%.0f%%", std::pow(2.f, zoom) * 100.f); sortItem->zoom = zoom; sortItem->browser = browser; menu->addChild(sortItem); } } void step() override { text = "Zoom: "; text += string::f("%.0f%%", std::pow(2.f, settings::browserZoom) * 100.f); ChoiceButton::step(); } }; struct UrlButton : ui::Button { std::string url; void onAction(const ActionEvent& e) override { system::openBrowser(url); } }; struct Browser : widget::OpaqueWidget { ui::SequentialLayout* headerLayout; BrowserSearchField* searchField; BrandButton* brandButton; TagButton* tagButton; FavoriteQuantity* favoriteQuantity; ui::OptionButton* favoriteButton; ClearButton* clearButton; ui::Label* countLabel; ui::ScrollWidget* modelScroll; widget::Widget* modelMargin; ui::SequentialLayout* modelContainer; std::string search; std::string brand; std::set tagIds = {}; bool favorite = false; // Caches and temporary state std::map prefilteredModelScores; std::map modelOrders; Browser() { const float margin = 10; // Header headerLayout = new ui::SequentialLayout; headerLayout->box.pos = math::Vec(0, 0); headerLayout->box.size.y = 0; headerLayout->margin = math::Vec(margin, margin); headerLayout->spacing = math::Vec(margin, margin); addChild(headerLayout); searchField = new BrowserSearchField; searchField->box.size.x = 150; searchField->placeholder = "Search modules"; searchField->browser = this; headerLayout->addChild(searchField); brandButton = new BrandButton; brandButton->box.size.x = 150; brandButton->browser = this; headerLayout->addChild(brandButton); tagButton = new TagButton; tagButton->box.size.x = 150; tagButton->browser = this; headerLayout->addChild(tagButton); favoriteQuantity = new FavoriteQuantity; favoriteQuantity->browser = this; favoriteButton = new ui::OptionButton; favoriteButton->quantity = favoriteQuantity; favoriteButton->text = "Favorites"; favoriteButton->box.size.x = 70; headerLayout->addChild(favoriteButton); clearButton = new ClearButton; clearButton->box.size.x = 100; clearButton->text = "Reset filters"; clearButton->browser = this; headerLayout->addChild(clearButton); countLabel = new ui::Label; countLabel->box.size.x = 100; countLabel->color.a = 0.5; headerLayout->addChild(countLabel); SortButton* sortButton = new SortButton; sortButton->box.size.x = 150; sortButton->browser = this; headerLayout->addChild(sortButton); ZoomButton* zoomButton = new ZoomButton; zoomButton->box.size.x = 100; zoomButton->browser = this; headerLayout->addChild(zoomButton); UrlButton* libraryButton = new UrlButton; libraryButton->box.size.x = 150; libraryButton->text = "Browse VCV Library"; libraryButton->url = "https://library.vcvrack.com/"; headerLayout->addChild(libraryButton); // Model container modelScroll = new ui::ScrollWidget; modelScroll->box.pos.y = BND_WIDGET_HEIGHT; addChild(modelScroll); modelMargin = new widget::Widget; modelScroll->container->addChild(modelMargin); modelContainer = new ui::SequentialLayout; modelContainer->margin = math::Vec(margin, 0); modelContainer->spacing = math::Vec(margin, margin); modelMargin->addChild(modelContainer); resetModelBoxes(); clear(); } ~Browser() { delete favoriteQuantity; } void resetModelBoxes() { modelContainer->clearChildren(); modelOrders.clear(); // Iterate plugins // for (int i = 0; i < 100; i++) for (plugin::Plugin* plugin : plugin::plugins) { // Iterate models in plugin int modelIndex = 0; for (plugin::Model* model : plugin->models) { // Create ModelBox ModelBox* modelBox = new ModelBox; modelBox->setModel(model); modelContainer->addChild(modelBox); modelOrders[model] = modelIndex; modelIndex++; } } } void updateZoom() { modelScroll->offset = math::Vec(); for (Widget* w : modelContainer->children) { ModelBox* mb = reinterpret_cast(w); assert(mb); mb->updateZoom(); } } void step() override { box = parent->box.zeroPos().grow(math::Vec(-40, -40)); headerLayout->box.size.x = box.size.x; const float margin = 10; modelScroll->box.pos = headerLayout->box.getBottomLeft(); modelScroll->box.size = box.size.minus(modelScroll->box.pos); modelMargin->box.size.x = modelScroll->box.size.x; modelMargin->box.size.y = modelContainer->box.size.y + margin; modelContainer->box.size.x = modelMargin->box.size.x - margin; 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); } bool isModelVisible(plugin::Model* model, const std::string& brand, std::set tagIds, bool favorite) { // Filter hidden if (model->hidden) return false; settings::ModuleInfo* mi = settings::getModuleInfo(model->plugin->slug, model->slug); if (mi && !mi->enabled) return false; // Filter favorites if (favorite) { if (!mi || !mi->favorite) return false; } // Filter brand if (!brand.empty()) { if (model->plugin->brand != brand) return false; } // Filter tag for (int tagId : tagIds) { auto it = std::find(model->tagIds.begin(), model->tagIds.end(), tagId); if (it == model->tagIds.end()) return false; } return true; }; // Determines if there is at least 1 visible Model with a given brand and tag bool hasVisibleModel(const std::string& brand, std::set tagIds, bool favorite) { for (const auto& pair : prefilteredModelScores) { plugin::Model* model = pair.first; if (isModelVisible(model, brand, tagIds, favorite)) return true; } return false; }; template void sortModels(F f) { modelContainer->children.sort([&](Widget* w1, Widget* w2) { ModelBox* m1 = reinterpret_cast(w1); ModelBox* m2 = reinterpret_cast(w2); return f(m1) < f(m2); }); } void refresh() { // Reset scroll position modelScroll->offset = math::Vec(); prefilteredModelScores.clear(); // Filter ModelBoxes by brand and tag for (Widget* w : modelContainer->children) { ModelBox* m = reinterpret_cast(w); m->setVisible(isModelVisible(m->model, brand, tagIds, favorite)); } // Filter and sort by search results if (search.empty()) { // Add all models to prefilteredModelScores with scores of 1 for (Widget* w : modelContainer->children) { ModelBox* m = reinterpret_cast(w); prefilteredModelScores[m->model] = 1.f; } // Sort ModelBoxes if (settings::browserSort == settings::BROWSER_SORT_UPDATED) { sortModels([this](ModelBox* m) { plugin::Plugin* p = m->model->plugin; int modelOrder = get(modelOrders, m->model, 0); return std::make_tuple(-p->modifiedTimestamp, p->brand, p->name, modelOrder); }); } else if (settings::browserSort == settings::BROWSER_SORT_LAST_USED) { sortModels([this](ModelBox* m) { plugin::Plugin* p = m->model->plugin; const settings::ModuleInfo* mi = settings::getModuleInfo(p->slug, m->model->slug); double lastAdded = mi ? mi->lastAdded : -INFINITY; int modelOrder = get(modelOrders, m->model, 0); return std::make_tuple(-lastAdded, -p->modifiedTimestamp, p->brand, p->name, modelOrder); }); } else if (settings::browserSort == settings::BROWSER_SORT_MOST_USED) { sortModels([this](ModelBox* m) { plugin::Plugin* p = m->model->plugin; const settings::ModuleInfo* mi = settings::getModuleInfo(p->slug, m->model->slug); int added = mi ? mi->added : 0; double lastAdded = mi ? mi->lastAdded : -INFINITY; int modelOrder = get(modelOrders, m->model, 0); return std::make_tuple(-added, -lastAdded, -p->modifiedTimestamp, p->brand, p->name, modelOrder); }); } else if (settings::browserSort == settings::BROWSER_SORT_BRAND) { sortModels([this](ModelBox* m) { plugin::Plugin* p = m->model->plugin; int modelOrder = get(modelOrders, m->model, 0); return std::make_tuple(p->brand, p->name, modelOrder); }); } else if (settings::browserSort == settings::BROWSER_SORT_NAME) { sortModels([](ModelBox* m) { plugin::Plugin* p = m->model->plugin; return std::make_tuple(m->model->name, p->brand); }); } else if (settings::browserSort == settings::BROWSER_SORT_RANDOM) { std::map randomOrder; for (Widget* w : modelContainer->children) { ModelBox* m = reinterpret_cast(w); randomOrder[m] = random::u64(); } sortModels([&](ModelBox* m) { return get(randomOrder, m, 0); }); } } else { // Lazily initialize search database fuzzySearchInit(); // Score results against search query auto results = modelDb.search(search); // DEBUG("============="); for (auto& result : results) { prefilteredModelScores[result.key] = result.score; // DEBUG("%s %s\t\t%f", result.key->plugin->slug.c_str(), result.key->slug.c_str(), result.score); } // Sort by score sortModels([&](ModelBox* m) { return -get(prefilteredModelScores, m->model, 0.f); }); // Filter by whether the score is above the threshold for (Widget* w : modelContainer->children) { ModelBox* m = reinterpret_cast(w); assert(m); if (m->isVisible()) { if (prefilteredModelScores.find(m->model) == prefilteredModelScores.end()) m->hide(); } } } // Count visible modules int count = 0; for (Widget* w : modelContainer->children) { if (w->isVisible()) count++; } countLabel->text = string::f("%d %s", count, (count == 1) ? "module" : "modules"); } void clear() { search = ""; searchField->setText(""); brand = ""; tagIds = {}; favorite = false; refresh(); } void onShow(const ShowEvent& e) override { refresh(); OpaqueWidget::onShow(e); } void onButton(const ButtonEvent& e) override { Widget::onButton(e); e.stopPropagating(); // Consume all mouse buttons if (!e.isConsumed()) e.consume(this); } }; // Implementations to resolve dependencies inline void FavoriteQuantity::setValue(float value) { browser->favorite = value; browser->refresh(); } inline float FavoriteQuantity::getValue() { return browser->favorite; } inline void ClearButton::onAction(const ActionEvent& e) { browser->clear(); } inline void BrowserSearchField::onSelectKey(const SelectKeyEvent& e) { if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) { // Backspace when the field is empty to clear filters. if (e.key == GLFW_KEY_BACKSPACE) { if (text == "") { browser->clear(); e.consume(this); } } } if (!e.getTarget()) ui::TextField::onSelectKey(e); } inline void BrowserSearchField::onChange(const ChangeEvent& e) { browser->search = string::trim(text); browser->refresh(); } inline void BrowserSearchField::onAction(const ActionEvent& e) { // Get first ModelBox ModelBox* mb = NULL; for (Widget* w : browser->modelContainer->children) { if (w->isVisible()) { mb = reinterpret_cast(w); break; } } if (mb) { chooseModel(mb->model); } } inline void BrandItem::onAction(const ActionEvent& e) { if (browser->brand == brand) browser->brand = ""; else browser->brand = brand; browser->refresh(); } inline void BrandItem::step() { rightText = CHECKMARK(browser->brand == brand); MenuItem::step(); } inline void BrandButton::onAction(const ActionEvent& e) { ui::Menu* menu = createMenu(); menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->box.size.x = box.size.x; BrandItem* noneItem = new BrandItem; noneItem->text = "All brands"; noneItem->brand = ""; noneItem->browser = browser; menu->addChild(noneItem); menu->addChild(new ui::MenuSeparator); // Collect brands from all plugins std::set brands; for (plugin::Plugin* plugin : plugin::plugins) { brands.insert(plugin->brand); } for (const std::string& brand : brands) { BrandItem* brandItem = new BrandItem; brandItem->text = brand; brandItem->brand = brand; brandItem->browser = browser; brandItem->disabled = !browser->hasVisibleModel(brand, browser->tagIds, browser->favorite); menu->addChild(brandItem); } } inline void BrandButton::step() { text = "Brand"; if (!browser->brand.empty()) { text += ": "; text += browser->brand; } text = string::ellipsize(text, 21); ChoiceButton::step(); } inline void TagItem::onAction(const ActionEvent& e) { auto it = browser->tagIds.find(tagId); bool isSelected = (it != browser->tagIds.end()); if (tagId >= 0) { // Specific tag if (!e.isConsumed()) { // Multi select if (isSelected) browser->tagIds.erase(tagId); else browser->tagIds.insert(tagId); e.unconsume(); } else { // Single select if (isSelected) browser->tagIds = {}; else { browser->tagIds = {tagId}; } } } else { // All tags browser->tagIds = {}; } browser->refresh(); } inline void TagItem::step() { // TODO Disable tags with no modules if (tagId >= 0) { auto it = browser->tagIds.find(tagId); bool isSelected = (it != browser->tagIds.end()); rightText = CHECKMARK(isSelected); } else { rightText = CHECKMARK(browser->tagIds.empty()); } MenuItem::step(); } inline void TagButton::onAction(const ActionEvent& e) { ui::Menu* menu = createMenu(); menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->box.size.x = box.size.x; TagItem* noneItem = new TagItem; noneItem->text = "All tags"; noneItem->tagId = -1; noneItem->browser = browser; menu->addChild(noneItem); menu->addChild(createMenuLabel(RACK_MOD_CTRL_NAME "+click to select multiple")); menu->addChild(new ui::MenuSeparator); for (int tagId = 0; tagId < (int) tag::tagAliases.size(); tagId++) { TagItem* tagItem = new TagItem; tagItem->text = tag::getTag(tagId); tagItem->tagId = tagId; tagItem->browser = browser; tagItem->disabled = !browser->hasVisibleModel(browser->brand, {tagId}, browser->favorite); menu->addChild(tagItem); } } inline void TagButton::step() { text = "Tags"; if (!browser->tagIds.empty()) { text += ": "; bool firstTag = true; for (int tagId : browser->tagIds) { if (!firstTag) text += ", "; text += tag::getTag(tagId); firstTag = false; } } text = string::ellipsize(text, 21); ChoiceButton::step(); } inline void SortItem::onAction(const ActionEvent& e) { settings::browserSort = sort; browser->refresh(); } inline void ZoomItem::onAction(const ActionEvent& e) { if (zoom != settings::browserZoom) { settings::browserZoom = zoom; browser->updateZoom(); } } } // namespace browser widget::Widget* browserCreate() { browser::BrowserOverlay* overlay = new browser::BrowserOverlay; overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33); browser::Browser* browser = new browser::Browser; overlay->addChild(browser); return overlay; } } // namespace app } // namespace rack