#pragma once #include "../storage/cbor-walker.h" #include "../storage/storage.h" #include "../param-info.h" #include #include #include #include #include namespace stfx { namespace web { /* Given an STFX template which (when insantiated) has inheritance like: EffectSTFX : Effect This produces a template with inheritance like: WebSTFX : EffectSTFX : WebBase : Effect The WebBase template replaces the ParamRange/ParamStepped classes, so that the UI can be updated when they change. The outer WebSTFX class adds methods for forwarding messages between the effect and its webview. */ template class EffectSTFX> struct WebUIHelper { struct MaybeChanged { virtual void maybeChanged() = 0; }; struct WebParamScanner { MaybeChanged *effect; // Do nothing for most types template void operator()(const char *key, V &v) { if constexpr (std::is_void_v) { v.state(*this); } } // Extra methods we add for STFX storage void info(const char *, const char *) {} int version(int v) { return v; } template RangeParamInfo & range(const char *key, PR ¶m) { param.effect = effect; param.info = std::unique_ptr{ new RangeParamInfo(param) }; return *param.info; } template SteppedParamInfo & stepped(const char *key, PS ¶m) { param.effect = effect; param.info = std::unique_ptr{ new SteppedParamInfo(param) }; return *param.info; } template bool changed(const char *key, V &v) { (*this)(key, v); return false; } void invalidate(const char *) {} }; template class WebBase : public Effect { using SuperRange = typename Effect::ParamRange; using SuperStepped = typename Effect::ParamStepped; public: using Effect::Effect; struct ParamRange : public SuperRange { using SuperRange::SuperRange; ParamRange & operator=(double v) { SuperRange::operator=(v); effect->maybeChanged(); return *this; } template void state(Storage &storage) { double prevV = *this; SuperRange::state(storage); if (storage.extra()) { storage.extra("$type", "ParamRange"); storage.extra("name", info->name); storage.extra("defaultValue", info->defaultValue); std::string text, units; info->toString((double)*this, text, units); storage.extra("text", text); storage.extra("textUnits", units); double unit = info->toUnit(*this); if (storage.changed("rangeUnit", unit)) { *this = info->fromUnit(unit); storage.invalidate("value"); } } if (prevV != *this) { effect->maybeChanged(); storage.invalidate("rangeUnit"); storage.invalidate("text"); storage.invalidate("textUnits"); } } private: friend struct WebParamScanner; MaybeChanged *effect = nullptr; std::unique_ptr info; }; struct ParamStepped : public SuperStepped { using SuperStepped::SuperStepped; ParamStepped & operator=(double v) { SuperStepped::operator=(v); effect->maybeChanged(); return *this; } template void state(Storage &storage) { int prevV = *this; SuperStepped::state(storage); if (storage.extra()) { storage.extra("name", info->name); storage.extra("$type", "ParamStepped"); std::string text = info->toString((int)*this); storage.extra("text", text); } if (prevV != *this) { effect->maybeChanged(); storage.invalidate("text"); } } private: friend struct WebParamScanner; MaybeChanged *effect = nullptr; std::unique_ptr info; }; }; struct WebStateWriter : public storage::StorageCborWriter { using Super = storage::StorageCborWriter; using Super::Super; void info(const char *name, const char *desc) { extra("name", name); extra("desc", desc); } int version(int v) { return v; // we don't use old states, so not worth tracking } bool extra() { return true; } template void extra(const char *key, V v) { (*this)(key, v); } template RangeParamIgnore range(const char *key, PR ¶m) { (*this)(key, param); return {}; } template SteppedParamIgnore stepped(const char *key, PS ¶m) { (*this)(key, param); return {}; } template bool changed(const char *key, V &v) { (*this)(key, v); return false; } void invalidate(const char *) {} }; struct WebStateReader : public storage::StorageCborReader { using Super = storage::StorageCborReader; using Super::Super; void info(const char *, const char *) {} int version(int v) {return v;} bool extra() { // We're interested in an extended set of values being sent back, even if we ignore actual "extra" things return true; } template void extra(const char *key, V v) {} template RangeParamIgnore range(const char *key, PR ¶m) { (*this)(key, param); return {}; } template SteppedParamIgnore stepped(const char *key, PS ¶m) { (*this)(key, param); return {}; } template bool changed(const char *key, V &v) { V prev = v; (*this)(key, v); return v != prev; } void invalidate(const char *invalidatedKey) { LOG_EXPR(invalidatedKey); } }; template struct WebSTFX : public EffectSTFX, ExtraArgs...>, MaybeChanged { // Inherit constructor(s) using Super = EffectSTFX, ExtraArgs...>; template WebSTFX(Args... args) : Super(args...) { WebParamScanner scanner{this}; this->state(scanner); }; std::string webPage = "html/generic.html"; int webWidth = 640, webHeight = 480; struct WebMessage { std::vector bytes; WebMessage() { bytes.reserve(256); } void sent() { readyToSend.clear(); } private: friend struct WebSTFX; std::atomic_flag readyToSend = ATOMIC_FLAG_INIT; void markReady() { readyToSend.test_and_set(); } }; bool hasPendingWebMessage() const { auto &message = queue[readIndex]; return message.readyToSend.test(); } // Poll on the main thread (and directly after any `.webReceive()`), calling `.sent()` after you've sent it, repeat until you get `nullptr` WebMessage * getPendingWebMessage() { auto &message = queue[readIndex]; if (!message.readyToSend.test()) return nullptr; if (++readIndex == queue.size()) readIndex = 0; if (resetQueue.test()) { // Clear ("send") every message aside from this one for (auto &m : queue) { if (&m == &message) continue; m.sent(); } // Ready to start sending messages again, starting from the next index writeIndex.store(readIndex); resetQueue.clear(); // any messages (state updates) sent after this won't assume a *newer* state than what we serialise below // Replace this one's message with the complete plugin state WebStateWriter storage(message.bytes); this->state(storage); } return &message; } // Call when the webview posts any message back void webReceive(const void *message, size_t size) { auto *bytes = (const unsigned char *)message; signalsmith::cbor::TaggedCborWalker cbor(bytes, bytes + size); if (cbor.isUtf8()) { if (cbor.utf8View() == "ready") { if (auto *m = getEmptyMessage()) { resetQueue.test_and_set(); m->markReady(); } // if this fails we're doing a reset anyway, so no worrie return; } } else if (cbor.isMap()) { // Apply it as a merge WebStateReader reader(cbor); this->state(reader); } } void maybeChanged() override { std::cout << "web effect maybe changed\n"; // Resend entire state again if (auto *m = getEmptyMessage()) { resetQueue.test_and_set(); m->markReady(); } } private: std::vector queue = std::vector(64); // power of 2 so that overflow doesn't mess with the modulo size_t readIndex = 0; std::atomic_flag resetQueue = ATOMIC_FLAG_INIT; // Atomic because multiple threads might write std::atomic writeIndex = 0; WebMessage * getEmptyMessage() { if (resetQueue.test()) return nullptr; auto reservedIndex = writeIndex.fetch_add(1); auto &message = queue[reservedIndex%queue.size()]; if (message.readyToSend.test()) { std::cerr << "Web message queue full - sending entire state instead\n"; // When our queue is full, we drop the entire thing and re-send the entire state resetQueue.test_and_set(); return nullptr; } message.bytes.resize(0); return &message; } }; }; // Use this instead of a plain LibraryEffect template class EffectSTFX, class... ExtraArgs> using WebUILibraryEffect = LibraryEffect::template WebSTFX, ExtraArgs...>; }} // namespace