| @@ -8,6 +8,12 @@ namespace rack { | |||||
| struct Label : Widget { | struct Label : Widget { | ||||
| std::string text; | std::string text; | ||||
| enum Align { | |||||
| LEFT_ALIGN, | |||||
| CENTER_ALIGN, | |||||
| RIGHT_ALIGN | |||||
| }; | |||||
| Align align = LEFT_ALIGN; | |||||
| Label() { | Label() { | ||||
| box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
| } | } | ||||
| @@ -28,8 +28,8 @@ inline int max(int a, int b) { | |||||
| /** Limits a value between a minimum and maximum | /** Limits a value between a minimum and maximum | ||||
| Assumes min <= max | Assumes min <= max | ||||
| */ | */ | ||||
| inline int clamp(int x, int minimum, int maximum) { | |||||
| return min(max(x, minimum), maximum); | |||||
| inline int clamp(int x, int min, int max) { | |||||
| return rack::min(rack::max(x, min), max); | |||||
| } | } | ||||
| /** Euclidean modulus, always returns 0 <= mod < base for positive base. | /** Euclidean modulus, always returns 0 <= mod < base for positive base. | ||||
| @@ -74,11 +74,11 @@ inline bool isNear(float a, float b, float epsilon = 1.0e-6f) { | |||||
| /** Limits a value between a minimum and maximum | /** Limits a value between a minimum and maximum | ||||
| Assumes min <= max | Assumes min <= max | ||||
| */ | */ | ||||
| inline float clamp(float x, float minimum, float maximum) { | |||||
| return fminf(fmaxf(x, minimum), maximum); | |||||
| inline float clamp(float x, float min, float max) { | |||||
| return fminf(fmaxf(x, min), max); | |||||
| } | } | ||||
| /** Limits a value between a minimum and maximum | |||||
| /** Limits a value between a min and max | |||||
| If min > max, switches the two values | If min > max, switches the two values | ||||
| */ | */ | ||||
| inline float clamp2(float x, float min, float max) { | inline float clamp2(float x, float min, float max) { | ||||
| @@ -180,6 +180,7 @@ struct Vec { | |||||
| return isfinite(x) && isfinite(y); | return isfinite(x) && isfinite(y); | ||||
| } | } | ||||
| Vec clamp(Rect bound); | Vec clamp(Rect bound); | ||||
| Vec clamp2(Rect bound); | |||||
| }; | }; | ||||
| @@ -260,8 +261,14 @@ struct Rect { | |||||
| inline Vec Vec::clamp(Rect bound) { | inline Vec Vec::clamp(Rect bound) { | ||||
| return Vec( | return Vec( | ||||
| clamp2(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
| clamp2(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
| rack::clamp(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
| rack::clamp(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
| } | |||||
| inline Vec Vec::clamp2(Rect bound) { | |||||
| return Vec( | |||||
| rack::clamp2(x, bound.pos.x, bound.pos.x + bound.size.x), | |||||
| rack::clamp2(y, bound.pos.y, bound.pos.y + bound.size.y)); | |||||
| } | } | ||||
| @@ -0,0 +1,389 @@ | |||||
| #include "app.hpp" | |||||
| #include "plugin.hpp" | |||||
| #include "window.hpp" | |||||
| #include <set> | |||||
| #define BND_LABEL_FONT_SIZE 13 | |||||
| namespace rack { | |||||
| static std::set<Model*> sFavoriteModels; | |||||
| bool isMatch(std::string s, std::string search) { | |||||
| s = lowercase(s); | |||||
| search = lowercase(search); | |||||
| return (s.find(search) != std::string::npos); | |||||
| } | |||||
| static bool isModelMatch(Model *model, std::string search) { | |||||
| if (search.empty()) | |||||
| return true; | |||||
| std::string s; | |||||
| s += model->plugin->slug; | |||||
| s += " "; | |||||
| s += model->manufacturer; | |||||
| s += " "; | |||||
| s += model->name; | |||||
| s += " "; | |||||
| s += model->slug; | |||||
| for (ModelTag tag : model->tags) { | |||||
| s += " "; | |||||
| s += gTagNames[tag]; | |||||
| } | |||||
| return isMatch(s, search); | |||||
| } | |||||
| struct FavoriteRadioButton : RadioButton { | |||||
| Model *model = NULL; | |||||
| void onAction(EventAction &e) override; | |||||
| }; | |||||
| struct BrowserListItem : OpaqueWidget { | |||||
| bool selected = false; | |||||
| BrowserListItem() { | |||||
| box.size.y = 3 * BND_WIDGET_HEIGHT; | |||||
| } | |||||
| void draw(NVGcontext *vg) override { | |||||
| BNDwidgetState state = selected ? BND_HOVER : BND_DEFAULT; | |||||
| bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, ""); | |||||
| Widget::draw(vg); | |||||
| } | |||||
| void onDragDrop(EventDragDrop &e) override { | |||||
| if (e.origin != this) | |||||
| return; | |||||
| doAction(); | |||||
| } | |||||
| void doAction() { | |||||
| EventAction eAction; | |||||
| eAction.consumed = true; | |||||
| onAction(eAction); | |||||
| if (eAction.consumed) { | |||||
| // deletes `this` | |||||
| gScene->setOverlay(NULL); | |||||
| } | |||||
| } | |||||
| void onMouseEnter(EventMouseEnter &e) override; | |||||
| }; | |||||
| struct ModelItem : BrowserListItem { | |||||
| Model *model; | |||||
| FavoriteRadioButton *favoriteButton; | |||||
| Label *nameLabel; | |||||
| Label *manufacturerLabel; | |||||
| Label *tagsLabel; | |||||
| ModelItem() { | |||||
| favoriteButton = new FavoriteRadioButton(); | |||||
| favoriteButton->box.pos = Vec(7, BND_WIDGET_HEIGHT); | |||||
| favoriteButton->box.size.x = 20; | |||||
| favoriteButton->label = "★"; | |||||
| addChild(favoriteButton); | |||||
| nameLabel = Widget::create<Label>(Vec(0, 0)); | |||||
| addChild(nameLabel); | |||||
| manufacturerLabel = Widget::create<Label>(Vec(0, 0)); | |||||
| manufacturerLabel->align = Label::RIGHT_ALIGN; | |||||
| addChild(manufacturerLabel); | |||||
| tagsLabel = Widget::create<Label>(Vec(26, BND_WIDGET_HEIGHT)); | |||||
| addChild(tagsLabel); | |||||
| } | |||||
| void setModel(Model *model) { | |||||
| assert(model); | |||||
| this->model = model; | |||||
| auto it = sFavoriteModels.find(model); | |||||
| if (it != sFavoriteModels.end()) | |||||
| favoriteButton->setValue(1); | |||||
| favoriteButton->model = model; | |||||
| nameLabel->text = model->name; | |||||
| manufacturerLabel->text = model->manufacturer; | |||||
| int i = 0; | |||||
| for (ModelTag tag : model->tags) { | |||||
| if (i++ > 0) | |||||
| tagsLabel->text += ", "; | |||||
| tagsLabel->text += gTagNames[tag]; | |||||
| } | |||||
| } | |||||
| void step() override { | |||||
| BrowserListItem::step(); | |||||
| manufacturerLabel->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH; | |||||
| } | |||||
| void onAction(EventAction &e) override { | |||||
| ModuleWidget *moduleWidget = model->createModuleWidget(); | |||||
| gRackWidget->moduleContainer->addChild(moduleWidget); | |||||
| // Move module nearest to the mouse position | |||||
| moduleWidget->box.pos = gRackWidget->lastMousePos.minus(moduleWidget->box.size.div(2)); | |||||
| gRackWidget->requestModuleBoxNearest(moduleWidget, moduleWidget->box); | |||||
| } | |||||
| }; | |||||
| struct ManufacturerItem : BrowserListItem { | |||||
| std::string manufacturer; | |||||
| Label *manufacturerLabel; | |||||
| ManufacturerItem() { | |||||
| manufacturerLabel = Widget::create<Label>(Vec(0, 0)); | |||||
| manufacturerLabel->text = "Show all modules"; | |||||
| addChild(manufacturerLabel); | |||||
| } | |||||
| void setManufacturer(std::string manufacturer) { | |||||
| this->manufacturer = manufacturer; | |||||
| manufacturerLabel->text = manufacturer; | |||||
| } | |||||
| void step() override { | |||||
| BrowserListItem::step(); | |||||
| } | |||||
| void onAction(EventAction &e) override; | |||||
| }; | |||||
| struct BrowserList : List { | |||||
| int selected = 0; | |||||
| void step() override { | |||||
| // If we have zero children, this result doesn't matter anyway. | |||||
| selected = clamp(selected, 0, children.size() - 1); | |||||
| int i = 0; | |||||
| for (Widget *w : children) { | |||||
| BrowserListItem *item = dynamic_cast<BrowserListItem*>(w); | |||||
| if (item) { | |||||
| item->selected = (i == selected); | |||||
| } | |||||
| i++; | |||||
| } | |||||
| List::step(); | |||||
| } | |||||
| void selectChild(Widget *child) { | |||||
| int i = 0; | |||||
| for (Widget *w : children) { | |||||
| if (w == child) { | |||||
| selected = i; | |||||
| break; | |||||
| } | |||||
| i++; | |||||
| } | |||||
| } | |||||
| Widget *getSelectedChild() { | |||||
| int i = 0; | |||||
| for (Widget *w : children) { | |||||
| if (i == selected) { | |||||
| return w; | |||||
| } | |||||
| i++; | |||||
| } | |||||
| return NULL; | |||||
| } | |||||
| }; | |||||
| struct ModuleBrowser; | |||||
| struct SearchModuleField : TextField { | |||||
| ModuleBrowser *moduleBrowser; | |||||
| void onTextChange() override; | |||||
| void onKey(EventKey &e) override; | |||||
| }; | |||||
| struct ModuleBrowser : OpaqueWidget { | |||||
| SearchModuleField *searchField; | |||||
| ScrollWidget *moduleScroll; | |||||
| BrowserList *moduleList; | |||||
| std::string manufacturerFilter; | |||||
| ModuleBrowser() { | |||||
| box.size.x = 400; | |||||
| // Search | |||||
| searchField = new SearchModuleField(); | |||||
| searchField->box.size.x = box.size.x; | |||||
| searchField->moduleBrowser = this; | |||||
| addChild(searchField); | |||||
| moduleList = new BrowserList(); | |||||
| moduleList->box.size = Vec(box.size.x, 0.0); | |||||
| // Module Scroll | |||||
| moduleScroll = new ScrollWidget(); | |||||
| moduleScroll->box.pos.y = searchField->box.size.y; | |||||
| moduleScroll->box.size.x = box.size.x; | |||||
| moduleScroll->container->addChild(moduleList); | |||||
| addChild(moduleScroll); | |||||
| // Trigger search update | |||||
| searchField->setText(""); | |||||
| } | |||||
| void refreshSearch() { | |||||
| std::string search = searchField->text; | |||||
| moduleList->clearChildren(); | |||||
| moduleList->selected = 0; | |||||
| // Favorites | |||||
| for (Model *model : sFavoriteModels) { | |||||
| if ((manufacturerFilter.empty() || manufacturerFilter == model->manufacturer) && isModelMatch(model, search)) { | |||||
| ModelItem *item = new ModelItem(); | |||||
| item->setModel(model); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| } | |||||
| // Manufacturers | |||||
| if (manufacturerFilter.empty()) { | |||||
| // Collect all manufacturers | |||||
| std::set<std::string> manufacturers; | |||||
| for (Plugin *plugin : gPlugins) { | |||||
| for (Model *model : plugin->models) { | |||||
| if (model->manufacturer.empty()) | |||||
| continue; | |||||
| manufacturers.insert(model->manufacturer); | |||||
| } | |||||
| } | |||||
| // Manufacturer items | |||||
| for (std::string manufacturer : manufacturers) { | |||||
| if (isMatch(manufacturer, search)) { | |||||
| ManufacturerItem *item = new ManufacturerItem(); | |||||
| item->setManufacturer(manufacturer); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| // Dummy manufacturer for clearing manufacturer filter | |||||
| ManufacturerItem *item = new ManufacturerItem(); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| // Models | |||||
| for (Plugin *plugin : gPlugins) { | |||||
| for (Model *model : plugin->models) { | |||||
| if ((manufacturerFilter.empty() || manufacturerFilter == model->manufacturer) && isModelMatch(model, search)) { | |||||
| ModelItem *item = new ModelItem(); | |||||
| item->setModel(model); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| void step() override { | |||||
| box.pos = parent->box.size.minus(box.size).div(2).round(); | |||||
| box.pos.y = 40; | |||||
| box.size.y = parent->box.size.y - 2 * box.pos.y; | |||||
| moduleScroll->box.size.y = box.size.y - moduleScroll->box.pos.y; | |||||
| gFocusedWidget = searchField; | |||||
| Widget::step(); | |||||
| } | |||||
| }; | |||||
| // Implementations of inline methods above | |||||
| void ManufacturerItem::onAction(EventAction &e) { | |||||
| ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
| moduleBrowser->manufacturerFilter = manufacturer; | |||||
| moduleBrowser->refreshSearch(); | |||||
| e.consumed = false; | |||||
| } | |||||
| void FavoriteRadioButton::onAction(EventAction &e) { | |||||
| if (!model) | |||||
| return; | |||||
| if (value) { | |||||
| sFavoriteModels.insert(model); | |||||
| } | |||||
| else { | |||||
| auto it = sFavoriteModels.find(model); | |||||
| if (it != sFavoriteModels.end()) | |||||
| sFavoriteModels.erase(it); | |||||
| } | |||||
| ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||||
| if (moduleBrowser) | |||||
| moduleBrowser->refreshSearch(); | |||||
| } | |||||
| void BrowserListItem::onMouseEnter(EventMouseEnter &e) { | |||||
| BrowserList *list = getAncestorOfType<BrowserList>(); | |||||
| list->selectChild(this); | |||||
| } | |||||
| void SearchModuleField::onTextChange() { | |||||
| moduleBrowser->refreshSearch(); | |||||
| } | |||||
| void SearchModuleField::onKey(EventKey &e) { | |||||
| switch (e.key) { | |||||
| case GLFW_KEY_ESCAPE: { | |||||
| gScene->setOverlay(NULL); | |||||
| e.consumed = true; | |||||
| return; | |||||
| } break; | |||||
| case GLFW_KEY_UP: { | |||||
| moduleBrowser->moduleList->selected--; | |||||
| e.consumed = true; | |||||
| } break; | |||||
| case GLFW_KEY_DOWN: { | |||||
| moduleBrowser->moduleList->selected++; | |||||
| e.consumed = true; | |||||
| } break; | |||||
| case GLFW_KEY_ENTER: { | |||||
| Widget *w = moduleBrowser->moduleList->getSelectedChild(); | |||||
| BrowserListItem *item = dynamic_cast<BrowserListItem*>(w); | |||||
| if (item) { | |||||
| item->doAction(); | |||||
| e.consumed = true; | |||||
| return; | |||||
| } | |||||
| } break; | |||||
| } | |||||
| if (!e.consumed) { | |||||
| TextField::onKey(e); | |||||
| } | |||||
| } | |||||
| // Global functions | |||||
| void appModuleBrowserCreate() { | |||||
| MenuOverlay *overlay = new MenuOverlay(); | |||||
| ModuleBrowser *moduleBrowser = new ModuleBrowser(); | |||||
| overlay->addChild(moduleBrowser); | |||||
| gScene->setOverlay(overlay); | |||||
| } | |||||
| json_t *appModuleBrowserToJson() { | |||||
| // TODO | |||||
| return json_object(); | |||||
| } | |||||
| void appModuleBrowserFromJson(json_t *root) { | |||||
| // TODO | |||||
| } | |||||
| } // namespace rack | |||||
| @@ -1,212 +0,0 @@ | |||||
| #include "app.hpp" | |||||
| #include "plugin.hpp" | |||||
| #include "window.hpp" | |||||
| #include <set> | |||||
| #define BND_LABEL_FONT_SIZE 13 | |||||
| namespace rack { | |||||
| static std::string sSearch; | |||||
| static std::set<Model*> sFavoriteModels; | |||||
| struct FavoriteRadioButton : RadioButton { | |||||
| Model *model = NULL; | |||||
| void onChange(EventChange &e) override { | |||||
| debug("HI"); | |||||
| if (!model) | |||||
| return; | |||||
| if (value) { | |||||
| sFavoriteModels.insert(model); | |||||
| } | |||||
| else { | |||||
| auto it = sFavoriteModels.find(model); | |||||
| if (it != sFavoriteModels.end()) | |||||
| sFavoriteModels.erase(it); | |||||
| } | |||||
| } | |||||
| }; | |||||
| struct ModuleListItem : OpaqueWidget { | |||||
| bool selected = false; | |||||
| FavoriteRadioButton *favoriteButton; | |||||
| ModuleListItem() { | |||||
| box.size.y = 3 * BND_WIDGET_HEIGHT; | |||||
| favoriteButton = new FavoriteRadioButton(); | |||||
| favoriteButton->box.pos = Vec(7, BND_WIDGET_HEIGHT); | |||||
| favoriteButton->box.size.x = 20; | |||||
| favoriteButton->label = "★"; | |||||
| addChild(favoriteButton); | |||||
| } | |||||
| void draw(NVGcontext *vg) override { | |||||
| BNDwidgetState state = selected ? BND_HOVER : BND_DEFAULT; | |||||
| bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, ""); | |||||
| Widget::draw(vg); | |||||
| } | |||||
| void onDragDrop(EventDragDrop &e) override { | |||||
| if (e.origin != this) | |||||
| return; | |||||
| EventAction eAction; | |||||
| eAction.consumed = true; | |||||
| onAction(eAction); | |||||
| if (eAction.consumed) { | |||||
| // deletes `this` | |||||
| gScene->setOverlay(NULL); | |||||
| } | |||||
| } | |||||
| }; | |||||
| struct ModelItem : ModuleListItem { | |||||
| Model *model; | |||||
| void setModel(Model *model) { | |||||
| this->model = model; | |||||
| auto it = sFavoriteModels.find(model); | |||||
| if (it != sFavoriteModels.end()) | |||||
| favoriteButton->setValue(1); | |||||
| favoriteButton->model = model; | |||||
| } | |||||
| void draw(NVGcontext *vg) override { | |||||
| ModuleListItem::draw(vg); | |||||
| // bndMenuItem(vg, 0.0, 0.0, box.size.x, box.size.y, BND_DEFAULT, -1, model->name.c_str()); | |||||
| float x = box.size.x - bndLabelWidth(vg, -1, model->manufacturer.c_str()); | |||||
| NVGcolor rightColor = bndGetTheme()->menuTheme.textColor; | |||||
| bndIconLabelValue(vg, x, 0.0, box.size.x, box.size.y, -1, rightColor, BND_LEFT, BND_LABEL_FONT_SIZE, model->manufacturer.c_str(), NULL); | |||||
| } | |||||
| void onAction(EventAction &e) override { | |||||
| ModuleWidget *moduleWidget = model->createModuleWidget(); | |||||
| gRackWidget->moduleContainer->addChild(moduleWidget); | |||||
| // Move module nearest to the mouse position | |||||
| // Rect box; | |||||
| // box.size = moduleWidget->box.size; | |||||
| // AddModuleWindow *w = getAncestorOfType<AddModuleWindow>(); | |||||
| // box.pos = w->modulePos.minus(box.getCenter()); | |||||
| // gRackWidget->requestModuleBoxNearest(moduleWidget, box); | |||||
| } | |||||
| }; | |||||
| struct ModuleBrowser; | |||||
| struct SearchModuleField : TextField { | |||||
| ModuleBrowser *moduleBrowser; | |||||
| void onTextChange() override; | |||||
| void onKey(EventKey &e) override; | |||||
| }; | |||||
| struct ModuleBrowser : OpaqueWidget { | |||||
| SearchModuleField *searchField; | |||||
| ScrollWidget *moduleScroll; | |||||
| List *moduleList; | |||||
| ModuleBrowser() { | |||||
| box.size.x = 300; | |||||
| // Search | |||||
| searchField = new SearchModuleField(); | |||||
| searchField->box.size.x = box.size.x; | |||||
| searchField->moduleBrowser = this; | |||||
| addChild(searchField); | |||||
| moduleList = new List(); | |||||
| moduleList->box.size = Vec(box.size.x, 0.0); | |||||
| // Module Scroll | |||||
| moduleScroll = new ScrollWidget(); | |||||
| moduleScroll->box.pos.y = searchField->box.size.y; | |||||
| moduleScroll->box.size.x = box.size.x; | |||||
| moduleScroll->container->addChild(moduleList); | |||||
| addChild(moduleScroll); | |||||
| // Focus search | |||||
| searchField->setText(sSearch); | |||||
| EventFocus eFocus; | |||||
| searchField->onFocus(eFocus); | |||||
| } | |||||
| void setSearch(std::string search) { | |||||
| moduleList->clearChildren(); | |||||
| // Favorites | |||||
| for (Model *model : sFavoriteModels) { | |||||
| ModelItem *item = new ModelItem(); | |||||
| item->setModel(model); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| // Models | |||||
| for (Plugin *plugin : gPlugins) { | |||||
| for (Model *model : plugin->models) { | |||||
| ModelItem *item = new ModelItem(); | |||||
| item->setModel(model); | |||||
| moduleList->addChild(item); | |||||
| } | |||||
| } | |||||
| } | |||||
| void step() override { | |||||
| box.pos = parent->box.size.minus(box.size).div(2).round(); | |||||
| box.pos.y = 40; | |||||
| box.size.y = parent->box.size.y - 2 * box.pos.y; | |||||
| moduleScroll->box.size.y = box.size.y - moduleScroll->box.pos.y; | |||||
| gFocusedWidget = searchField; | |||||
| Widget::step(); | |||||
| } | |||||
| }; | |||||
| void SearchModuleField::onTextChange() { | |||||
| sSearch = text; | |||||
| moduleBrowser->setSearch(text); | |||||
| } | |||||
| void SearchModuleField::onKey(EventKey &e) { | |||||
| switch (e.key) { | |||||
| case GLFW_KEY_ESCAPE: { | |||||
| gScene->setOverlay(NULL); | |||||
| e.consumed = true; | |||||
| return; | |||||
| } break; | |||||
| } | |||||
| if (!e.consumed) { | |||||
| TextField::onKey(e); | |||||
| } | |||||
| } | |||||
| void appModuleBrowserCreate() { | |||||
| MenuOverlay *overlay = new MenuOverlay(); | |||||
| ModuleBrowser *moduleBrowser = new ModuleBrowser(); | |||||
| overlay->addChild(moduleBrowser); | |||||
| gScene->setOverlay(overlay); | |||||
| } | |||||
| json_t *appModuleBrowserToJson() { | |||||
| // TODO | |||||
| return json_object(); | |||||
| } | |||||
| void appModuleBrowserFromJson(json_t *root) { | |||||
| // TODO | |||||
| } | |||||
| } // namespace rack | |||||
| @@ -92,14 +92,14 @@ static void engineStep() { | |||||
| // Step ports | // Step ports | ||||
| for (Input &input : module->inputs) { | for (Input &input : module->inputs) { | ||||
| if (input.active) { | if (input.active) { | ||||
| float value = input.value / 10.0; | |||||
| float value = input.value / 10.f; | |||||
| input.plugLights[0].setBrightnessSmooth(value); | input.plugLights[0].setBrightnessSmooth(value); | ||||
| input.plugLights[1].setBrightnessSmooth(-value); | input.plugLights[1].setBrightnessSmooth(-value); | ||||
| } | } | ||||
| } | } | ||||
| for (Output &output : module->outputs) { | for (Output &output : module->outputs) { | ||||
| if (output.active) { | if (output.active) { | ||||
| float value = output.value / 10.0; | |||||
| float value = output.value / 10.f; | |||||
| output.plugLights[0].setBrightnessSmooth(value); | output.plugLights[0].setBrightnessSmooth(value); | ||||
| output.plugLights[1].setBrightnessSmooth(-value); | output.plugLights[1].setBrightnessSmooth(-value); | ||||
| } | } | ||||
| @@ -3,8 +3,18 @@ | |||||
| namespace rack { | namespace rack { | ||||
| void Label::draw(NVGcontext *vg) { | void Label::draw(NVGcontext *vg) { | ||||
| bndLabel(vg, 0.0, 0.0, box.size.x, box.size.y, -1, text.c_str()); | |||||
| float x = 0.0; | |||||
| if (align == RIGHT_ALIGN) { | |||||
| x = box.size.x - bndLabelWidth(vg, -1, text.c_str()); | |||||
| } | |||||
| else if (align == CENTER_ALIGN) { | |||||
| x = (box.size.x - bndLabelWidth(vg, -1, text.c_str())) / 2.0; | |||||
| } | |||||
| bndLabel(vg, x, 0.0, box.size.x, box.size.y, -1, text.c_str()); | |||||
| } | } | ||||
| @@ -16,7 +16,7 @@ void List::step() { | |||||
| child->box.pos = Vec(0.0, box.size.y); | child->box.pos = Vec(0.0, box.size.y); | ||||
| box.size.y += child->box.size.y; | box.size.y += child->box.size.y; | ||||
| // Resize width of child | // Resize width of child | ||||
| child->box.size.x = box.size.x - BND_SCROLLBAR_WIDTH; | |||||
| child->box.size.x = box.size.x; | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,6 +5,9 @@ | |||||
| namespace rack { | namespace rack { | ||||
| static const float SCROLL_SPEED = 1.2; | |||||
| /** Parent must be a ScrollWidget */ | /** Parent must be a ScrollWidget */ | ||||
| struct ScrollBar : OpaqueWidget { | struct ScrollBar : OpaqueWidget { | ||||
| enum Orientation { | enum Orientation { | ||||
| @@ -33,9 +36,9 @@ struct ScrollBar : OpaqueWidget { | |||||
| ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent); | ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent); | ||||
| assert(scrollWidget); | assert(scrollWidget); | ||||
| if (orientation == HORIZONTAL) | if (orientation == HORIZONTAL) | ||||
| scrollWidget->offset.x += e.mouseRel.x; | |||||
| scrollWidget->offset.x += SCROLL_SPEED * e.mouseRel.x; | |||||
| else | else | ||||
| scrollWidget->offset.y += e.mouseRel.y; | |||||
| scrollWidget->offset.y += SCROLL_SPEED * e.mouseRel.y; | |||||
| } | } | ||||
| void onDragEnd(EventDragEnd &e) override { | void onDragEnd(EventDragEnd &e) override { | ||||
| @@ -69,7 +72,13 @@ void ScrollWidget::draw(NVGcontext *vg) { | |||||
| void ScrollWidget::step() { | void ScrollWidget::step() { | ||||
| // Clamp scroll offset | // Clamp scroll offset | ||||
| Vec containerCorner = container->getChildrenBoundingBox().getBottomRight(); | Vec containerCorner = container->getChildrenBoundingBox().getBottomRight(); | ||||
| offset = offset.clamp(Rect(Vec(0, 0), containerCorner.minus(box.size))); | |||||
| Rect containerBox = Rect(Vec(0, 0), containerCorner.minus(box.size)); | |||||
| offset = offset.clamp(containerBox); | |||||
| // Lock offset to top/left if no scrollbar will display | |||||
| if (containerBox.size.x < 0.0) | |||||
| offset.x = 0.0; | |||||
| if (containerBox.size.y < 0.0) | |||||
| offset.y = 0.0; | |||||
| // Update the container's positions from the offset | // Update the container's positions from the offset | ||||
| container->box.pos = offset.neg().round(); | container->box.pos = offset.neg().round(); | ||||