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