@@ -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(); | ||||