Add C++ implementation of custom knob widget and example

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
Christopher Arndt 2024-04-10 18:14:00 +02:00
parent e0003dded5
commit 2da51dd7cd
6 changed files with 533 additions and 8 deletions

View File

@ -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})

View File

@ -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=<repo path or URL>` 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
| | | |

238
fancyknob.cpp Normal file
View File

@ -0,0 +1,238 @@
#include <algorithm>
#include <nanogui/opengl.h>
#include <nanogui/theme.h>
#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)

85
fancyknob.hpp Normal file
View File

@ -0,0 +1,85 @@
#ifndef FANCYKNOB_H
#define FANCYKNOB_H
#include <nanogui/widget.h>
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<void(float)> callback() const { return m_callback; }
void set_callback(const std::function<void(float)>& 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<void(float)> m_callback;
};
NAMESPACE_END(nanogui)
#endif // FANCYKNOB_H

View File

@ -1 +0,0 @@
../../nanogui

144
nanogui_knobs.cpp Normal file
View File

@ -0,0 +1,144 @@
/*
src/nanogui_customwidget.cpp -- C++ version of an customwidget example application
*/
#include <nanogui/button.h>
#include <nanogui/label.h>
#include <nanogui/layout.h>
#include <nanogui/opengl.h>
#include <nanogui/screen.h>
#include <nanogui/textbox.h>
#include <nanogui/widget.h>
#include <nanogui/window.h>
#include <iostream>
#include <string>
#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<float>* entry = new FloatBox<float>(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<KnobsApplication> 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;
}