Signed-off-by: falkTX <falktx@falktx.com>pull/457/head
| @@ -48,6 +48,26 @@ endif | |||||
| endif | endif | ||||
| # --------------------------------------------------------------------------------------------------------------------- | |||||
| # Check for proper UI_TYPE parameter | |||||
| ifeq ($(UI_TYPE),) | |||||
| else ifeq ($(UI_TYPE),generic) | |||||
| else ifeq ($(UI_TYPE),external) | |||||
| else ifeq ($(UI_TYPE),cairo) | |||||
| else ifeq ($(UI_TYPE),opengl) | |||||
| else ifeq ($(UI_TYPE),opengl3) | |||||
| USE_OPENGL3 = true | |||||
| else ifeq ($(UI_TYPE),vulkan) | |||||
| else ifeq ($(UI_TYPE),webview) | |||||
| USE_WEB_VIEW = true | |||||
| else | |||||
| $(error unknown UI_TYPE $(UI_TYPE)) | |||||
| endif | |||||
| # --------------------------------------------------------------------------------------------------------------------- | |||||
| # Include DPF base setup | |||||
| include $(DPF_PATH)/Makefile.base.mk | include $(DPF_PATH)/Makefile.base.mk | ||||
| # --------------------------------------------------------------------------------------------------------------------- | # --------------------------------------------------------------------------------------------------------------------- | ||||
| @@ -251,11 +271,16 @@ HAVE_DGL = false | |||||
| endif | endif | ||||
| endif | endif | ||||
| ifeq ($(UI_TYPE),web) | |||||
| DGL_FLAGS += -DDGL_WEB -DHAVE_DGL | |||||
| ifeq ($(UI_TYPE),webview) | |||||
| DGL_FLAGS += -DDGL_EXTERNAL -DHAVE_DGL | |||||
| ifeq ($(HAVE_STUB),true) | |||||
| DGL_FLAGS += $(STUB_FLAGS) | |||||
| DGL_LIBS += $(STUB_LIBS) | |||||
| DGL_LIB = $(DGL_BUILD_DIR)/libdgl-stub.a | DGL_LIB = $(DGL_BUILD_DIR)/libdgl-stub.a | ||||
| HAVE_DGL = true | HAVE_DGL = true | ||||
| USE_WEB_VIEW = true | |||||
| else | |||||
| HAVE_DGL = false | |||||
| endif | |||||
| endif | endif | ||||
| ifeq ($(HAVE_DGL)$(LINUX)$(USE_WEB_VIEW),truetruetrue) | ifeq ($(HAVE_DGL)$(LINUX)$(USE_WEB_VIEW),truetruetrue) | ||||
| @@ -1,6 +1,6 @@ | |||||
| /* | /* | ||||
| * DISTRHO Plugin Framework (DPF) | * DISTRHO Plugin Framework (DPF) | ||||
| * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com> | |||||
| * Copyright (C) 2012-2024 Filipe Coelho <falktx@falktx.com> | |||||
| * | * | ||||
| * Permission to use, copy, modify, and/or distribute this software for any purpose with | * Permission to use, copy, modify, and/or distribute this software for any purpose with | ||||
| * or without fee is hereby granted, provided that the above copyright notice and this | * or without fee is hereby granted, provided that the above copyright notice and this | ||||
| @@ -192,8 +192,7 @@ public: | |||||
| This function does not block the event loop. | This function does not block the event loop. | ||||
| @note This is exactly the same API as provided by the Window class, | |||||
| but redeclared here so that non-embed/DGL based UIs can still use file browser related functions. | |||||
| @note This is exactly the same API as provided by the Window class, but redeclared here for convenience. | |||||
| */ | */ | ||||
| bool openFileBrowser(const DISTRHO_NAMESPACE::FileBrowserOptions& options = FileBrowserOptions()); | bool openFileBrowser(const DISTRHO_NAMESPACE::FileBrowserOptions& options = FileBrowserOptions()); | ||||
| #endif | #endif | ||||
| @@ -415,10 +415,10 @@ WebViewHandle webViewCreate(const char* const url, | |||||
| SetParent(hwnd, reinterpret_cast<HWND>(windowId)); | SetParent(hwnd, reinterpret_cast<HWND>(windowId)); | ||||
| SetWindowPos(hwnd, nullptr, | SetWindowPos(hwnd, nullptr, | ||||
| options.offset.x * scaleFactor, | |||||
| options.offset.y * scaleFactor, | |||||
| (initialWidth - options.offset.x) * scaleFactor, | |||||
| (initialHeight - options.offset.y) * scaleFactor, | |||||
| options.offset.x, | |||||
| options.offset.y, | |||||
| initialWidth - options.offset.x, | |||||
| initialHeight - options.offset.y, | |||||
| SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); | ||||
| ShowWindow(hwnd, SW_SHOW); | ShowWindow(hwnd, SW_SHOW); | ||||
| #endif | #endif | ||||
| @@ -448,8 +448,8 @@ WebViewHandle webViewCreate(const char* const url, | |||||
| const CGRect rect = CGRectMake(options.offset.x / scaleFactor, | const CGRect rect = CGRectMake(options.offset.x / scaleFactor, | ||||
| options.offset.y / scaleFactor, | options.offset.y / scaleFactor, | ||||
| initialWidth, | |||||
| initialHeight); | |||||
| initialWidth / scaleFactor, | |||||
| initialHeight / scaleFactor); | |||||
| WKWebView* const webview = [[WKWebView alloc] initWithFrame:rect | WKWebView* const webview = [[WKWebView alloc] initWithFrame:rect | ||||
| configuration:config]; | configuration:config]; | ||||
| @@ -84,7 +84,7 @@ struct WebViewOptions { | |||||
| This means it will draw on top of whatever is below it, | This means it will draw on top of whatever is below it, | ||||
| something to take into consideration if mixing regular widgets with web views. | something to take into consideration if mixing regular widgets with web views. | ||||
| Provided metrics must not have scale factor pre-applied. | |||||
| Provided metrics must have scale factor pre-applied. | |||||
| @p windowId: The native window id to attach this view to (X11 Window, HWND or NSView*) | @p windowId: The native window id to attach this view to (X11 Window, HWND or NSView*) | ||||
| @p scaleFactor: Scale factor in use | @p scaleFactor: Scale factor in use | ||||
| @@ -163,7 +163,7 @@ | |||||
| # error invalid build config: file browser requested but `USE_FILE_BROWSER` build option is not set | # error invalid build config: file browser requested but `USE_FILE_BROWSER` build option is not set | ||||
| #endif | #endif | ||||
| #if DISTRHO_UI_USE_WEB_VIEW && !defined(DGL_UI_USE_WEB_VIEW) | |||||
| #if DISTRHO_UI_USE_WEB_VIEW && !defined(DGL_USE_WEB_VIEW) | |||||
| # error invalid build config: web view requested but `USE_WEB_VIEW` build option is not set | # error invalid build config: web view requested but `USE_WEB_VIEW` build option is not set | ||||
| #endif | #endif | ||||
| @@ -82,11 +82,6 @@ END_NAMESPACE_DISTRHO | |||||
| START_NAMESPACE_DISTRHO | START_NAMESPACE_DISTRHO | ||||
| /* ------------------------------------------------------------------------------------------------------------ | |||||
| * Static data, see DistrhoUIInternal.hpp */ | |||||
| const char* g_nextBundlePath = nullptr; | |||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| * get global scale factor */ | * get global scale factor */ | ||||
| @@ -178,8 +173,8 @@ UI::PrivateData* UI::PrivateData::s_nextPrivateData = nullptr; | |||||
| PluginWindow& UI::PrivateData::createNextWindow(UI* const ui, uint width, uint height, const bool adjustForScaleFactor) | PluginWindow& UI::PrivateData::createNextWindow(UI* const ui, uint width, uint height, const bool adjustForScaleFactor) | ||||
| { | { | ||||
| UI::PrivateData* const pData = s_nextPrivateData; | |||||
| const double scaleFactor = d_isNotZero(pData->scaleFactor) ? pData->scaleFactor : getDesktopScaleFactor(pData->winId); | |||||
| UI::PrivateData* const uiData = s_nextPrivateData; | |||||
| const double scaleFactor = d_isNotZero(uiData->scaleFactor) ? uiData->scaleFactor : getDesktopScaleFactor(uiData->winId); | |||||
| if (adjustForScaleFactor && d_isNotZero(scaleFactor) && d_isNotEqual(scaleFactor, 1.0)) | if (adjustForScaleFactor && d_isNotZero(scaleFactor) && d_isNotEqual(scaleFactor, 1.0)) | ||||
| { | { | ||||
| @@ -188,15 +183,132 @@ PluginWindow& UI::PrivateData::createNextWindow(UI* const ui, uint width, uint h | |||||
| } | } | ||||
| d_stdout("createNextWindow %u %u %f %d", width, height, scaleFactor, adjustForScaleFactor); | d_stdout("createNextWindow %u %u %f %d", width, height, scaleFactor, adjustForScaleFactor); | ||||
| pData->window = new PluginWindow(ui, pData->app, pData->winId, width, height, scaleFactor); | |||||
| uiData->window = new PluginWindow(ui, uiData->app, uiData->winId, width, height, scaleFactor); | |||||
| if (uiData->callbacksPtr != nullptr) | |||||
| { | |||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| String path; | |||||
| if (uiData->bundlePath != nullptr) | |||||
| { | |||||
| path = getResourcePath(uiData->bundlePath); | |||||
| } | |||||
| else | |||||
| { | |||||
| path = getBinaryFilename(); | |||||
| path.truncate(path.rfind(DISTRHO_OS_SEP)); | |||||
| path += "/resources"; | |||||
| } | |||||
| // TODO convert win32 paths to web | |||||
| // TODO encode paths (e.g. %20 for space) | |||||
| WebViewOptions opts; | |||||
| opts.initialJS = "" | |||||
| "editParameter = function(index, started){ postMessage('editparam '+index+' '+(started ? 1 : 0)) };" | |||||
| "setParameterValue = function(index, value){ postMessage('setparam '+index+' '+value) };" | |||||
| #if DISTRHO_PLUGIN_WANT_STATE | |||||
| "setState = function(key, value){ postMessage('setstate '+key+' '+value) };" | |||||
| "requestStateFile = function(key){ postMessage('reqstatefile '+key) };" | |||||
| #endif | |||||
| #if DISTRHO_PLUGIN_WANT_MIDI_INPUT | |||||
| "sendNote = function(channel, note, velocity){ postMessage('sendnote '+channel+' '+note+' '+velocity) };" | |||||
| #endif | |||||
| ; | |||||
| opts.callback = webViewMessageCallback; | |||||
| opts.callbackPtr = uiData; | |||||
| uiData->webview = webViewCreate("file://" + path + "/index.html", uiData->winId, width, height, scaleFactor, opts); | |||||
| #endif | |||||
| } | |||||
| // If there are no callbacks, this is most likely a temporary window, so ignore idle callbacks | // If there are no callbacks, this is most likely a temporary window, so ignore idle callbacks | ||||
| if (pData->callbacksPtr == nullptr) | |||||
| pData->window->setIgnoreIdleCallbacks(); | |||||
| else | |||||
| { | |||||
| uiData->window->setIgnoreIdleCallbacks(); | |||||
| } | |||||
| return pData->window.getObject(); | |||||
| return uiData->window.getObject(); | |||||
| } | } | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| void UI::PrivateData::webViewMessageCallback(void* const arg, char* const msg) | |||||
| { | |||||
| UI::PrivateData* const uiData = static_cast<UI::PrivateData*>(arg); | |||||
| if (std::strncmp(msg, "setparam ", 9) == 0) | |||||
| { | |||||
| const char* const strindex = msg + 9; | |||||
| char* strvalue = nullptr; | |||||
| const ulong index = std::strtoul(strindex, &strvalue, 10); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(strvalue != nullptr && strindex != strvalue,); | |||||
| float value; | |||||
| { | |||||
| const ScopedSafeLocale ssl; | |||||
| value = std::atof(strvalue); | |||||
| } | |||||
| uiData->setParamCallback(index + uiData->parameterOffset, value); | |||||
| return; | |||||
| } | |||||
| if (std::strncmp(msg, "editparam ", 10) == 0) | |||||
| { | |||||
| const char* const strindex = msg + 10; | |||||
| char* strvalue = nullptr; | |||||
| const ulong index = std::strtoul(strindex, &strvalue, 10); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(strvalue != nullptr && strindex != strvalue,); | |||||
| const bool started = strvalue[0] != '0'; | |||||
| uiData->editParamCallback(index + uiData->parameterOffset, started); | |||||
| return; | |||||
| } | |||||
| #if DISTRHO_PLUGIN_WANT_STATE | |||||
| if (std::strncmp(msg, "setstate ", 9) == 0) | |||||
| { | |||||
| char* const key = msg + 9; | |||||
| char* const sep = std::strchr(key, ' '); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(sep != nullptr,); | |||||
| *sep = 0; | |||||
| char* const value = sep + 1; | |||||
| uiData->setStateCallback(key, value); | |||||
| return; | |||||
| } | |||||
| if (std::strncmp(msg, "reqstatefile ", 13) == 0) | |||||
| { | |||||
| const char* const key = msg + 13; | |||||
| uiData->fileRequestCallback(key); | |||||
| return; | |||||
| } | |||||
| #endif | |||||
| #if DISTRHO_PLUGIN_WANT_MIDI_INPUT | |||||
| if (std::strncmp(msg, "sendnote ", 9) == 0) | |||||
| { | |||||
| const char* const strchannel = msg + 9; | |||||
| char* strnote = nullptr; | |||||
| char* strvelocity = nullptr; | |||||
| char* end = nullptr; | |||||
| const ulong channel = std::strtoul(strchannel, &strnote, 10); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(strnote != nullptr && strchannel != strnote,); | |||||
| const ulong note = std::strtoul(strnote, &strvelocity, 10); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(strvelocity != nullptr && strchannel != strvelocity,); | |||||
| const ulong velocity = std::strtoul(strvelocity, &end, 10); | |||||
| DISTRHO_SAFE_ASSERT_RETURN(end != nullptr && strvelocity != end,); | |||||
| uiData->sendNoteCallback(channel, note, velocity); | |||||
| return; | |||||
| } | |||||
| #endif | |||||
| d_stderr("UI received unknown message '%s'", msg); | |||||
| } | |||||
| #endif | |||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| * UI */ | * UI */ | ||||
| @@ -238,6 +350,10 @@ UI::UI(const uint width, const uint height, const bool automaticallyScaleAndSetA | |||||
| UI::~UI() | UI::~UI() | ||||
| { | { | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| webViewDestroy(uiData->webview); | |||||
| #endif | |||||
| } | } | ||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| @@ -323,27 +439,91 @@ void* UI::getPluginInstancePointer() const noexcept | |||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| * DSP/Plugin Callbacks */ | * DSP/Plugin Callbacks */ | ||||
| void UI::parameterChanged(uint32_t, float) | |||||
| void UI::parameterChanged(const uint32_t index, const float value) | |||||
| { | { | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| { | |||||
| char msg[128]; | |||||
| { | |||||
| const ScopedSafeLocale ssl; | |||||
| std::snprintf(msg, sizeof(msg) - 1, | |||||
| "typeof(parameterChanged) === 'function' && parameterChanged(%u,%f)", index, value); | |||||
| } | |||||
| webViewEvaluateJS(uiData->webview, msg); | |||||
| } | |||||
| #else | |||||
| // unused | |||||
| (void)index; | |||||
| (void)value; | |||||
| #endif | |||||
| } | } | ||||
| #if DISTRHO_PLUGIN_WANT_PROGRAMS | #if DISTRHO_PLUGIN_WANT_PROGRAMS | ||||
| void UI::programLoaded(uint32_t) | |||||
| void UI::programLoaded(const uint32_t index) | |||||
| { | { | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| { | |||||
| char msg[128]; | |||||
| std::snprintf(msg, sizeof(msg) - 1, | |||||
| "typeof(programLoaded) === 'function' && programLoaded(%u)", index); | |||||
| webViewEvaluateJS(uiData->webview, msg); | |||||
| } | |||||
| #else | |||||
| // unused | |||||
| (void)index; | |||||
| #endif | |||||
| } | } | ||||
| #endif | #endif | ||||
| #if DISTRHO_PLUGIN_WANT_STATE | #if DISTRHO_PLUGIN_WANT_STATE | ||||
| void UI::stateChanged(const char*, const char*) | |||||
| void UI::stateChanged(const char* const key, const char* const value) | |||||
| { | { | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| { | |||||
| const size_t keylen = std::strlen(key); | |||||
| const size_t valuelen = std::strlen(value); | |||||
| const size_t msglen = keylen + valuelen + 60; | |||||
| if (char* const msg = static_cast<char*>(std::malloc(msglen))) | |||||
| { | |||||
| // TODO escape \\' | |||||
| std::snprintf(msg, sizeof(msglen) - 1, | |||||
| "typeof(stateChanged) === 'function' && stateChanged('%s','%s')", key, value); | |||||
| msg[msglen - 1] = '\0'; | |||||
| webViewEvaluateJS(uiData->webview, msg); | |||||
| std::free(msg); | |||||
| } | |||||
| } | |||||
| #else | |||||
| // unused | |||||
| (void)key; | |||||
| (void)value; | |||||
| #endif | |||||
| } | } | ||||
| #endif | #endif | ||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| * DSP/Plugin Callbacks (optional) */ | * DSP/Plugin Callbacks (optional) */ | ||||
| void UI::sampleRateChanged(double) | |||||
| void UI::sampleRateChanged(const double sampleRate) | |||||
| { | { | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| { | |||||
| char msg[128]; | |||||
| { | |||||
| const ScopedSafeLocale ssl; | |||||
| std::snprintf(msg, sizeof(msg) - 1, | |||||
| "typeof(sampleRateChanged) === 'function' && sampleRateChanged(%f)", sampleRate); | |||||
| } | |||||
| webViewEvaluateJS(uiData->webview, msg); | |||||
| } | |||||
| #else | |||||
| // unused | |||||
| (void)sampleRate; | |||||
| #endif | |||||
| } | } | ||||
| /* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
| @@ -21,11 +21,6 @@ | |||||
| START_NAMESPACE_DISTRHO | START_NAMESPACE_DISTRHO | ||||
| // ----------------------------------------------------------------------- | |||||
| // Static data, see DistrhoUI.cpp | |||||
| extern const char* g_nextBundlePath; | |||||
| // ----------------------------------------------------------------------- | // ----------------------------------------------------------------------- | ||||
| // UI exporter class | // UI exporter class | ||||
| @@ -75,12 +70,10 @@ public: | |||||
| uiData->setSizeCallbackFunc = setSizeCall; | uiData->setSizeCallbackFunc = setSizeCall; | ||||
| uiData->fileRequestCallbackFunc = fileRequestCall; | uiData->fileRequestCallbackFunc = fileRequestCall; | ||||
| g_nextBundlePath = bundlePath; | |||||
| UI::PrivateData::s_nextPrivateData = uiData; | UI::PrivateData::s_nextPrivateData = uiData; | ||||
| UI* const uiPtr = createUI(); | UI* const uiPtr = createUI(); | ||||
| g_nextBundlePath = nullptr; | |||||
| // enter context called in the PluginWindow constructor, see DistrhoUIPrivateData.hpp | // enter context called in the PluginWindow constructor, see DistrhoUIPrivateData.hpp | ||||
| uiData->window->leaveContext(); | uiData->window->leaveContext(); | ||||
| UI::PrivateData::s_nextPrivateData = nullptr; | UI::PrivateData::s_nextPrivateData = nullptr; | ||||
| @@ -201,12 +194,18 @@ public: | |||||
| uiData->window->focus(); | uiData->window->focus(); | ||||
| uiData->app.addIdleCallback(cb); | uiData->app.addIdleCallback(cb); | ||||
| uiData->app.exec(); | uiData->app.exec(); | ||||
| uiData->app.removeIdleCallback(cb); | |||||
| } | } | ||||
| void exec_idle() | void exec_idle() | ||||
| { | { | ||||
| DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr, ); | DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr, ); | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| webViewIdle(uiData->webview); | |||||
| #endif | |||||
| ui->uiIdle(); | ui->uiIdle(); | ||||
| uiData->app.repaintIfNeeeded(); | uiData->app.repaintIfNeeeded(); | ||||
| } | } | ||||
| @@ -223,6 +222,12 @@ public: | |||||
| DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr, false); | DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr, false); | ||||
| uiData->app.idle(); | uiData->app.idle(); | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| webViewIdle(uiData->webview); | |||||
| #endif | |||||
| ui->uiIdle(); | ui->uiIdle(); | ||||
| uiData->app.repaintIfNeeeded(); | uiData->app.repaintIfNeeeded(); | ||||
| return ! uiData->app.isQuitting(); | return ! uiData->app.isQuitting(); | ||||
| @@ -252,6 +257,12 @@ public: | |||||
| DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); | DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); | ||||
| uiData->app.triggerIdleCallbacks(); | uiData->app.triggerIdleCallbacks(); | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| if (uiData->webview != nullptr) | |||||
| webViewIdle(uiData->webview); | |||||
| #endif | |||||
| ui->uiIdle(); | ui->uiIdle(); | ||||
| uiData->app.repaintIfNeeeded(); | uiData->app.repaintIfNeeeded(); | ||||
| } | } | ||||
| @@ -32,6 +32,10 @@ | |||||
| # include <string> | # include <string> | ||||
| #endif | #endif | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| # include "extra/WebView.hpp" | |||||
| #endif | |||||
| #if defined(DISTRHO_PLUGIN_TARGET_JACK) || defined(DISTRHO_PLUGIN_TARGET_DSSI) | #if defined(DISTRHO_PLUGIN_TARGET_JACK) || defined(DISTRHO_PLUGIN_TARGET_DSSI) | ||||
| # define DISTRHO_UI_IS_STANDALONE 1 | # define DISTRHO_UI_IS_STANDALONE 1 | ||||
| #else | #else | ||||
| @@ -247,6 +251,9 @@ struct UI::PrivateData { | |||||
| // DGL | // DGL | ||||
| PluginApplication app; | PluginApplication app; | ||||
| ScopedPointer<PluginWindow> window; | ScopedPointer<PluginWindow> window; | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| WebViewHandle webview; | |||||
| #endif | |||||
| // DSP | // DSP | ||||
| double sampleRate; | double sampleRate; | ||||
| @@ -279,6 +286,9 @@ struct UI::PrivateData { | |||||
| PrivateData(const char* const appClassName) noexcept | PrivateData(const char* const appClassName) noexcept | ||||
| : app(appClassName), | : app(appClassName), | ||||
| window(nullptr), | window(nullptr), | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| webview(nullptr), | |||||
| #endif | |||||
| sampleRate(0), | sampleRate(0), | ||||
| parameterOffset(0), | parameterOffset(0), | ||||
| dspPtr(nullptr), | dspPtr(nullptr), | ||||
| @@ -359,10 +369,13 @@ struct UI::PrivateData { | |||||
| } | } | ||||
| // implemented below, after PluginWindow | // implemented below, after PluginWindow | ||||
| bool fileRequestCallback(const char* const key); | |||||
| bool fileRequestCallback(const char* key); | |||||
| static UI::PrivateData* s_nextPrivateData; | static UI::PrivateData* s_nextPrivateData; | ||||
| static PluginWindow& createNextWindow(UI* ui, uint width, uint height, bool adjustForScaleFactor); | static PluginWindow& createNextWindow(UI* ui, uint width, uint height, bool adjustForScaleFactor); | ||||
| #if DISTRHO_UI_USE_WEB_VIEW | |||||
| static void webViewMessageCallback(void* arg, char* msg); | |||||
| #endif | |||||
| }; | }; | ||||
| // ----------------------------------------------------------------------- | // ----------------------------------------------------------------------- | ||||
| @@ -0,0 +1,12 @@ | |||||
| # CMake file for DISTRHO Plugins # | |||||
| # ------------------------------ # | |||||
| dpf_add_plugin(d_meters | |||||
| TARGETS jack dssi lv2 vst2 vst3 clap | |||||
| FILES_DSP | |||||
| ExamplePluginMeters.cpp | |||||
| FILES_UI | |||||
| ExampleUIMeters.cpp) | |||||
| target_include_directories( | |||||
| d_meters PUBLIC ".") | |||||
| @@ -0,0 +1,43 @@ | |||||
| /* | |||||
| * DISTRHO Plugin Framework (DPF) | |||||
| * Copyright (C) 2012-2024 Filipe Coelho <falktx@falktx.com> | |||||
| * | |||||
| * Permission to use, copy, modify, and/or distribute this software for any purpose with | |||||
| * or without fee is hereby granted, provided that the above copyright notice and this | |||||
| * permission notice appear in all copies. | |||||
| * | |||||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD | |||||
| * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN | |||||
| * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL | |||||
| * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER | |||||
| * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |||||
| * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||||
| */ | |||||
| #ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED | |||||
| #define DISTRHO_PLUGIN_INFO_H_INCLUDED | |||||
| #define DISTRHO_PLUGIN_BRAND "DISTRHO" | |||||
| #define DISTRHO_PLUGIN_NAME "Web Meters" | |||||
| #define DISTRHO_PLUGIN_URI "http://distrho.sf.net/examples/WebMeters" | |||||
| #define DISTRHO_PLUGIN_CLAP_ID "studio.kx.distrho.examples.webmeters" | |||||
| #define DISTRHO_PLUGIN_BRAND_ID Dstr | |||||
| #define DISTRHO_PLUGIN_UNIQUE_ID wMtr | |||||
| #define DISTRHO_PLUGIN_HAS_UI 1 | |||||
| #define DISTRHO_PLUGIN_IS_RT_SAFE 1 | |||||
| #define DISTRHO_PLUGIN_NUM_INPUTS 2 | |||||
| #define DISTRHO_PLUGIN_NUM_OUTPUTS 2 | |||||
| #define DISTRHO_PLUGIN_WANT_STATE 1 | |||||
| #define DISTRHO_UI_FILE_BROWSER 0 | |||||
| #define DISTRHO_UI_USER_RESIZABLE 1 | |||||
| #define DISTRHO_UI_USE_WEB_VIEW 1 | |||||
| #define METER_COLOR_GREEN 0 | |||||
| #define METER_COLOR_BLUE 1 | |||||
| #define DISTRHO_UI_DEFAULT_WIDTH 100 | |||||
| #define DISTRHO_UI_DEFAULT_HEIGHT 500 | |||||
| #endif // DISTRHO_PLUGIN_INFO_H_INCLUDED | |||||
| @@ -0,0 +1,288 @@ | |||||
| /* | |||||
| * DISTRHO Plugin Framework (DPF) | |||||
| * Copyright (C) 2012-2024 Filipe Coelho <falktx@falktx.com> | |||||
| * | |||||
| * Permission to use, copy, modify, and/or distribute this software for any purpose with | |||||
| * or without fee is hereby granted, provided that the above copyright notice and this | |||||
| * permission notice appear in all copies. | |||||
| * | |||||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD | |||||
| * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN | |||||
| * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL | |||||
| * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER | |||||
| * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |||||
| * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||||
| */ | |||||
| #include "DistrhoPlugin.hpp" | |||||
| START_NAMESPACE_DISTRHO | |||||
| // ----------------------------------------------------------------------------------------------------------- | |||||
| /** | |||||
| Plugin to demonstrate parameter outputs using meters. | |||||
| */ | |||||
| class ExamplePluginMeters : public Plugin | |||||
| { | |||||
| public: | |||||
| ExamplePluginMeters() | |||||
| : Plugin(3, 0, 0), // 3 parameters, 0 programs, 0 states | |||||
| fColor(0.0f), | |||||
| fOutLeft(0.0f), | |||||
| fOutRight(0.0f), | |||||
| fNeedsReset(true) | |||||
| { | |||||
| } | |||||
| protected: | |||||
| /* -------------------------------------------------------------------------------------------------------- | |||||
| * Information */ | |||||
| /** | |||||
| Get the plugin label. | |||||
| A plugin label follows the same rules as Parameter::symbol, with the exception that it can start with numbers. | |||||
| */ | |||||
| const char* getLabel() const override | |||||
| { | |||||
| return "meters"; | |||||
| } | |||||
| /** | |||||
| Get an extensive comment/description about the plugin. | |||||
| */ | |||||
| const char* getDescription() const override | |||||
| { | |||||
| return "Plugin to demonstrate parameter outputs using meters."; | |||||
| } | |||||
| /** | |||||
| Get the plugin author/maker. | |||||
| */ | |||||
| const char* getMaker() const override | |||||
| { | |||||
| return "DISTRHO"; | |||||
| } | |||||
| /** | |||||
| Get the plugin homepage. | |||||
| */ | |||||
| const char* getHomePage() const override | |||||
| { | |||||
| return "https://github.com/DISTRHO/DPF"; | |||||
| } | |||||
| /** | |||||
| Get the plugin license name (a single line of text). | |||||
| For commercial plugins this should return some short copyright information. | |||||
| */ | |||||
| const char* getLicense() const override | |||||
| { | |||||
| return "ISC"; | |||||
| } | |||||
| /** | |||||
| Get the plugin version, in hexadecimal. | |||||
| */ | |||||
| uint32_t getVersion() const override | |||||
| { | |||||
| return d_version(1, 0, 0); | |||||
| } | |||||
| /* -------------------------------------------------------------------------------------------------------- | |||||
| * Init */ | |||||
| /** | |||||
| Initialize the audio port @a index.@n | |||||
| This function will be called once, shortly after the plugin is created. | |||||
| */ | |||||
| void initAudioPort(bool input, uint32_t index, AudioPort& port) override | |||||
| { | |||||
| // treat meter audio ports as stereo | |||||
| port.groupId = kPortGroupStereo; | |||||
| // everything else is as default | |||||
| Plugin::initAudioPort(input, index, port); | |||||
| } | |||||
| /** | |||||
| Initialize the parameter @a index.@n | |||||
| This function will be called once, shortly after the plugin is created. | |||||
| */ | |||||
| void initParameter(uint32_t index, Parameter& parameter) override | |||||
| { | |||||
| /** | |||||
| All parameters in this plugin have the same ranges. | |||||
| */ | |||||
| parameter.ranges.min = 0.0f; | |||||
| parameter.ranges.max = 1.0f; | |||||
| parameter.ranges.def = 0.0f; | |||||
| /** | |||||
| Set parameter data. | |||||
| */ | |||||
| switch (index) | |||||
| { | |||||
| case 0: | |||||
| parameter.hints = kParameterIsAutomatable|kParameterIsInteger; | |||||
| parameter.name = "color"; | |||||
| parameter.symbol = "color"; | |||||
| parameter.enumValues.count = 2; | |||||
| parameter.enumValues.restrictedMode = true; | |||||
| { | |||||
| ParameterEnumerationValue* const values = new ParameterEnumerationValue[2]; | |||||
| parameter.enumValues.values = values; | |||||
| values[0].label = "Green"; | |||||
| values[0].value = METER_COLOR_GREEN; | |||||
| values[1].label = "Blue"; | |||||
| values[1].value = METER_COLOR_BLUE; | |||||
| } | |||||
| break; | |||||
| case 1: | |||||
| parameter.hints = kParameterIsAutomatable|kParameterIsOutput; | |||||
| parameter.name = "out-left"; | |||||
| parameter.symbol = "out_left"; | |||||
| break; | |||||
| case 2: | |||||
| parameter.hints = kParameterIsAutomatable|kParameterIsOutput; | |||||
| parameter.name = "out-right"; | |||||
| parameter.symbol = "out_right"; | |||||
| break; | |||||
| } | |||||
| } | |||||
| /** | |||||
| Set a state key and default value. | |||||
| This function will be called once, shortly after the plugin is created. | |||||
| */ | |||||
| void initState(uint32_t, String&, String&) override | |||||
| { | |||||
| // we are using states but don't want them saved in the host | |||||
| } | |||||
| /* -------------------------------------------------------------------------------------------------------- | |||||
| * Internal data */ | |||||
| /** | |||||
| Get the current value of a parameter. | |||||
| */ | |||||
| float getParameterValue(uint32_t index) const override | |||||
| { | |||||
| switch (index) | |||||
| { | |||||
| case 0: return fColor; | |||||
| case 1: return fOutLeft; | |||||
| case 2: return fOutRight; | |||||
| } | |||||
| return 0.0f; | |||||
| } | |||||
| /** | |||||
| Change a parameter value. | |||||
| */ | |||||
| void setParameterValue(uint32_t index, float value) override | |||||
| { | |||||
| // this is only called for input paramters, and we only have one of those. | |||||
| if (index != 0) return; | |||||
| fColor = value; | |||||
| } | |||||
| /** | |||||
| Change an internal state. | |||||
| */ | |||||
| void setState(const char* key, const char*) override | |||||
| { | |||||
| if (std::strcmp(key, "reset") != 0) | |||||
| return; | |||||
| fNeedsReset = true; | |||||
| } | |||||
| /* -------------------------------------------------------------------------------------------------------- | |||||
| * Process */ | |||||
| /** | |||||
| Run/process function for plugins without MIDI input. | |||||
| */ | |||||
| void run(const float** inputs, float** outputs, uint32_t frames) override | |||||
| { | |||||
| float tmp; | |||||
| float tmpLeft = 0.0f; | |||||
| float tmpRight = 0.0f; | |||||
| for (uint32_t i=0; i<frames; ++i) | |||||
| { | |||||
| // left | |||||
| tmp = std::abs(inputs[0][i]); | |||||
| if (tmp > tmpLeft) | |||||
| tmpLeft = tmp; | |||||
| // right | |||||
| tmp = std::abs(inputs[1][i]); | |||||
| if (tmp > tmpRight) | |||||
| tmpRight = tmp; | |||||
| } | |||||
| if (tmpLeft > 1.0f) | |||||
| tmpLeft = 1.0f; | |||||
| if (tmpRight > 1.0f) | |||||
| tmpRight = 1.0f; | |||||
| if (fNeedsReset) | |||||
| { | |||||
| fOutLeft = tmpLeft; | |||||
| fOutRight = tmpRight; | |||||
| fNeedsReset = false; | |||||
| } | |||||
| else | |||||
| { | |||||
| if (tmpLeft > fOutLeft) | |||||
| fOutLeft = tmpLeft; | |||||
| if (tmpRight > fOutRight) | |||||
| fOutRight = tmpRight; | |||||
| } | |||||
| // copy inputs over outputs if needed | |||||
| if (outputs[0] != inputs[0]) | |||||
| std::memcpy(outputs[0], inputs[0], sizeof(float)*frames); | |||||
| if (outputs[1] != inputs[1]) | |||||
| std::memcpy(outputs[1], inputs[1], sizeof(float)*frames); | |||||
| } | |||||
| // ------------------------------------------------------------------------------------------------------- | |||||
| private: | |||||
| /** | |||||
| Parameters. | |||||
| */ | |||||
| float fColor, fOutLeft, fOutRight; | |||||
| /** | |||||
| Boolean used to reset meter values. | |||||
| The UI will send a "reset" message which sets this as true. | |||||
| */ | |||||
| volatile bool fNeedsReset; | |||||
| /** | |||||
| Set our plugin class as non-copyable and add a leak detector just in case. | |||||
| */ | |||||
| DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ExamplePluginMeters) | |||||
| }; | |||||
| /* ------------------------------------------------------------------------------------------------------------ | |||||
| * Plugin entry point, called by DPF to create a new plugin instance. */ | |||||
| Plugin* createPlugin() | |||||
| { | |||||
| return new ExamplePluginMeters(); | |||||
| } | |||||
| // ----------------------------------------------------------------------------------------------------------- | |||||
| END_NAMESPACE_DISTRHO | |||||
| @@ -0,0 +1,61 @@ | |||||
| /* | |||||
| * DISTRHO Plugin Framework (DPF) | |||||
| * Copyright (C) 2012-2024 Filipe Coelho <falktx@falktx.com> | |||||
| * | |||||
| * Permission to use, copy, modify, and/or distribute this software for any purpose with | |||||
| * or without fee is hereby granted, provided that the above copyright notice and this | |||||
| * permission notice appear in all copies. | |||||
| * | |||||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD | |||||
| * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN | |||||
| * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL | |||||
| * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER | |||||
| * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |||||
| * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||||
| */ | |||||
| #include "DistrhoUI.hpp" | |||||
| START_NAMESPACE_DISTRHO | |||||
| // ----------------------------------------------------------------------------------------------------------- | |||||
| class ExampleUIMeters : public UI | |||||
| { | |||||
| public: | |||||
| ExampleUIMeters() | |||||
| : UI() | |||||
| { | |||||
| /* | |||||
| const double scaleFactor = getScaleFactor(); | |||||
| if (d_isNotEqual(scaleFactor, 1.0)) | |||||
| { | |||||
| setGeometryConstraints(DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor); | |||||
| setSize(DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor); | |||||
| } | |||||
| else | |||||
| { | |||||
| setGeometryConstraints(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT); | |||||
| } | |||||
| */ | |||||
| } | |||||
| private: | |||||
| /** | |||||
| Set our UI class as non-copyable and add a leak detector just in case. | |||||
| */ | |||||
| DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ExampleUIMeters) | |||||
| }; | |||||
| /* ------------------------------------------------------------------------------------------------------------ | |||||
| * UI entry point, called by DPF to create a new UI instance. */ | |||||
| UI* createUI() | |||||
| { | |||||
| return new ExampleUIMeters(); | |||||
| } | |||||
| // ----------------------------------------------------------------------------------------------------------- | |||||
| END_NAMESPACE_DISTRHO | |||||
| @@ -0,0 +1,68 @@ | |||||
| #!/usr/bin/make -f | |||||
| # Makefile for DISTRHO Plugins # | |||||
| # ---------------------------- # | |||||
| # Created by falkTX | |||||
| # | |||||
| # -------------------------------------------------------------- | |||||
| # Project name, used for binaries | |||||
| NAME = d_web | |||||
| # -------------------------------------------------------------- | |||||
| # Files to build | |||||
| FILES_DSP = \ | |||||
| ExamplePluginWebMeters.cpp | |||||
| FILES_UI = \ | |||||
| ExampleUIWebMeters.cpp | |||||
| # -------------------------------------------------------------- | |||||
| # Do some magic | |||||
| # MACOS_NO_DEAD_STRIP = true | |||||
| UI_TYPE = webview | |||||
| include ../../Makefile.plugins.mk | |||||
| # BUILD_CXX_FLAGS += $(shell pkg-config --cflags gtk+-3.0 webkit2gtk-4.0) | |||||
| BUILD_CXX_FLAGS += -Wno-unused-parameter -Wno-unused-result | |||||
| BUILD_CXX_FLAGS += -Wno-deprecated-declarations | |||||
| # LINK_FLAGS += $(shell pkg-config --libs gtk+-3.0 webkit2gtk-4.0) | |||||
| # -------------------------------------------------------------- | |||||
| # Enable all possible plugin types | |||||
| ifeq ($(HAVE_OPENGL),true) | |||||
| TARGETS += jack | |||||
| ifneq ($(MACOS_OR_WINDOWS),true) | |||||
| ifeq ($(HAVE_LIBLO),true) | |||||
| TARGETS += dssi | |||||
| endif # HAVE_LIBLO | |||||
| endif # MACOS_OR_WINDOWS | |||||
| TARGETS += lv2_sep | |||||
| TARGETS += vst2 | |||||
| TARGETS += vst3 | |||||
| TARGETS += clap | |||||
| TARGETS += au | |||||
| endif # HAVE_OPENGL | |||||
| ifeq ($(MACOS_APP_BUNDLE),true) | |||||
| jackfiles += $(TARGET_DIR)/$(NAME).app/Contents/Resources/index.html | |||||
| else | |||||
| jackfiles += $(TARGET_DIR)/resources/index.html | |||||
| endif | |||||
| vst3files += $(TARGET_DIR)/$(NAME).vst3/Contents/Resources/index.html | |||||
| all: $(TARGETS) $(jackfiles) $(vst3files) | |||||
| %/index.html: index.html | |||||
| -$(SILENT)$(shell mkdir -p "$(shell dirname $(abspath $@))") | |||||
| install -m 644 $< $(abspath $@) | |||||
| # -------------------------------------------------------------- | |||||
| @@ -0,0 +1,8 @@ | |||||
| # Meters example | |||||
| This example will show how parameter outputs can be used for UI meters in DPF.<br/> | |||||
| The plugin will inspect the host audio buffer but it won't change it in any way.<br/> | |||||
| In this example the UI will display a simple meter based on the plugin's parameter outputs.<br/> | |||||
| In order to make drawing easier the UI uses NanoVG instead of raw OpenGL.<br/> | |||||
| Please see the Parameters and States examples before studying this one.<br/> | |||||
| @@ -0,0 +1,150 @@ | |||||
| <!DOCTYPE html> | |||||
| <html lang=""> | |||||
| <head> | |||||
| <meta charset="utf-8"> | |||||
| <title></title> | |||||
| <script> | |||||
| const METER_COLOR_GREEN = 0; | |||||
| const METER_COLOR_BLUE = 1; | |||||
| const kSmoothMultiplier = 3.0; | |||||
| let fColorValue = null; | |||||
| let fColor = 'rgb(93, 231, 61)'; | |||||
| let fOutLeft = 0.0; | |||||
| let fOutRight = 0.0; | |||||
| setTimeout(function() { | |||||
| document.getElementById('user-agent').textContent = window.navigator.userAgent; | |||||
| document.getElementById('left-meter').onclick = function() { | |||||
| console.log("left meter clicked"); | |||||
| fColorValue = fColorValue == 1 ? 0 : 1; | |||||
| updateColor(fColorValue, true); | |||||
| setParameterValue(0, fColorValue); | |||||
| repaint(); | |||||
| } | |||||
| document.getElementById('right-meter').onclick = function() { | |||||
| console.log("right meter clicked"); | |||||
| fColorValue = fColorValue == 1 ? 0 : 1; | |||||
| updateColor(fColorValue, true); | |||||
| setParameterValue(0, fColorValue); | |||||
| repaint(); | |||||
| } | |||||
| }, 1) | |||||
| function repaint() { | |||||
| const lmeter = document.getElementById('left-meter-x'); | |||||
| const rmeter = document.getElementById('right-meter-x'); | |||||
| lmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutLeft)) + '%;height:' + (100 * fOutLeft) + '%'); | |||||
| rmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutRight)) + '%;height:' + (100 * fOutRight) + '%'); | |||||
| setTimeout(function() { | |||||
| setState('reset', ''); | |||||
| }, 1) | |||||
| } | |||||
| function updateColor(color, forced) { | |||||
| if (fColorValue === color && !forced) | |||||
| return; | |||||
| fColorValue = color; | |||||
| switch (color) { | |||||
| case METER_COLOR_GREEN: | |||||
| fColor = "rgb(93, 231, 61)"; | |||||
| break; | |||||
| case METER_COLOR_BLUE: | |||||
| fColor = "rgb(82, 238, 248)"; | |||||
| break; | |||||
| } | |||||
| repaint(); | |||||
| } | |||||
| function parameterChanged(index, value) { | |||||
| // console.log("paramChanged", index, value) | |||||
| switch (index) { | |||||
| case 0: // color | |||||
| updateColor(parseInt(Math.round(value))); | |||||
| break; | |||||
| case 1: // out-left | |||||
| value = (fOutLeft * kSmoothMultiplier + value) / (kSmoothMultiplier + 1.0); | |||||
| /**/ if (value < 0.001) value = 0.0; | |||||
| else if (value > 0.999) value = 1.0; | |||||
| if (fOutLeft != value) | |||||
| { | |||||
| fOutLeft = value; | |||||
| repaint(); | |||||
| } | |||||
| break; | |||||
| case 2: // out-right | |||||
| value = (fOutRight * kSmoothMultiplier + value) / (kSmoothMultiplier + 1.0); | |||||
| /**/ if (value < 0.001) value = 0.0; | |||||
| else if (value > 0.999) value = 1.0; | |||||
| if (fOutRight != value) | |||||
| { | |||||
| fOutRight = value; | |||||
| repaint(); | |||||
| } | |||||
| break; | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style> | |||||
| html, body { | |||||
| background: grey; | |||||
| color: white; | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| } | |||||
| body { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| } | |||||
| p { | |||||
| margin: 6px; | |||||
| font-size: 15px; | |||||
| overflow: hidden; | |||||
| text-overflow: ellipsis; | |||||
| width: calc(100% - 12px); | |||||
| height: 15px; | |||||
| white-space: nowrap; | |||||
| } | |||||
| #meters { | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| } | |||||
| .meter { | |||||
| background: black; | |||||
| margin: 6px; | |||||
| margin-top: 0px; | |||||
| width: calc(50vw - 9px); | |||||
| height: calc(100vh - 12px - 6px - 15px); | |||||
| } | |||||
| .meter:first-child { | |||||
| margin-right: 3px; | |||||
| } | |||||
| .meter:last-child { | |||||
| margin-left: 3px; | |||||
| } | |||||
| .meter-x { | |||||
| background: rgb(93, 231, 61); | |||||
| position: relative; | |||||
| top: 0%; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 0%; | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <p id="user-agent"> </p> | |||||
| <div id="meters"> | |||||
| <div class="meter" id="left-meter"> | |||||
| <div class="meter-x" id="left-meter-x"></div> | |||||
| </div> | |||||
| <div class="meter" id="right-meter"> | |||||
| <div class="meter-x" id="right-meter-x"></div> | |||||
| </div> | |||||
| </div> | |||||
| </body> | |||||
| </html> | |||||