@@ -312,6 +312,7 @@ struct SVGScrew : FramebufferWidget { | |||
struct Toolbar : OpaqueWidget { | |||
Slider *wireOpacitySlider; | |||
Slider *wireTensionSlider; | |||
Slider *zoomSlider; | |||
RadioButton *cpuUsageButton; | |||
RadioButton *plugLightButton; | |||
@@ -17,9 +17,12 @@ namespace rack { | |||
extern GLFWwindow *gWindow; | |||
extern NVGcontext *gVg; | |||
extern NVGcontext *gFramebufferVg; | |||
extern std::shared_ptr<Font> gGuiFont; | |||
extern float gPixelRatio; | |||
extern bool gAllowCursorLock; | |||
extern int gGuiFrame; | |||
extern Vec gMousePos; | |||
void guiInit(); | |||
@@ -191,6 +191,12 @@ struct Vec { | |||
Vec round() { | |||
return Vec(roundf(x), roundf(y)); | |||
} | |||
Vec floor() { | |||
return Vec(floorf(x), floorf(y)); | |||
} | |||
Vec ceil() { | |||
return Vec(ceilf(x), ceilf(y)); | |||
} | |||
bool isZero() { | |||
return x == 0.0 && y == 0.0; | |||
} | |||
@@ -216,10 +216,6 @@ Events are not passed to the underlying scene. | |||
struct FramebufferWidget : virtual Widget { | |||
/** Set this to true to re-render the children to the framebuffer in the next step() override */ | |||
bool dirty = true; | |||
/** A margin in pixels around the children in the framebuffer | |||
This prevents cutting the rendered SVG off on the box edges. | |||
*/ | |||
float oversample = 2.0; | |||
/** The root object in the framebuffer scene | |||
The FramebufferWidget owns the pointer | |||
*/ | |||
@@ -228,7 +224,6 @@ struct FramebufferWidget : virtual Widget { | |||
FramebufferWidget(); | |||
~FramebufferWidget(); | |||
void step() override; | |||
void draw(NVGcontext *vg) override; | |||
int getImageHandle(); | |||
}; | |||
@@ -354,6 +349,7 @@ struct Slider : OpaqueWidget, QuantityWidget { | |||
void onDragStart() override; | |||
void onDragMove(Vec mouseRel) override; | |||
void onDragEnd() override; | |||
void onMouseDownOpaque(int button) override; | |||
}; | |||
/** Parent must be a ScrollWidget */ | |||
@@ -439,12 +435,10 @@ struct Scene : OpaqueWidget { | |||
// globals | |||
//////////////////// | |||
extern Vec gMousePos; | |||
extern Widget *gHoveredWidget; | |||
extern Widget *gDraggedWidget; | |||
extern Widget *gDragHoveredWidget; | |||
extern Widget *gFocusedWidget; | |||
extern int gGuiFrame; | |||
extern Scene *gScene; | |||
@@ -31,7 +31,6 @@ RackScene::RackScene() { | |||
scrollWidget = new RackScrollWidget(); | |||
{ | |||
zoomWidget = new ZoomWidget(); | |||
zoomWidget->zoom = 1.0; | |||
{ | |||
assert(!gRackWidget); | |||
gRackWidget = new RackWidget(); | |||
@@ -63,6 +62,9 @@ void RackScene::step() { | |||
.plus(Vec(500, 500)) | |||
.div(zoomWidget->zoom); | |||
// Set zoom from the toolbar's zoom slider | |||
zoomWidget->zoom = gToolbar->zoomSlider->value / 100.0; | |||
Scene::step(); | |||
zoomWidget->box.size = gRackWidget->box.size.mult(zoomWidget->zoom); | |||
@@ -1,4 +1,5 @@ | |||
#include "app.hpp" | |||
#include "gui.hpp" | |||
namespace rack { | |||
@@ -189,9 +189,9 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
// version | |||
json_t *versionJ = json_object_get(rootJ, "version"); | |||
if (versionJ) { | |||
const char *version = json_string_value(versionJ); | |||
if (gApplicationVersion != version) | |||
message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version, gApplicationVersion.c_str()); | |||
std::string version = json_string_value(versionJ); | |||
if (!version.empty() && gApplicationVersion != version) | |||
message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version.c_str(), gApplicationVersion.empty() ? "dev" : gApplicationVersion.c_str()); | |||
} | |||
// modules | |||
@@ -404,6 +404,9 @@ struct AddManufacturerMenuItem : MenuItem { | |||
for (Model *model : models) { | |||
AddModuleMenuItem *item = new AddModuleMenuItem(); | |||
item->text = model->name; | |||
// item->rightText = model->plugin->slug; | |||
// if (!model->plugin->version.empty()) | |||
// item->rightText += " v" + model->plugin->version; | |||
item->model = model; | |||
item->modulePos = modulePos; | |||
menu->pushChild(item); | |||
@@ -139,13 +139,26 @@ Toolbar::Toolbar() { | |||
wireTensionSlider->box.pos = Vec(xPos, margin); | |||
wireTensionSlider->box.size.x = 150; | |||
wireTensionSlider->label = "Cable tension"; | |||
// wireTensionSlider->unit = ""; | |||
wireTensionSlider->unit = ""; | |||
wireTensionSlider->setLimits(0.0, 1.0); | |||
wireTensionSlider->setDefaultValue(0.5); | |||
addChild(wireTensionSlider); | |||
xPos += wireTensionSlider->box.size.x; | |||
} | |||
xPos += margin; | |||
{ | |||
zoomSlider = new Slider(); | |||
zoomSlider->box.pos = Vec(xPos, margin); | |||
zoomSlider->box.size.x = 150; | |||
zoomSlider->label = "Zoom"; | |||
zoomSlider->unit = "%"; | |||
zoomSlider->setLimits(50.0, 200.0); | |||
zoomSlider->setDefaultValue(100.0); | |||
addChild(zoomSlider); | |||
xPos += zoomSlider->box.size.x; | |||
} | |||
xPos += margin; | |||
{ | |||
plugLightButton = new RadioButton(); | |||
@@ -1,6 +1,7 @@ | |||
#include "app.hpp" | |||
#include "engine.hpp" | |||
#include "components.hpp" | |||
#include "gui.hpp" | |||
namespace rack { | |||
@@ -29,9 +29,12 @@ namespace rack { | |||
GLFWwindow *gWindow = NULL; | |||
NVGcontext *gVg = NULL; | |||
NVGcontext *gFramebufferVg = NULL; | |||
std::shared_ptr<Font> gGuiFont; | |||
float gPixelRatio = 0.0; | |||
bool gAllowCursorLock = true; | |||
int gGuiFrame; | |||
Vec gMousePos; | |||
void windowSizeCallback(GLFWwindow* window, int width, int height) { | |||
@@ -220,15 +223,15 @@ void renderGui() { | |||
gPixelRatio = (float)width / windowWidth; | |||
// Update and render | |||
glViewport(0, 0, width, height); | |||
glClearColor(0.0, 0.0, 0.0, 1.0); | |||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||
nvgBeginFrame(gVg, width, height, gPixelRatio); | |||
nvgReset(gVg); | |||
nvgScale(gVg, gPixelRatio, gPixelRatio); | |||
gScene->draw(gVg); | |||
glViewport(0, 0, width, height); | |||
glClearColor(0.0, 0.0, 0.0, 1.0); | |||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||
nvgEndFrame(gVg); | |||
glfwSwapBuffers(gWindow); | |||
} | |||
@@ -288,6 +291,9 @@ void guiInit() { | |||
// gVg = nvgCreateGL3(NVG_ANTIALIAS); | |||
assert(gVg); | |||
gFramebufferVg = nvgCreateGL2(NVG_ANTIALIAS); | |||
assert(gFramebufferVg); | |||
// Set up Blendish | |||
gGuiFont = Font::load(assetGlobal("res/DejaVuSans.ttf")); | |||
bndSetFont(gGuiFont->handle); | |||
@@ -298,6 +304,7 @@ void guiDestroy() { | |||
gGuiFont.reset(); | |||
nvgDeleteGL2(gVg); | |||
// nvgDeleteGL3(gVg); | |||
nvgDeleteGL2(gFramebufferVg); | |||
glfwDestroyWindow(gWindow); | |||
glfwTerminate(); | |||
} | |||
@@ -27,6 +27,11 @@ static json_t *settingsToJson() { | |||
json_t *tensionJ = json_real(tension); | |||
json_object_set_new(rootJ, "wireTension", tensionJ); | |||
// zoom | |||
float zoom = gToolbar->zoomSlider->value; | |||
json_t *zoomJ = json_real(zoom); | |||
json_object_set_new(rootJ, "zoom", zoomJ); | |||
// allowCursorLock | |||
json_t *allowCursorLockJ = json_boolean(gAllowCursorLock); | |||
json_object_set_new(rootJ, "allowCursorLock", allowCursorLockJ); | |||
@@ -62,6 +67,11 @@ static void settingsFromJson(json_t *rootJ) { | |||
if (tensionJ) | |||
gToolbar->wireTensionSlider->value = json_number_value(tensionJ); | |||
// zoom | |||
json_t *zoomJ = json_object_get(rootJ, "zoom"); | |||
if (zoomJ) | |||
gToolbar->zoomSlider->value = json_number_value(zoomJ); | |||
// allowCursorLock | |||
json_t *allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); | |||
if (allowCursorLockJ) | |||
@@ -2,12 +2,10 @@ | |||
namespace rack { | |||
Vec gMousePos; | |||
Widget *gHoveredWidget = NULL; | |||
Widget *gDraggedWidget = NULL; | |||
Widget *gDragHoveredWidget = NULL; | |||
Widget *gFocusedWidget = NULL; | |||
int gGuiFrame; | |||
Scene *gScene = NULL; | |||
@@ -9,9 +9,16 @@ | |||
namespace rack { | |||
/** A margin in pixels around the children in the framebuffer | |||
This prevents cutting the rendered SVG off on the box edges. | |||
*/ | |||
static const float oversample = 2.0; | |||
struct FramebufferWidget::Internal { | |||
NVGLUframebuffer *fb = NULL; | |||
Rect box; | |||
Vec lastS; | |||
~Internal() { | |||
setFramebuffer(NULL); | |||
@@ -32,15 +39,30 @@ FramebufferWidget::~FramebufferWidget() { | |||
delete internal; | |||
} | |||
void FramebufferWidget::step() { | |||
// Step children before rendering | |||
Widget::step(); | |||
void FramebufferWidget::draw(NVGcontext *vg) { | |||
// Bypass framebuffer rendering entirely | |||
// Widget::draw(vg); | |||
// return; | |||
// Get world transform | |||
float xform[6]; | |||
nvgCurrentTransform(vg, xform); | |||
// Skew is not supported | |||
assert(fabsf(xform[1]) < 1e-6); | |||
assert(fabsf(xform[2]) < 1e-6); | |||
Vec s = Vec(xform[0], xform[3]); | |||
Vec b = Vec(xform[4], xform[5]); | |||
// Check if scale has changed | |||
if (s.x != internal->lastS.x || s.y != internal->lastS.y) { | |||
dirty = true; | |||
} | |||
internal->lastS = s; | |||
// Render the scene to the framebuffer if dirty | |||
// Render to framebuffer | |||
if (dirty) { | |||
internal->box.pos = Vec(0, 0); | |||
internal->box.size = box.size; | |||
internal->box.size = Vec(ceilf(internal->box.size.x), ceilf(internal->box.size.y)); | |||
internal->box.size = box.size.mult(s).ceil().plus(Vec(1, 1)); | |||
Vec fbSize = internal->box.size.mult(gPixelRatio * oversample); | |||
// assert(fbSize.isFinite()); | |||
@@ -50,6 +72,7 @@ void FramebufferWidget::step() { | |||
// Delete old one first to free up GPU memory | |||
internal->setFramebuffer(NULL); | |||
// Create a framebuffer from the main nanovg context. We will draw to this in the secondary nanovg context. | |||
NVGLUframebuffer *fb = nvgluCreateFramebuffer(gVg, fbSize.x, fbSize.y, NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); | |||
if (!fb) | |||
return; | |||
@@ -59,49 +82,44 @@ void FramebufferWidget::step() { | |||
glViewport(0.0, 0.0, fbSize.x, fbSize.y); | |||
glClearColor(0.0, 0.0, 0.0, 0.0); | |||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||
nvgBeginFrame(gVg, fbSize.x, fbSize.y, gPixelRatio * oversample); | |||
nvgScale(gVg, gPixelRatio * oversample, gPixelRatio * oversample); | |||
Widget::draw(gVg); | |||
nvgBeginFrame(gFramebufferVg, fbSize.x, fbSize.y, gPixelRatio * oversample); | |||
nvgScale(gFramebufferVg, gPixelRatio * oversample, gPixelRatio * oversample); | |||
// Use local scaling | |||
Vec bFrac = Vec(fmodf(b.x, 1.0), fmodf(b.y, 1.0)); | |||
nvgTranslate(gFramebufferVg, bFrac.x, bFrac.y); | |||
nvgScale(gFramebufferVg, s.x, s.y); | |||
Widget::draw(gFramebufferVg); | |||
nvgEndFrame(gVg); | |||
nvgEndFrame(gFramebufferVg); | |||
nvgluBindFramebuffer(NULL); | |||
dirty = false; | |||
} | |||
} | |||
void FramebufferWidget::draw(NVGcontext *vg) { | |||
// { | |||
// float xform[6]; | |||
// nvgCurrentTransform(vg, xform); | |||
// printf("%f %f %f %f %f %f\n", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]); | |||
// nvgSave(vg); | |||
// nvgResetTransform(vg); | |||
// nvgTranslate(vg, xform[5], xform[6]); | |||
// nvgBeginPath(vg); | |||
// nvgRect(vg, 0, 0, 50, 50); | |||
// nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0)); | |||
// nvgFill(vg); | |||
// nvgRestore(vg); | |||
// } | |||
if (!internal->fb) { | |||
// Bypass framebuffer cache entirely | |||
// Widget::draw(vg); | |||
return; | |||
} | |||
// Draw framebuffer image | |||
// Draw framebuffer image, using world coordinates | |||
b = b.floor(); | |||
nvgSave(vg); | |||
nvgResetTransform(vg); | |||
nvgTranslate(vg, b.x, b.y); | |||
nvgBeginPath(vg); | |||
nvgRect(vg, internal->box.pos.x, internal->box.pos.y, internal->box.size.x, internal->box.size.y); | |||
NVGpaint paint = nvgImagePattern(vg, internal->box.pos.x, internal->box.pos.y, internal->box.size.x, internal->box.size.y, 0.0, internal->fb->image, 1.0); | |||
nvgFillPaint(vg, paint); | |||
nvgFill(vg); | |||
// For debugging bounding box of framebuffer image | |||
// nvgFillColor(vg, nvgRGBA(255, 0, 0, 64)); | |||
// nvgFill(vg); | |||
// For debugging the bounding box of the framebuffer | |||
// nvgStrokeWidth(vg, 2.0); | |||
// nvgStrokeColor(vg, nvgRGBA(255, 0, 0, 128)); | |||
// nvgStroke(vg); | |||
nvgRestore(vg); | |||
} | |||
int FramebufferWidget::getImageHandle() { | |||
@@ -1,4 +1,5 @@ | |||
#include "widgets.hpp" | |||
#include "gui.hpp" | |||
namespace rack { | |||
@@ -25,5 +25,11 @@ void Slider::onDragEnd() { | |||
guiCursorUnlock(); | |||
} | |||
void Slider::onMouseDownOpaque(int button) { | |||
if (button == 1) { | |||
setValue(defaultValue); | |||
} | |||
} | |||
} // namespace rack |
@@ -18,6 +18,10 @@ void TextField::draw(NVGcontext *vg) { | |||
state = BND_DEFAULT; | |||
bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); | |||
// Draw placeholder text | |||
if (text.empty() && state != BND_ACTIVE) { | |||
bndIconLabelCaret(vg, 0.0, 0.0, box.size.x, box.size.y, -1, bndGetTheme()->textFieldTheme.itemColor, 13, placeholder.c_str(), bndGetTheme()->textFieldTheme.itemColor, 0, -1); | |||
} | |||
} | |||
Widget *TextField::onMouseDown(Vec pos, int button) { | |||
@@ -1,4 +1,5 @@ | |||
#include "widgets.hpp" | |||
#include "gui.hpp" | |||
namespace rack { | |||