@@ -26,7 +26,7 @@ struct ModuleWidget : widget::OpaqueWidget { | |||||
math::Vec dragPos; | math::Vec dragPos; | ||||
math::Vec oldPos; | math::Vec oldPos; | ||||
ModuleWidget() {} | |||||
ModuleWidget(); | |||||
DEPRECATED ModuleWidget(engine::Module *module) { | DEPRECATED ModuleWidget(engine::Module *module) { | ||||
setModule(module); | setModule(module); | ||||
} | } | ||||
@@ -1,5 +1,5 @@ | |||||
#include "common.hpp" | |||||
#include "sse_mathfun.h" | #include "sse_mathfun.h" | ||||
#include <cstring> | |||||
#include <emmintrin.h> | #include <emmintrin.h> | ||||
@@ -7,7 +7,10 @@ namespace rack { | |||||
namespace dsp { | namespace dsp { | ||||
/** Casts an int to float, bitwise without conversion. */ | |||||
inline float cast_i32_f32(int i) { | inline float cast_i32_f32(int i) { | ||||
static_assert(sizeof(int) == sizeof(float), "int and float must be the same size"); | |||||
// Should be optimized to two `mov` instructions | |||||
float f; | float f; | ||||
std::memcpy(&f, &i, sizeof(f)); | std::memcpy(&f, &i, sizeof(f)); | ||||
return f; | return f; | ||||
@@ -21,6 +24,17 @@ inline int cast_f32_i32(float f) { | |||||
} | } | ||||
/** Generic class for vector float types. | |||||
This class is designed to be used just like `float` scalars, with extra features for handling bitwise logic, conditions, loading, and storing. | |||||
Usage example: | |||||
float a[4], b[4]; | |||||
f32_4 a = f32_4::load(in); | |||||
f32_4 b = 2.f * a / (1 - a); | |||||
b *= sin(2 * M_PI * a); | |||||
b.store(out); | |||||
*/ | |||||
template <int N> | template <int N> | ||||
struct f32; | struct f32; | ||||
@@ -65,7 +79,7 @@ typedef f32<4> f32_4; | |||||
// Operator overloads | // Operator overloads | ||||
/** `a operator b` */ | |||||
/** `a @ b` */ | |||||
#define DECLARE_F32_4_OPERATOR_INFIX(operator, func) \ | #define DECLARE_F32_4_OPERATOR_INFIX(operator, func) \ | ||||
inline f32_4 operator(const f32_4 &a, const f32_4 &b) { \ | inline f32_4 operator(const f32_4 &a, const f32_4 &b) { \ | ||||
return f32_4(func(a.v, b.v)); \ | return f32_4(func(a.v, b.v)); \ | ||||
@@ -79,7 +93,7 @@ typedef f32<4> f32_4; | |||||
return operator(a, f32_4(b)); \ | return operator(a, f32_4(b)); \ | ||||
} | } | ||||
/** `a operator b` */ | |||||
/** `a @= b` */ | |||||
#define DECLARE_F32_4_OPERATOR_INCREMENT(operator, opfunc) \ | #define DECLARE_F32_4_OPERATOR_INCREMENT(operator, opfunc) \ | ||||
inline f32_4 &operator(f32_4 &a, const f32_4 &b) { \ | inline f32_4 &operator(f32_4 &a, const f32_4 &b) { \ | ||||
a = opfunc(a, b); \ | a = opfunc(a, b); \ | ||||
@@ -95,7 +109,7 @@ DECLARE_F32_4_OPERATOR_INFIX(operator-, _mm_sub_ps) | |||||
DECLARE_F32_4_OPERATOR_INFIX(operator*, _mm_mul_ps) | DECLARE_F32_4_OPERATOR_INFIX(operator*, _mm_mul_ps) | ||||
DECLARE_F32_4_OPERATOR_INFIX(operator/, _mm_div_ps) | DECLARE_F32_4_OPERATOR_INFIX(operator/, _mm_div_ps) | ||||
/** Boolean operators on vectors give 0x00000000 for false and 0xffffffff for true, for each vector element. | |||||
/** | |||||
Use these to apply logic, bit masks, and conditions to elements. | Use these to apply logic, bit masks, and conditions to elements. | ||||
Examples: | Examples: | ||||
@@ -106,6 +120,8 @@ DECLARE_F32_4_OPERATOR_INFIX(operator^, _mm_xor_ps) | |||||
DECLARE_F32_4_OPERATOR_INFIX(operator&, _mm_and_ps) | DECLARE_F32_4_OPERATOR_INFIX(operator&, _mm_and_ps) | ||||
DECLARE_F32_4_OPERATOR_INFIX(operator|, _mm_mul_ps) | DECLARE_F32_4_OPERATOR_INFIX(operator|, _mm_mul_ps) | ||||
/** Boolean operators on vectors give 0x00000000 for false and 0xffffffff for true, for each vector element. | |||||
*/ | |||||
DECLARE_F32_4_OPERATOR_INCREMENT(operator+=, operator+); | DECLARE_F32_4_OPERATOR_INCREMENT(operator+=, operator+); | ||||
DECLARE_F32_4_OPERATOR_INCREMENT(operator-=, operator-); | DECLARE_F32_4_OPERATOR_INCREMENT(operator-=, operator-); | ||||
DECLARE_F32_4_OPERATOR_INCREMENT(operator*=, operator*); | DECLARE_F32_4_OPERATOR_INCREMENT(operator*=, operator*); | ||||
@@ -245,30 +245,42 @@ struct Zoom : Event { | |||||
}; | }; | ||||
/** Occurs after the Widget's position is set by setPos(); | |||||
/** Occurs after a Widget's position is set by Widget::setPos(). | |||||
*/ | */ | ||||
struct Reposition : Event { | struct Reposition : Event { | ||||
}; | }; | ||||
/** Occurs after the Widget's size is set by setSize(); | |||||
/** Occurs after a Widget's size is set by Widget::setSize(). | |||||
*/ | */ | ||||
struct Resize : Event { | struct Resize : Event { | ||||
}; | }; | ||||
/** Occurs after the Widget is added to a parent. | |||||
/** Occurs after a Widget is added to a parent. | |||||
*/ | */ | ||||
struct Add : Event { | struct Add : Event { | ||||
}; | }; | ||||
/** Occurs before the Widget is remove from its parent. | |||||
/** Occurs before a Widget is removed from its parent. | |||||
*/ | */ | ||||
struct Remove : Event { | struct Remove : Event { | ||||
}; | }; | ||||
/** Occurs after a Widget is shown with Widget::show(). | |||||
*/ | |||||
struct Show : Event { | |||||
}; | |||||
/** Occurs after a Widget is hidden with Widget::hide(). | |||||
*/ | |||||
struct Hide : Event { | |||||
}; | |||||
struct State { | struct State { | ||||
widget::Widget *rootWidget = NULL; | widget::Widget *rootWidget = NULL; | ||||
/** State widgets | /** State widgets | ||||
@@ -3,7 +3,7 @@ | |||||
#include "plugin/Plugin.hpp" | #include "plugin/Plugin.hpp" | ||||
#include "plugin/Model.hpp" | #include "plugin/Model.hpp" | ||||
#include <vector> | #include <vector> | ||||
#include <list> | |||||
#include <set> | |||||
namespace rack { | namespace rack { | ||||
@@ -16,21 +16,21 @@ namespace plugin { | |||||
void init(bool devMode); | void init(bool devMode); | ||||
void destroy(); | void destroy(); | ||||
void logIn(std::string email, std::string password); | |||||
void logIn(const std::string &email, const std::string &password); | |||||
void logOut(); | void logOut(); | ||||
/** Returns whether a new plugin is available, and downloads it unless doing a dry run */ | /** Returns whether a new plugin is available, and downloads it unless doing a dry run */ | ||||
bool sync(bool dryRun); | bool sync(bool dryRun); | ||||
void cancelDownload(); | void cancelDownload(); | ||||
bool isLoggedIn(); | bool isLoggedIn(); | ||||
Plugin *getPlugin(std::string pluginSlug); | |||||
Model *getModel(std::string pluginSlug, std::string modelSlug); | |||||
std::string getAllowedTag(std::string tag); | |||||
Plugin *getPlugin(const std::string &pluginSlug); | |||||
Model *getModel(const std::string &pluginSlug, const std::string &modelSlug); | |||||
std::string normalizeTag(const std::string &tag); | |||||
/** Checks that the slug contains only alphanumeric characters, "-", and "_" */ | /** Checks that the slug contains only alphanumeric characters, "-", and "_" */ | ||||
bool isSlugValid(std::string slug); | |||||
bool isSlugValid(const std::string &slug); | |||||
extern const std::vector<std::string> allowedTags; | extern const std::vector<std::string> allowedTags; | ||||
extern std::list<Plugin*> plugins; | |||||
extern std::vector<Plugin*> plugins; | |||||
extern bool isDownloading; | extern bool isDownloading; | ||||
extern float downloadProgress; | extern float downloadProgress; | ||||
extern std::string downloadName; | extern std::string downloadName; | ||||
@@ -2,7 +2,7 @@ | |||||
#include "common.hpp" | #include "common.hpp" | ||||
#include "plugin/Plugin.hpp" | #include "plugin/Plugin.hpp" | ||||
#include <jansson.h> | #include <jansson.h> | ||||
#include <list> | |||||
#include <set> | |||||
namespace rack { | namespace rack { | ||||
@@ -31,7 +31,7 @@ struct Model { | |||||
/** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | /** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | ||||
std::string name; | std::string name; | ||||
/** List of tags representing the function(s) of the module */ | /** List of tags representing the function(s) of the module */ | ||||
std::list<std::string> tags; | |||||
std::set<std::string> tags; | |||||
/** A one-line summary of the module's purpose */ | /** A one-line summary of the module's purpose */ | ||||
std::string description; | std::string description; | ||||
@@ -0,0 +1,19 @@ | |||||
#pragma once | |||||
#include "widget/Widget.hpp" | |||||
#include "ui/common.hpp" | |||||
namespace rack { | |||||
namespace ui { | |||||
/** Positions children with a margin between the layout's box. */ | |||||
struct MarginLayout : widget::Widget { | |||||
math::Vec margin; | |||||
void step() override; | |||||
}; | |||||
} // namespace ui | |||||
} // namespace rack |
@@ -40,6 +40,9 @@ struct Widget { | |||||
void setPos(math::Vec pos); | void setPos(math::Vec pos); | ||||
void setSize(math::Vec size); | void setSize(math::Vec size); | ||||
void show(); | |||||
void hide(); | |||||
void requestDelete(); | |||||
virtual math::Rect getChildrenBoundingBox(); | virtual math::Rect getChildrenBoundingBox(); | ||||
/** Returns `v` transformed into the coordinate system of `relative` */ | /** Returns `v` transformed into the coordinate system of `relative` */ | ||||
@@ -156,6 +159,8 @@ struct Widget { | |||||
virtual void onResize(const event::Resize &e) {} | virtual void onResize(const event::Resize &e) {} | ||||
virtual void onAdd(const event::Add &e) {} | virtual void onAdd(const event::Add &e) {} | ||||
virtual void onRemove(const event::Remove &e) {} | virtual void onRemove(const event::Remove &e) {} | ||||
virtual void onShow(const event::Show &e) {recurseEvent(&Widget::onShow, e);} | |||||
virtual void onHide(const event::Hide &e) {recurseEvent(&Widget::onHide, e);} | |||||
}; | }; | ||||
@@ -14,6 +14,7 @@ struct BlankPanel : Widget { | |||||
void step() override { | void step() override { | ||||
panelBorder->box.size = box.size; | panelBorder->box.size = box.size; | ||||
Widget::step(); | |||||
} | } | ||||
void draw(const DrawContext &ctx) override { | void draw(const DrawContext &ctx) override { | ||||
@@ -16,7 +16,7 @@ void LedDisplay::draw(const widget::DrawContext &ctx) { | |||||
nvgFillColor(ctx.vg, nvgRGB(0x00, 0x00, 0x00)); | nvgFillColor(ctx.vg, nvgRGB(0x00, 0x00, 0x00)); | ||||
nvgFill(ctx.vg); | nvgFill(ctx.vg); | ||||
nvgScissor(ctx.vg, 0, 0, box.size.x, box.size.y); | |||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
widget::Widget::draw(ctx); | widget::Widget::draw(ctx); | ||||
nvgResetScissor(ctx.vg); | nvgResetScissor(ctx.vg); | ||||
} | } | ||||
@@ -79,7 +79,7 @@ LedDisplayTextField::LedDisplayTextField() { | |||||
void LedDisplayTextField::draw(const widget::DrawContext &ctx) { | void LedDisplayTextField::draw(const widget::DrawContext &ctx) { | ||||
nvgScissor(ctx.vg, 0, 0, box.size.x, box.size.y); | |||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
// Background | // Background | ||||
nvgBeginPath(ctx.vg); | nvgBeginPath(ctx.vg); | ||||
@@ -4,9 +4,14 @@ | |||||
#include "widget/ZoomWidget.hpp" | #include "widget/ZoomWidget.hpp" | ||||
#include "ui/ScrollWidget.hpp" | #include "ui/ScrollWidget.hpp" | ||||
#include "ui/SequentialLayout.hpp" | #include "ui/SequentialLayout.hpp" | ||||
#include "ui/MarginLayout.hpp" | |||||
#include "ui/Label.hpp" | #include "ui/Label.hpp" | ||||
#include "ui/TextField.hpp" | #include "ui/TextField.hpp" | ||||
#include "ui/MenuOverlay.hpp" | #include "ui/MenuOverlay.hpp" | ||||
#include "ui/List.hpp" | |||||
#include "ui/MenuItem.hpp" | |||||
#include "ui/Button.hpp" | |||||
#include "ui/ChoiceButton.hpp" | |||||
#include "app/ModuleWidget.hpp" | #include "app/ModuleWidget.hpp" | ||||
#include "app/Scene.hpp" | #include "app/Scene.hpp" | ||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
@@ -68,96 +73,135 @@ struct BrowserOverlay : widget::OpaqueWidget { | |||||
return; | return; | ||||
if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { | if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { | ||||
this->visible = false; | |||||
hide(); | |||||
} | } | ||||
} | } | ||||
}; | |||||
void onHoverKey(const event::HoverKey &e) override { | |||||
if (e.action == GLFW_PRESS) { | |||||
switch (e.key) { | |||||
case GLFW_KEY_ESCAPE: { | |||||
this->visible = false; | |||||
e.consume(this); | |||||
} break; | |||||
} | |||||
} | |||||
if (!e.getConsumed()) | |||||
widget::OpaqueWidget::onHoverKey(e); | |||||
} | |||||
}; | |||||
static const float MODEL_BOX_ZOOM = 0.5f; | |||||
struct ModelBox : widget::OpaqueWidget { | struct ModelBox : widget::OpaqueWidget { | ||||
plugin::Model *model; | plugin::Model *model; | ||||
widget::Widget *infoWidget; | |||||
/** Lazily created */ | /** Lazily created */ | ||||
widget::Widget *previewWidget = NULL; | widget::Widget *previewWidget = NULL; | ||||
/** Number of frames since draw() has been called */ | /** Number of frames since draw() has been called */ | ||||
int visibleFrames = 0; | int visibleFrames = 0; | ||||
bool selected = false; | bool selected = false; | ||||
ModelBox() { | |||||
box.size.x = 0.f; | |||||
box.size.y = std::ceil(RACK_GRID_HEIGHT * MODEL_BOX_ZOOM); | |||||
} | |||||
void setModel(plugin::Model *model) { | void setModel(plugin::Model *model) { | ||||
this->model = model; | this->model = model; | ||||
box.size.x = 70.f; | |||||
box.size.y = std::ceil(RACK_GRID_SIZE.y * 0.5f); | |||||
infoWidget = new widget::Widget; | |||||
infoWidget->box.size.x = 140; | |||||
infoWidget->box.size.y = box.size.y; | |||||
addChild(infoWidget); | |||||
math::Vec p; | |||||
p.y = box.size.y; | |||||
box.size.y += 40.0; | |||||
math::Vec pos; | |||||
ui::Label *nameLabel = new ui::Label; | ui::Label *nameLabel = new ui::Label; | ||||
// nameLabel->box.size.x = infoWidget->box.size.x; | |||||
nameLabel->box.pos = pos; | |||||
nameLabel->text = model->name; | nameLabel->text = model->name; | ||||
nameLabel->box.pos = p; | |||||
p.y += nameLabel->box.size.y; | |||||
addChild(nameLabel); | |||||
infoWidget->addChild(nameLabel); | |||||
pos = nameLabel->box.getBottomLeft(); | |||||
ui::Label *pluginLabel = new ui::Label; | ui::Label *pluginLabel = new ui::Label; | ||||
// pluginLabel->box.size.x = infoWidget->box.size.x; | |||||
pluginLabel->box.pos = pos; | |||||
pluginLabel->text = model->plugin->name; | pluginLabel->text = model->plugin->name; | ||||
pluginLabel->box.pos = p; | |||||
p.y += pluginLabel->box.size.y; | |||||
addChild(pluginLabel); | |||||
infoWidget->addChild(pluginLabel); | |||||
pos = pluginLabel->box.getBottomLeft(); | |||||
ui::Label *descriptionLabel = new ui::Label; | |||||
descriptionLabel->box.size.x = infoWidget->box.size.x; | |||||
descriptionLabel->box.pos = pos; | |||||
descriptionLabel->text = model->description; | |||||
infoWidget->addChild(descriptionLabel); | |||||
pos = descriptionLabel->box.getBottomLeft(); | |||||
pos.y = infoWidget->box.size.y; | |||||
for (const std::string &tag : model->tags) { | |||||
ui::Button *tagButton = new ui::Button; | |||||
tagButton->box.size.x = infoWidget->box.size.x; | |||||
tagButton->box.pos = pos; | |||||
tagButton->box.pos.y -= tagButton->box.size.y; | |||||
tagButton->text = tag; | |||||
infoWidget->addChild(tagButton); | |||||
pos = tagButton->box.getTopLeft(); | |||||
} | |||||
ui::Button *favoriteButton = new ui::Button; | |||||
favoriteButton->box.size.x = infoWidget->box.size.x; | |||||
favoriteButton->box.pos = pos; | |||||
favoriteButton->box.pos.y -= favoriteButton->box.size.y; | |||||
favoriteButton->text = "★"; | |||||
infoWidget->addChild(favoriteButton); | |||||
pos = favoriteButton->box.getTopLeft(); | |||||
} | |||||
void createPreview() { | |||||
assert(!previewWidget); | |||||
previewWidget = new widget::TransparentWidget; | |||||
previewWidget->box.size.y = std::ceil(RACK_GRID_HEIGHT * MODEL_BOX_ZOOM); | |||||
addChild(previewWidget); | |||||
widget::FramebufferWidget *fbWidget = 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 | |||||
fbWidget->oversample = 2.0; | |||||
} | |||||
previewWidget->addChild(fbWidget); | |||||
widget::ZoomWidget *zoomWidget = new widget::ZoomWidget; | |||||
zoomWidget->setZoom(MODEL_BOX_ZOOM); | |||||
fbWidget->addChild(zoomWidget); | |||||
ModuleWidget *moduleWidget = model->createModuleWidgetNull(); | |||||
zoomWidget->addChild(moduleWidget); | |||||
zoomWidget->box.size.x = moduleWidget->box.size.x * MODEL_BOX_ZOOM; | |||||
zoomWidget->box.size.y = RACK_GRID_HEIGHT * MODEL_BOX_ZOOM; | |||||
previewWidget->box.size.x = std::ceil(zoomWidget->box.size.x); | |||||
// Reposition infoWidget | |||||
infoWidget->box.pos.x = previewWidget->box.size.x; | |||||
box.size.x = previewWidget->box.size.x + infoWidget->box.size.x; | |||||
} | |||||
void deletePreview() { | |||||
assert(previewWidget); | |||||
removeChild(previewWidget); | |||||
delete previewWidget; | |||||
previewWidget = NULL; | |||||
} | } | ||||
void step() override { | void step() override { | ||||
if (previewWidget && ++visibleFrames >= 60) { | if (previewWidget && ++visibleFrames >= 60) { | ||||
removeChild(previewWidget); | |||||
delete previewWidget; | |||||
previewWidget = NULL; | |||||
deletePreview(); | |||||
} | } | ||||
} | } | ||||
void draw(const widget::DrawContext &ctx) override { | void draw(const widget::DrawContext &ctx) override { | ||||
visibleFrames = 0; | visibleFrames = 0; | ||||
// Lazily create ModuleWidget when drawn | |||||
// Lazily create preview when drawn | |||||
if (!previewWidget) { | if (!previewWidget) { | ||||
widget::Widget *transparentWidget = new widget::TransparentWidget; | |||||
addChild(transparentWidget); | |||||
widget::FramebufferWidget *fbWidget = 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 | |||||
fbWidget->oversample = 2.0; | |||||
} | |||||
transparentWidget->addChild(fbWidget); | |||||
widget::ZoomWidget *zoomWidget = new widget::ZoomWidget; | |||||
zoomWidget->setZoom(0.5f); | |||||
fbWidget->addChild(zoomWidget); | |||||
ModuleWidget *moduleWidget = model->createModuleWidgetNull(); | |||||
zoomWidget->addChild(moduleWidget); | |||||
zoomWidget->box.size.x = moduleWidget->box.size.x * zoomWidget->zoom; | |||||
zoomWidget->box.size.y = RACK_GRID_HEIGHT; | |||||
float width = std::ceil(zoomWidget->box.size.x); | |||||
box.size.x = std::max(box.size.x, width); | |||||
previewWidget = transparentWidget; | |||||
createPreview(); | |||||
} | } | ||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
widget::OpaqueWidget::draw(ctx); | widget::OpaqueWidget::draw(ctx); | ||||
nvgResetScissor(ctx.vg); | |||||
// Translucent overlay when selected | |||||
if (selected) { | if (selected) { | ||||
nvgBeginPath(ctx.vg); | nvgBeginPath(ctx.vg); | ||||
nvgRect(ctx.vg, 0.0, 0.0, box.size.x, box.size.y); | nvgRect(ctx.vg, 0.0, 0.0, box.size.x, box.size.y); | ||||
@@ -180,20 +224,82 @@ struct ModelBox : widget::OpaqueWidget { | |||||
struct BrowserSearchField : ui::TextField { | struct BrowserSearchField : ui::TextField { | ||||
void step() override { | |||||
// Steal focus when step is called | |||||
APP->event->setSelected(this); | |||||
ui::TextField::step(); | |||||
} | |||||
void onSelectKey(const event::SelectKey &e) override { | |||||
if (e.action == GLFW_PRESS) { | |||||
if (e.key == GLFW_KEY_ESCAPE) { | |||||
BrowserOverlay *overlay = getAncestorOfType<BrowserOverlay>(); | |||||
overlay->hide(); | |||||
e.consume(this); | |||||
} | |||||
} | |||||
if (!e.getConsumed()) | |||||
ui::TextField::onSelectKey(e); | |||||
} | |||||
void onChange(const event::Change &e) override; | void onChange(const event::Change &e) override; | ||||
void onHide(const event::Hide &e) override { | |||||
setText(""); | |||||
APP->event->setSelected(NULL); | |||||
ui::TextField::onHide(e); | |||||
} | |||||
}; | }; | ||||
struct BrowserSidebar : widget::Widget { | struct BrowserSidebar : widget::Widget { | ||||
BrowserSearchField *searchField; | BrowserSearchField *searchField; | ||||
ui::List *pluginList; | |||||
ui::ScrollWidget *pluginScroll; | |||||
ui::List *tagList; | |||||
ui::ScrollWidget *tagScroll; | |||||
BrowserSidebar() { | BrowserSidebar() { | ||||
searchField = new BrowserSearchField; | searchField = new BrowserSearchField; | ||||
addChild(searchField); | addChild(searchField); | ||||
pluginScroll = new ui::ScrollWidget; | |||||
pluginScroll->box.pos = searchField->box.getBottomLeft(); | |||||
addChild(pluginScroll); | |||||
pluginList = new ui::List; | |||||
pluginScroll->container->addChild(pluginList); | |||||
std::set<std::string> pluginNames; | |||||
for (plugin::Plugin *plugin : plugin::plugins) { | |||||
pluginNames.insert(plugin->name); | |||||
} | |||||
for (const std::string &pluginName : pluginNames) { | |||||
ui::MenuItem *item = new ui::MenuItem; | |||||
item->text = pluginName; | |||||
pluginList->addChild(item); | |||||
} | |||||
tagScroll = new ui::ScrollWidget; | |||||
tagScroll->box.pos = searchField->box.getBottomLeft(); | |||||
addChild(tagScroll); | |||||
tagList = new ui::List; | |||||
tagScroll->container->addChild(tagList); | |||||
for (const std::string &tag : plugin::allowedTags) { | |||||
ui::MenuItem *item = new ui::MenuItem; | |||||
item->text = tag; | |||||
tagList->addChild(item); | |||||
} | |||||
} | } | ||||
void step() override { | void step() override { | ||||
searchField->box.size.x = box.size.x; | searchField->box.size.x = box.size.x; | ||||
pluginScroll->box.size.y = box.size.y - searchField->box.size.y; | |||||
pluginList->box.size.x = pluginScroll->box.size.x = box.size.x / 2; | |||||
tagScroll->box.pos.x = box.size.x / 2; | |||||
tagScroll->box.size.y = box.size.y - searchField->box.size.y; | |||||
tagList->box.size.x = tagScroll->box.size.x = box.size.x / 2; | |||||
widget::Widget::step(); | widget::Widget::step(); | ||||
} | } | ||||
}; | }; | ||||
@@ -202,6 +308,7 @@ struct BrowserSidebar : widget::Widget { | |||||
struct ModuleBrowser : widget::OpaqueWidget { | struct ModuleBrowser : widget::OpaqueWidget { | ||||
BrowserSidebar *sidebar; | BrowserSidebar *sidebar; | ||||
ui::ScrollWidget *modelScroll; | ui::ScrollWidget *modelScroll; | ||||
ui::MarginLayout *modelMargin; | |||||
ui::SequentialLayout *modelContainer; | ui::SequentialLayout *modelContainer; | ||||
ModuleBrowser() { | ModuleBrowser() { | ||||
@@ -212,9 +319,13 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
modelScroll = new ui::ScrollWidget; | modelScroll = new ui::ScrollWidget; | ||||
addChild(modelScroll); | addChild(modelScroll); | ||||
modelMargin = new ui::MarginLayout; | |||||
modelMargin->margin = math::Vec(20, 20); | |||||
modelScroll->container->addChild(modelMargin); | |||||
modelContainer = new ui::SequentialLayout; | modelContainer = new ui::SequentialLayout; | ||||
modelContainer->spacing = math::Vec(10, 10); | |||||
modelScroll->container->addChild(modelContainer); | |||||
modelContainer->spacing = math::Vec(20, 20); | |||||
modelMargin->addChild(modelContainer); | |||||
for (plugin::Plugin *plugin : plugin::plugins) { | for (plugin::Plugin *plugin : plugin::plugins) { | ||||
for (plugin::Model *model : plugin->models) { | for (plugin::Model *model : plugin->models) { | ||||
@@ -233,8 +344,8 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
modelScroll->box.pos.x = sidebar->box.size.x; | modelScroll->box.pos.x = sidebar->box.size.x; | ||||
modelScroll->box.size.x = box.size.x - sidebar->box.size.x; | modelScroll->box.size.x = box.size.x - sidebar->box.size.x; | ||||
modelScroll->box.size.y = box.size.y; | modelScroll->box.size.y = box.size.y; | ||||
modelContainer->box.size.x = modelScroll->box.size.x; | |||||
modelContainer->box.size.y = modelContainer->getChildrenBoundingBox().getBottomRight().y; | |||||
modelMargin->box.size.x = modelScroll->box.size.x; | |||||
modelMargin->box.size.y = modelContainer->getChildrenBoundingBox().size.y + 2 * modelMargin->margin.y; | |||||
widget::OpaqueWidget::step(); | widget::OpaqueWidget::step(); | ||||
} | } | ||||
@@ -251,6 +362,8 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
bool match = isModelMatch(modelBox->model, search); | bool match = isModelMatch(modelBox->model, search); | ||||
modelBox->visible = match; | modelBox->visible = match; | ||||
} | } | ||||
// Reset scroll position | |||||
modelScroll->offset = math::Vec(); | |||||
} | } | ||||
}; | }; | ||||
@@ -259,6 +372,10 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
void ModelBox::onButton(const event::Button &e) { | void ModelBox::onButton(const event::Button &e) { | ||||
widget::OpaqueWidget::onButton(e); | |||||
if (e.getConsumed() != this) | |||||
return; | |||||
if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { | if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { | ||||
// Create module | // Create module | ||||
ModuleWidget *moduleWidget = model->createModuleWidget(); | ModuleWidget *moduleWidget = model->createModuleWidget(); | ||||
@@ -268,9 +385,9 @@ void ModelBox::onButton(const event::Button &e) { | |||||
// Pretend the moduleWidget was clicked so it can be dragged in the RackWidget | // Pretend the moduleWidget was clicked so it can be dragged in the RackWidget | ||||
e.consume(moduleWidget); | e.consume(moduleWidget); | ||||
// Close Module Browser | |||||
// Hide Module Browser | |||||
BrowserOverlay *overlay = getAncestorOfType<BrowserOverlay>(); | BrowserOverlay *overlay = getAncestorOfType<BrowserOverlay>(); | ||||
overlay->visible = false; | |||||
overlay->hide(); | |||||
// Push ModuleAdd history action | // Push ModuleAdd history action | ||||
history::ModuleAdd *h = new history::ModuleAdd; | history::ModuleAdd *h = new history::ModuleAdd; | ||||
@@ -278,7 +395,6 @@ void ModelBox::onButton(const event::Button &e) { | |||||
h->setModule(moduleWidget); | h->setModule(moduleWidget); | ||||
APP->history->push(h); | APP->history->push(h); | ||||
} | } | ||||
widget::OpaqueWidget::onButton(e); | |||||
} | } | ||||
void BrowserSearchField::onChange(const event::Change &e) { | void BrowserSearchField::onChange(const event::Change &e) { | ||||
@@ -133,15 +133,21 @@ struct ModuleDeleteItem : ui::MenuItem { | |||||
}; | }; | ||||
ModuleWidget::ModuleWidget() { | |||||
box.size = math::Vec(0, RACK_GRID_HEIGHT); | |||||
} | |||||
ModuleWidget::~ModuleWidget() { | ModuleWidget::~ModuleWidget() { | ||||
setModule(NULL); | setModule(NULL); | ||||
} | } | ||||
void ModuleWidget::draw(const widget::DrawContext &ctx) { | void ModuleWidget::draw(const widget::DrawContext &ctx) { | ||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
if (module && module->bypass) { | if (module && module->bypass) { | ||||
nvgGlobalAlpha(ctx.vg, 0.25); | nvgGlobalAlpha(ctx.vg, 0.25); | ||||
} | } | ||||
// nvgScissor(ctx.vg, 0, 0, box.size.x, box.size.y); | |||||
widget::Widget::draw(ctx); | widget::Widget::draw(ctx); | ||||
// Power meter | // Power meter | ||||
@@ -175,7 +181,7 @@ void ModuleWidget::draw(const widget::DrawContext &ctx) { | |||||
// bndLabel(ctx.vg, 0, 0, INFINITY, INFINITY, -1, debugText.c_str()); | // bndLabel(ctx.vg, 0, 0, INFINITY, INFINITY, -1, debugText.c_str()); | ||||
// } | // } | ||||
// nvgResetScissor(ctx.vg); | |||||
nvgResetScissor(ctx.vg); | |||||
} | } | ||||
void ModuleWidget::drawShadow(const widget::DrawContext &ctx) { | void ModuleWidget::drawShadow(const widget::DrawContext &ctx) { | ||||
@@ -307,13 +313,11 @@ void ModuleWidget::setPanel(std::shared_ptr<Svg> svg) { | |||||
panel = NULL; | panel = NULL; | ||||
} | } | ||||
{ | |||||
SvgPanel *panel = new SvgPanel; | |||||
panel->setBackground(svg); | |||||
addChild(panel); | |||||
box.size = panel->box.size; | |||||
this->panel = panel; | |||||
} | |||||
SvgPanel *svgPanel = new SvgPanel; | |||||
svgPanel->setBackground(svg); | |||||
addChild(svgPanel); | |||||
box.size.x = svgPanel->box.size.x; | |||||
panel = svgPanel; | |||||
} | } | ||||
void ModuleWidget::addParam(ParamWidget *param) { | void ModuleWidget::addParam(ParamWidget *param) { | ||||
@@ -17,6 +17,14 @@ namespace rack { | |||||
namespace engine { | namespace engine { | ||||
void disableDenormals() { | |||||
// Set CPU to flush-to-zero (FTZ) and denormals-are-zero (DAZ) mode | |||||
// https://software.intel.com/en-us/node/682949 | |||||
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); | |||||
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); | |||||
} | |||||
/** Threads which obtain a VIPLock will cause wait() to block for other less important threads. | /** Threads which obtain a VIPLock will cause wait() to block for other less important threads. | ||||
This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. | This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. | ||||
*/ | */ | ||||
@@ -111,6 +119,7 @@ struct EngineWorker { | |||||
void run() { | void run() { | ||||
system::setThreadName("Engine worker"); | system::setThreadName("Engine worker"); | ||||
system::setThreadRealTime(); | system::setThreadRealTime(); | ||||
disableDenormals(); | |||||
while (running) { | while (running) { | ||||
step(); | step(); | ||||
} | } | ||||
@@ -179,7 +188,7 @@ Engine::~Engine() { | |||||
static void Engine_stepModules(Engine *engine, int threadId) { | static void Engine_stepModules(Engine *engine, int threadId) { | ||||
Engine::Internal *internal = engine->internal; | Engine::Internal *internal = engine->internal; | ||||
int threadCount = internal->threadCount; | |||||
// int threadCount = internal->threadCount; | |||||
int modulesLen = internal->modules.size(); | int modulesLen = internal->modules.size(); | ||||
// Step each module | // Step each module | ||||
@@ -259,11 +268,7 @@ static void Engine_run(Engine *engine) { | |||||
// Set up thread | // Set up thread | ||||
system::setThreadName("Engine"); | system::setThreadName("Engine"); | ||||
system::setThreadRealTime(); | system::setThreadRealTime(); | ||||
// Set CPU to flush-to-zero (FTZ) and denormals-are-zero (DAZ) mode | |||||
// https://software.intel.com/en-us/node/682949 | |||||
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); | |||||
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); | |||||
disableDenormals(); | |||||
// Every time the engine waits and locks a mutex, it steps this many frames | // Every time the engine waits and locks a mutex, it steps this many frames | ||||
const int mutexSteps = 64; | const int mutexSteps = 64; | ||||
@@ -116,6 +116,17 @@ static bool loadPlugin(std::string path) { | |||||
initCallback(plugin); | initCallback(plugin); | ||||
plugin->fromJson(rootJ); | plugin->fromJson(rootJ); | ||||
// Normalize tags | |||||
for (Model *model : plugin->models) { | |||||
std::set<std::string> normalizedTags; | |||||
for (const std::string &tag : model->tags) { | |||||
std::string normalizedTag = normalizeTag(tag); | |||||
if (!normalizedTag.empty()) | |||||
normalizedTags.insert(normalizedTag); | |||||
} | |||||
model->tags = normalizedTags; | |||||
} | |||||
// Check slug | // Check slug | ||||
if (!isSlugValid(plugin->slug)) { | if (!isSlugValid(plugin->slug)) { | ||||
WARN("Plugin slug \"%s\" is invalid", plugin->slug.c_str()); | WARN("Plugin slug \"%s\" is invalid", plugin->slug.c_str()); | ||||
@@ -305,7 +316,7 @@ static int extractZip(const char *filename, const char *path) { | |||||
return err; | return err; | ||||
} | } | ||||
static void extractPackages(std::string path) { | |||||
static void extractPackages(const std::string &path) { | |||||
std::string message; | std::string message; | ||||
for (std::string packagePath : system::listEntries(path)) { | for (std::string packagePath : system::listEntries(path)) { | ||||
@@ -374,7 +385,7 @@ void destroy() { | |||||
plugins.clear(); | plugins.clear(); | ||||
} | } | ||||
void logIn(std::string email, std::string password) { | |||||
void logIn(const std::string &email, const std::string &password) { | |||||
json_t *reqJ = json_object(); | json_t *reqJ = json_object(); | ||||
json_object_set(reqJ, "email", json_string(email.c_str())); | json_object_set(reqJ, "email", json_string(email.c_str())); | ||||
json_object_set(reqJ, "password", json_string(password.c_str())); | json_object_set(reqJ, "password", json_string(password.c_str())); | ||||
@@ -496,7 +507,7 @@ bool isLoggedIn() { | |||||
return settings.token != ""; | return settings.token != ""; | ||||
} | } | ||||
Plugin *getPlugin(std::string pluginSlug) { | |||||
Plugin *getPlugin(const std::string &pluginSlug) { | |||||
for (Plugin *plugin : plugins) { | for (Plugin *plugin : plugins) { | ||||
if (plugin->slug == pluginSlug) { | if (plugin->slug == pluginSlug) { | ||||
return plugin; | return plugin; | ||||
@@ -505,7 +516,7 @@ Plugin *getPlugin(std::string pluginSlug) { | |||||
return NULL; | return NULL; | ||||
} | } | ||||
Model *getModel(std::string pluginSlug, std::string modelSlug) { | |||||
Model *getModel(const std::string &pluginSlug, const std::string &modelSlug) { | |||||
Plugin *plugin = getPlugin(pluginSlug); | Plugin *plugin = getPlugin(pluginSlug); | ||||
if (!plugin) | if (!plugin) | ||||
return NULL; | return NULL; | ||||
@@ -599,21 +610,21 @@ const std::map<std::string, std::string> tagAliases = { | |||||
}; | }; | ||||
std::string getAllowedTag(std::string tag) { | |||||
tag = string::lowercase(tag); | |||||
std::string normalizeTag(const std::string &tag) { | |||||
std::string lowercaseTag = string::lowercase(tag); | |||||
// Transform aliases | // Transform aliases | ||||
auto it = tagAliases.find(tag); | |||||
auto it = tagAliases.find(lowercaseTag); | |||||
if (it != tagAliases.end()) | if (it != tagAliases.end()) | ||||
tag = it->second; | |||||
lowercaseTag = it->second; | |||||
// Find allowed tag | // Find allowed tag | ||||
for (std::string allowedTag : allowedTags) { | for (std::string allowedTag : allowedTags) { | ||||
if (tag == string::lowercase(allowedTag)) | |||||
if (lowercaseTag == string::lowercase(allowedTag)) | |||||
return allowedTag; | return allowedTag; | ||||
} | } | ||||
return ""; | return ""; | ||||
} | } | ||||
bool isSlugValid(std::string slug) { | |||||
bool isSlugValid(const std::string &slug) { | |||||
for (char c : slug) { | for (char c : slug) { | ||||
if (!(std::isalnum(c) || c == '-' || c == '_')) | if (!(std::isalnum(c) || c == '-' || c == '_')) | ||||
return false; | return false; | ||||
@@ -622,7 +633,7 @@ bool isSlugValid(std::string slug) { | |||||
} | } | ||||
std::list<Plugin*> plugins; | |||||
std::vector<Plugin*> plugins; | |||||
bool isDownloading = false; | bool isDownloading = false; | ||||
float downloadProgress = 0.f; | float downloadProgress = 0.f; | ||||
std::string downloadName; | std::string downloadName; | ||||
@@ -20,7 +20,7 @@ void Model::fromJson(json_t *rootJ) { | |||||
json_t *tagJ; | json_t *tagJ; | ||||
json_array_foreach(tagsJ, i, tagJ) { | json_array_foreach(tagsJ, i, tagJ) { | ||||
std::string tag = json_string_value(tagJ); | std::string tag = json_string_value(tagJ); | ||||
tags.push_back(tag); | |||||
tags.insert(tag); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,20 @@ | |||||
#include "ui/MarginLayout.hpp" | |||||
#include <vector> | |||||
namespace rack { | |||||
namespace ui { | |||||
void MarginLayout::step() { | |||||
widget::Widget::step(); | |||||
math::Rect childBox = box.zeroPos().grow(margin.neg()); | |||||
for (Widget *child : children) { | |||||
child->box = childBox; | |||||
} | |||||
} | |||||
} // namespace ui | |||||
} // namespace rack |
@@ -78,7 +78,9 @@ void MenuItem::doAction() { | |||||
return; | return; | ||||
widget::Widget *overlay = getAncestorOfType<MenuOverlay>(); | widget::Widget *overlay = getAncestorOfType<MenuOverlay>(); | ||||
overlay->requestedDelete = true; | |||||
if (overlay) { | |||||
overlay->requestedDelete = true; | |||||
} | |||||
} | } | ||||
@@ -8,9 +8,6 @@ namespace rack { | |||||
namespace ui { | namespace ui { | ||||
static const float SCROLLBAR_SENSITIVITY = 2.f; | |||||
ScrollBar::ScrollBar() { | ScrollBar::ScrollBar() { | ||||
box.size = math::Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | box.size = math::Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | ||||
} | } | ||||
@@ -26,12 +23,14 @@ void ScrollBar::onDragStart(const event::DragStart &e) { | |||||
} | } | ||||
void ScrollBar::onDragMove(const event::DragMove &e) { | void ScrollBar::onDragMove(const event::DragMove &e) { | ||||
const float sensitivity = 1.f; | |||||
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 += SCROLLBAR_SENSITIVITY * e.mouseDelta.x; | |||||
scrollWidget->offset.x += sensitivity * e.mouseDelta.x; | |||||
else | else | ||||
scrollWidget->offset.y += SCROLLBAR_SENSITIVITY * e.mouseDelta.y; | |||||
scrollWidget->offset.y += sensitivity * e.mouseDelta.y; | |||||
} | } | ||||
void ScrollBar::onDragEnd(const event::DragEnd &e) { | void ScrollBar::onDragEnd(const event::DragEnd &e) { | ||||
@@ -28,7 +28,7 @@ void ScrollWidget::scrollTo(math::Rect r) { | |||||
} | } | ||||
void ScrollWidget::draw(const widget::DrawContext &ctx) { | void ScrollWidget::draw(const widget::DrawContext &ctx) { | ||||
nvgScissor(ctx.vg, 0, 0, box.size.x, box.size.y); | |||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
widget::Widget::draw(ctx); | widget::Widget::draw(ctx); | ||||
nvgResetScissor(ctx.vg); | nvgResetScissor(ctx.vg); | ||||
} | } | ||||
@@ -6,8 +6,9 @@ namespace rack { | |||||
namespace ui { | namespace ui { | ||||
#define X(_v) (orientation == HORIZONTAL_ORIENTATION ? (_v).x : (_v).y) | |||||
#define Y(_v) (orientation == HORIZONTAL_ORIENTATION ? (_v).y : (_v).x) | |||||
#define X(v) (orientation == HORIZONTAL_ORIENTATION ? (v).x : (v).y) | |||||
#define Y(v) (orientation == HORIZONTAL_ORIENTATION ? (v).y : (v).x) | |||||
void SequentialLayout::step() { | void SequentialLayout::step() { | ||||
widget::Widget::step(); | widget::Widget::step(); | ||||
@@ -9,7 +9,7 @@ TextField::TextField() { | |||||
} | } | ||||
void TextField::draw(const widget::DrawContext &ctx) { | void TextField::draw(const widget::DrawContext &ctx) { | ||||
nvgScissor(ctx.vg, 0, 0, box.size.x, box.size.y); | |||||
nvgScissor(ctx.vg, RECT_ARGS(ctx.clipBox)); | |||||
BNDwidgetState state; | BNDwidgetState state; | ||||
if (this == APP->event->selectedWidget) | if (this == APP->event->selectedWidget) | ||||
@@ -180,7 +180,6 @@ void TextField::onSelectKey(const event::SelectKey &e) { | |||||
} | } | ||||
} | } | ||||
/** Inserts text at the cursor, replacing the selection if necessary */ | |||||
void TextField::insertText(std::string text) { | void TextField::insertText(std::string text) { | ||||
if (cursor != selection) { | if (cursor != selection) { | ||||
int begin = std::min(cursor, selection); | int begin = std::min(cursor, selection); | ||||
@@ -194,12 +193,15 @@ void TextField::insertText(std::string text) { | |||||
onChange(eChange); | onChange(eChange); | ||||
} | } | ||||
/** Replaces the entire text */ | |||||
void TextField::setText(std::string text) { | void TextField::setText(std::string text) { | ||||
bool changed = (text != this->text); | |||||
this->text = text; | this->text = text; | ||||
selection = cursor = text.size(); | selection = cursor = text.size(); | ||||
event::Change eChange; | |||||
onChange(eChange); | |||||
if (changed) { | |||||
// event::Change | |||||
event::Change eChange; | |||||
onChange(eChange); | |||||
} | |||||
} | } | ||||
void TextField::selectAll() { | void TextField::selectAll() { | ||||
@@ -28,6 +28,28 @@ void Widget::setSize(math::Vec size) { | |||||
onResize(eResize); | onResize(eResize); | ||||
} | } | ||||
void Widget::show() { | |||||
if (visible) | |||||
return; | |||||
visible = true; | |||||
// event::Show | |||||
event::Show eShow; | |||||
onShow(eShow); | |||||
} | |||||
void Widget::hide() { | |||||
if (!visible) | |||||
return; | |||||
visible = false; | |||||
// event::Hide | |||||
event::Hide eHide; | |||||
onHide(eHide); | |||||
} | |||||
void Widget::requestDelete() { | |||||
requestedDelete = true; | |||||
} | |||||
math::Rect Widget::getChildrenBoundingBox() { | math::Rect Widget::getChildrenBoundingBox() { | ||||
math::Vec min = math::Vec(INFINITY, INFINITY); | math::Vec min = math::Vec(INFINITY, INFINITY); | ||||
math::Vec max = math::Vec(-INFINITY, -INFINITY); | math::Vec max = math::Vec(-INFINITY, -INFINITY); | ||||