diff --git a/CMakeLists.txt b/CMakeLists.txt index 6898337..7317788 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,19 +1,33 @@ -cmake_minimum_required(VERSION 3.13) -project(nanogui_helloworld) +cmake_minimum_required(VERSION 3.14) +project(nanogui_experiments + VERSION 1.0 + DESCRIPTION "NanoGUI experiments" + LANGUAGES CXX +) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED YES) +set(NANOGUI_REPO "https://github.com/SpotlightKid/nanogui" CACHE STRING "nanoGUi repository URL or path") set(NANOGUI_BUILD_EXAMPLES OFF) set(NANOGUI_BUILD_PYTHON OFF) set(NANOGUI_BUILD_SHARED OFF) -add_subdirectory(lib/nanogui) +include(FetchContent) +FetchContent_Declare(nanogui + GIT_REPOSITORY ${NANOGUI_REPO} + GIT_TAG nanogui-experiments + GIT_SHALLOW true + SOURCE_DIR lib/nanogui +) +FetchContent_MakeAvailable(nanogui) + include_directories(lib/nanogui/include) include_directories(${NANOGUI_EXTRA_INCS}) add_definitions(${NANOGUI_EXTRA_DEFS}) - set_property(TARGET glfw glfw_objects nanogui PROPERTY FOLDER "dependencies") add_executable(nanogui_helloworld nanogui_helloworld.cpp) +target_compile_features(nanogui_helloworld PRIVATE cxx_std_17) target_link_libraries(nanogui_helloworld nanogui ${NANOGUI_EXTRA_LIBS}) +add_executable(nanogui_knobs nanogui_knobs.cpp fancyknob.cpp) +target_compile_features(nanogui_knobs PRIVATE cxx_std_17) +target_link_libraries(nanogui_knobs nanogui ${NANOGUI_EXTRA_LIBS}) diff --git a/README.md b/README.md index 5dc8b1d..19c1ae5 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,67 @@ ![NanoGUI Knobs example app](nanogui_knobs.png) -## Quickstart +## Building + +### Python + +Set up virtual environment providing nanogui Python bindings: ```con python3 -m venv venv source venv/bin/activate (venv) pip install -r requirenments.txt +``` + +Run helloworld examples: + +```con (venv) python3 nanogui_helloworld.py +(venv) python3 nanogui_custowidget.py +``` + +### C++ + +Configure build with *CMake*: + +```con +cmake -B build -S . +``` + +(This will checkout the NanoGUI library from GitHub into the build directory. +You can use `-DNANOGUI_REPO=` to change from where Git will +retrieve the repository.) + +Build the examples: + +```con +cmake --build build +``` + +Run hellworld example: + +```con +./build/nanogui_helloworld ``` ## Knobs Example + +### Python + ```con (venv) python3 nanogui_knobs.py ``` + +### C++ + +```con +./build/nanogui_knobs +``` + + ### Key and Mouse Bindings | | | | diff --git a/fancyknob.cpp b/fancyknob.cpp new file mode 100644 index 0000000..7c592a3 --- /dev/null +++ b/fancyknob.cpp @@ -0,0 +1,238 @@ +#include + +#include +#include + +#include "fancyknob.hpp" + +NAMESPACE_BEGIN(nanogui) + +FancyKnob::FancyKnob(Widget* parent, int rad) + : Widget(parent) + , m_value(0.0f) + , m_min_val(0.0f) + , m_max_val(100.0f) + , m_increment(0.1f) + , m_fine_mode(false) + , m_scroll_speed(2.0f) + , m_gauge_width(0.125f) + , m_indicator_size(0.35f) +{ + // TBD: getters & setters for all colors + set_gauge_color(Color(255, 80, 80, 255)); + m_knob_color_1 = Color(86, 92, 95, 255); + m_knob_color_2 = Color(39, 42, 43, 255); + m_outline_color_1 = Color(190, 190, 190, 0); + m_outline_color_2 = Color(23, 23, 23, 255); + m_scroll_increment = (m_max_val - m_min_val) / 100.0 * m_scroll_speed; + m_drag_increment = (m_max_val - m_min_val) / 100.0; + set_cursor(Cursor::Hand); + Widget::set_size({ rad, rad }); +} + +const float FancyKnob::m_pi = std::acos(-1.0f); + +Vector2i FancyKnob::preferred_size(NVGcontext*) const +{ + return m_size; +} + +void FancyKnob::set_min_value(float value) +{ + m_min_val = value; + m_scroll_increment = (m_max_val - m_min_val) / 100.0 * m_scroll_speed; + m_drag_increment = (m_max_val - m_min_val) / 100.0; +} + +void FancyKnob::set_max_value(float value) +{ + m_max_val = value; + m_scroll_increment = (m_max_val - m_min_val) / 100.0 * m_scroll_speed; + m_drag_increment = (m_max_val - m_min_val) / 100.0; +} + +void FancyKnob::set_scroll_speed(float value) +{ + m_scroll_speed = value; + m_scroll_increment = (m_max_val - m_min_val) / 100.0 * m_scroll_speed; +} + +void FancyKnob::set_gauge_color(const Color& color) +{ + m_gauge_color = color; + NVGcolor bg = nvgLerpRGBA(Color(40, 40, 40, 255), m_gauge_color, 0.3f); + m_gauge_bg_color = Color(bg.r, bg.g, bg.b, bg.a); +} + +bool FancyKnob::adjust_value(float value, float incr) +{ + if (m_fine_mode) + incr = m_increment; + + float new_val = m_value + value * incr; + new_val = std::max(m_min_val, std::min(m_max_val, new_val)); + + if (new_val != m_value) { + m_value = new_val; + + if (m_callback) + m_callback(m_value); + + return true; + } + + return false; +} + +bool FancyKnob::scroll_event(const Vector2i& p, const Vector2f& rel) +{ + if (!m_enabled) + return false; + + adjust_value(rel[1], m_scroll_increment); + return true; +} + +bool FancyKnob::mouse_enter_event(const Vector2i& p, bool enter) +{ + if (enter) + request_focus(); + + set_focused(enter); + return true; +} + +bool FancyKnob::mouse_drag_event(const Vector2i& p, const Vector2i& rel, int /* button */, int /* modifiers */) +{ + if (!m_enabled) + return false; + + adjust_value((float)-rel[1], m_drag_increment); + + return true; +} + +bool FancyKnob::mouse_button_event(const Vector2i& /* p */, int button, bool down, int modifiers) +{ + if (!m_enabled) + return false; + + if (button == GLFW_MOUSE_BUTTON_1 && modifiers & GLFW_MOD_CONTROL && down) { + if (m_value != m_default) { + m_value = m_default; + + if (m_callback) + m_callback(m_value); + } + return true; + } + + return false; +} + +bool FancyKnob::keyboard_event(int key, int scancode, int action, int modifiers) +{ + if (key == GLFW_KEY_LEFT_SHIFT || key == GLFW_KEY_RIGHT_SHIFT) { + if (action == GLFW_PRESS) + m_fine_mode = true; + else if (action == GLFW_RELEASE) + m_fine_mode = false; + return true; + } else if (action == GLFW_PRESS || action == GLFW_REPEAT) { + if (key == GLFW_KEY_UP) { + adjust_value(1, m_drag_increment); + return true; + } else if (key == GLFW_KEY_DOWN) { + adjust_value(-1, m_drag_increment); + return true; + } else if (key == GLFW_KEY_PAGE_UP) { + adjust_value(10, m_drag_increment); + return true; + } else if (key == GLFW_KEY_PAGE_DOWN) { + adjust_value(-10, m_drag_increment); + return true; + } + } + + return false; +} + +void FancyKnob::draw(NVGcontext* ctx) +{ + float height = (float)m_size.y(); + float radius = height / 2.0; + float gauge_width = radius * m_gauge_width; + float margin = gauge_width / 2.0; + float percent_filled = (m_value - m_min_val) / (m_max_val - m_min_val); + float knob_diameter = (radius - gauge_width) * 2.0 - margin; + float indicator_length = radius * m_indicator_size; + float indicator_start = radius - margin - indicator_length; + + nvgSave(ctx); + nvgTranslate(ctx, m_pos.x(), m_pos.y()); + + // Gauge (background) + nvgBeginPath(ctx); + + nvgStrokeWidth(ctx, gauge_width); + nvgStrokeColor(ctx, m_gauge_bg_color); + nvgLineCap(ctx, NVG_ROUND); + nvgArc(ctx, radius, radius, radius - margin, 0.75 * m_pi, 0.25 * m_pi, NVG_CW); + nvgStroke(ctx); + + // Gauge (fill) + nvgBeginPath(ctx); + + nvgStrokeWidth(ctx, gauge_width); + nvgStrokeColor(ctx, m_gauge_color); + nvgLineCap(ctx, NVG_ROUND); + nvgArc( + ctx, + radius, + radius, + radius - margin, + 0.75 * m_pi, + (0.75 + 1.5 * percent_filled) * m_pi, + NVG_CW); + nvgStroke(ctx); + + // Knob + nvgBeginPath(ctx); + + nvgStrokeWidth(ctx, 2.0); + NVGpaint outline_paint = nvgLinearGradient( + ctx, + 0, + 0, + 0, + height - 10, + m_outline_color_1, + m_outline_color_2); + nvgStrokePaint(ctx, outline_paint); + + NVGpaint knob_paint = nvgLinearGradient( + ctx, radius, gauge_width, radius, knob_diameter, m_knob_color_1, m_knob_color_2); + nvgFillPaint(ctx, knob_paint); + + nvgCircle(ctx, radius, radius, knob_diameter / 2.0); + nvgFill(ctx); + nvgStroke(ctx); + + // Indicator + nvgBeginPath(ctx); + + nvgTranslate(ctx, radius, radius); + nvgRotate(ctx, (2.0 + ((percent_filled - 0.5) * 1.5)) * m_pi); + + nvgStrokeColor(ctx, m_gauge_color); + nvgStrokeWidth(ctx, gauge_width); + nvgLineCap(ctx, NVG_ROUND); + nvgMoveTo(ctx, 0, -indicator_start); + nvgLineTo(ctx, 0, -(indicator_start + indicator_length)); + nvgStroke(ctx); + + nvgRestore(ctx); + nvgClosePath(ctx); +} + +NAMESPACE_END(nanogui) diff --git a/fancyknob.hpp b/fancyknob.hpp new file mode 100644 index 0000000..cededde --- /dev/null +++ b/fancyknob.hpp @@ -0,0 +1,85 @@ +#ifndef FANCYKNOB_H +#define FANCYKNOB_H + +#include + +NAMESPACE_BEGIN(nanogui) + +class NANOGUI_EXPORT FancyKnob : public Widget { +public: + FancyKnob(Widget* parent, int rad = 80); + + float value() const { return m_value; } + void set_value(float value) + { + if (value != m_value) + m_value = value; + } + + float min_default() const { return m_default; } + void set_default(float value) { m_default = value; } + + float min_value() const { return m_min_val; } + void set_min_value(float value); + + float max_value() const { return m_max_val; } + void set_max_value(float value); + + float increment() const { return m_increment; } + void set_increment(float value) { m_increment = value; } + + float scroll_speed() const { return m_scroll_speed; } + void set_scroll_speed(float value); + + const Color& gauge_color() const { return m_gauge_color; } + void set_gauge_color(const Color& color); + + std::function callback() const { return m_callback; } + void set_callback(const std::function& callback) { m_callback = callback; } + + virtual Vector2i preferred_size(NVGcontext* ctx) const override; + virtual bool mouse_enter_event(const Vector2i& p, bool enter); + virtual bool mouse_drag_event(const Vector2i& p, const Vector2i& rel, int button, int modifiers) override; + virtual bool mouse_button_event(const Vector2i& p, int button, bool down, int modifiers) override; + virtual bool scroll_event(const Vector2i& p, const Vector2f& rel) override; + virtual bool keyboard_event(int key, int scancode, int action, int modifiers); + virtual void draw(NVGcontext* ctx) override; + +protected: + bool adjust_value(float value, float incr); + +private: + static const float m_pi; + + // colors + Color + m_gauge_color, + m_gauge_bg_color, + m_knob_color_1, + m_knob_color_2, + m_outline_color_1, + m_outline_color_2; + + // sizes, value and range + float + m_value, + m_min_val, + m_max_val, + m_default, + m_increment, + m_scroll_speed, + m_scroll_increment, + m_drag_increment, + /* value gauge width as ratio of knob radius */ + m_gauge_width, + /* value indicator line length as ratio of knob radius */ + m_indicator_size; + + // behaviour + bool m_fine_mode = false; + std::function m_callback; +}; + +NAMESPACE_END(nanogui) + +#endif // FANCYKNOB_H diff --git a/lib/nanogui b/lib/nanogui deleted file mode 120000 index 4183cf3..0000000 --- a/lib/nanogui +++ /dev/null @@ -1 +0,0 @@ -../../nanogui \ No newline at end of file diff --git a/nanogui_knobs.cpp b/nanogui_knobs.cpp new file mode 100644 index 0000000..15b6c87 --- /dev/null +++ b/nanogui_knobs.cpp @@ -0,0 +1,144 @@ +/* + src/nanogui_customwidget.cpp -- C++ version of an customwidget example application +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "fancyknob.hpp" + +using namespace nanogui; + +struct KnobSpec { + std::string name; + float default_value; + float min_value; + float max_value; + float increment; + std::string unit; + Vector4i color; +}; + +KnobSpec knobs[4] = { + { "Attack", 0.0, 0.0, 5.0, 0.01, "s", { 224, 128, 128, 255 } }, + { "Decay", 0.2, 0.0, 5.0, 0.01, "s", { 128, 224, 128, 255 } }, + { "Sustain", 100.0, 0.0, 100.0, 0.01, "%", { 128, 128, 224, 255 } }, + { "Release", 0.2, 0.0, 5.0, 0.01, "s", { 224, 224, 128, 255 } }, +}; + +class KnobsApplication : public Screen { +public: + KnobsApplication() + : Screen(Vector2i(452, 250), "NanoGUI Knobs") + { + inc_ref(); + set_background(Color(96, 96, 96, 255)); + + window = new Window(this, "Envelope"); + window->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 20, 20)); + resize_event(size()); + + for (int i = 0; i < 4; i++) { + Widget* box = new Widget(window); + box->set_layout(new BoxLayout(Orientation::Vertical, Alignment::Middle, 0, 10)); + + KnobSpec knob = knobs[i]; + FancyKnob* k = new FancyKnob(box); + k->set_default(knob.default_value); + k->set_value(knob.default_value); + k->set_min_value(knob.min_value); + k->set_max_value(knob.max_value); + k->set_increment(knob.increment); + k->set_gauge_color(Color(knob.color)); + + FloatBox* entry = new FloatBox(box); + entry->set_fixed_size({ 88, 24 }); + entry->set_font_size(20); + entry->set_editable(true); + entry->set_spinnable(true); + entry->set_value(k->value()); + entry->number_format("%02.2f"); + entry->set_min_value(k->min_value()); + entry->set_max_value(k->max_value()); + entry->set_value_increment(k->increment()); + entry->set_units(knob.unit); + entry->set_callback([k](float f) { k->set_value(f); }); + + k->set_callback([entry, knob](float f) { + entry->set_value(f); + std::cout << "'" << knob.name << "' value: " << f << std::endl; + }); + + Label* l = new Label(box, knob.name, "sans", 20); + } + + perform_layout(); + } + + virtual bool resize_event(const Vector2i& size) + { + window->set_fixed_size(size); + window->set_size(size); + window->center(); + window->perform_layout(nvg_context()); + return true; + } + + virtual bool keyboard_event(int key, int scancode, int action, int modifiers) + { + if (Screen::keyboard_event(key, scancode, action, modifiers)) + return true; + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { + set_visible(false); + return true; + } + return false; + } + + virtual void draw(NVGcontext* ctx) + { + /* Draw the user interface */ + Screen::draw(ctx); + } + +private: + Window* window; +}; + +int main(int /* argc */, char** /* argv */) +{ + try { + nanogui::init(); + + /* scoped variables */ { + ref app = new KnobsApplication(); + app->dec_ref(); + app->draw_all(); + app->set_visible(true); + nanogui::mainloop(1 / 60.f * 1000); + } + + nanogui::shutdown(); + } catch (const std::exception& e) { + std::string error_msg = std::string("Caught a fatal error: ") + std::string(e.what()); +#if defined(_WIN32) + MessageBoxA(nullptr, error_msg.c_str(), NULL, MB_ICONERROR | MB_OK); +#else + std::cerr << error_msg << std::endl; +#endif + return -1; + } catch (...) { + std::cerr << "Caught an unknown error!" << std::endl; + } + + return 0; +}