| @@ -8,6 +8,12 @@ namespace rack { | |||
| struct Label : Widget { | |||
| std::string text; | |||
| enum Align { | |||
| LEFT_ALIGN, | |||
| CENTER_ALIGN, | |||
| RIGHT_ALIGN | |||
| }; | |||
| Align align = LEFT_ALIGN; | |||
| Label() { | |||
| 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 | |||
| 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. | |||
| @@ -74,11 +74,11 @@ inline bool isNear(float a, float b, float epsilon = 1.0e-6f) { | |||
| /** Limits a value between a minimum and maximum | |||
| 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 | |||
| */ | |||
| inline float clamp2(float x, float min, float max) { | |||
| @@ -180,6 +180,7 @@ struct Vec { | |||
| return isfinite(x) && isfinite(y); | |||
| } | |||
| Vec clamp(Rect bound); | |||
| Vec clamp2(Rect bound); | |||
| }; | |||
| @@ -260,8 +261,14 @@ struct Rect { | |||
| inline Vec Vec::clamp(Rect bound) { | |||
| 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 | |||
| for (Input &input : module->inputs) { | |||
| if (input.active) { | |||
| float value = input.value / 10.0; | |||
| float value = input.value / 10.f; | |||
| input.plugLights[0].setBrightnessSmooth(value); | |||
| input.plugLights[1].setBrightnessSmooth(-value); | |||
| } | |||
| } | |||
| for (Output &output : module->outputs) { | |||
| if (output.active) { | |||
| float value = output.value / 10.0; | |||
| float value = output.value / 10.f; | |||
| output.plugLights[0].setBrightnessSmooth(value); | |||
| output.plugLights[1].setBrightnessSmooth(-value); | |||
| } | |||
| @@ -3,8 +3,18 @@ | |||
| namespace rack { | |||
| 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); | |||
| box.size.y += child->box.size.y; | |||
| // 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 { | |||
| static const float SCROLL_SPEED = 1.2; | |||
| /** Parent must be a ScrollWidget */ | |||
| struct ScrollBar : OpaqueWidget { | |||
| enum Orientation { | |||
| @@ -33,9 +36,9 @@ struct ScrollBar : OpaqueWidget { | |||
| ScrollWidget *scrollWidget = dynamic_cast<ScrollWidget*>(parent); | |||
| assert(scrollWidget); | |||
| if (orientation == HORIZONTAL) | |||
| scrollWidget->offset.x += e.mouseRel.x; | |||
| scrollWidget->offset.x += SCROLL_SPEED * e.mouseRel.x; | |||
| else | |||
| scrollWidget->offset.y += e.mouseRel.y; | |||
| scrollWidget->offset.y += SCROLL_SPEED * e.mouseRel.y; | |||
| } | |||
| void onDragEnd(EventDragEnd &e) override { | |||
| @@ -69,7 +72,13 @@ void ScrollWidget::draw(NVGcontext *vg) { | |||
| void ScrollWidget::step() { | |||
| // Clamp scroll offset | |||
| 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 | |||
| container->box.pos = offset.neg().round(); | |||