1
0
basics/stfx/ui/web-ui.h

357 lines
9.0 KiB
C++

#pragma once
#include "../storage/cbor-walker.h"
#include "../storage/storage.h"
#include "../param-info.h"
#include <string>
#include <vector>
#include <functional>
#include <atomic>
#include <memory>
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<template<class, class...> class EffectSTFX>
struct WebUIHelper {
struct MaybeChanged {
virtual void maybeChanged() = 0;
};
struct WebParamScanner {
MaybeChanged *effect;
// Do nothing for most types
template<class V>
void operator()(const char *key, V &v) {
if constexpr (std::is_void_v<decltype(v.state(*this))>) {
v.state(*this);
}
}
// Extra methods we add for STFX storage
void info(const char *, const char *) {}
int version(int v) {
return v;
}
template<class PR>
RangeParamInfo & range(const char *key, PR &param) {
param.effect = effect;
param.info = std::unique_ptr<RangeParamInfo>{
new RangeParamInfo(param)
};
return *param.info;
}
template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) {
param.effect = effect;
param.info = std::unique_ptr<SteppedParamInfo>{
new SteppedParamInfo(param)
};
return *param.info;
}
template<class V>
bool changed(const char *key, V &v) {
(*this)(key, v);
return false;
}
void invalidate(const char *) {}
};
template<class Effect>
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<class Storage>
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<RangeParamInfo> info;
};
struct ParamStepped : public SuperStepped {
using SuperStepped::SuperStepped;
ParamStepped & operator=(double v) {
SuperStepped::operator=(v);
effect->maybeChanged();
return *this;
}
template<class Storage>
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<SteppedParamInfo> info;
};
};
struct WebStateWriter : public storage::StorageCborWriter<false, WebStateWriter> {
using Super = storage::StorageCborWriter<false, WebStateWriter>;
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<class V>
void extra(const char *key, V v) {
(*this)(key, v);
}
template<class PR>
RangeParamIgnore range(const char *key, PR &param) {
(*this)(key, param);
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
(*this)(key, param);
return {};
}
template<class V>
bool changed(const char *key, V &v) {
(*this)(key, v);
return false;
}
void invalidate(const char *) {}
};
struct WebStateReader : public storage::StorageCborReader<false, WebStateReader> {
using Super = storage::StorageCborReader<false, WebStateReader>;
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<class V>
void extra(const char *key, V v) {}
template<class PR>
RangeParamIgnore range(const char *key, PR &param) {
(*this)(key, param);
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
(*this)(key, param);
return {};
}
template<class V>
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<class Effect, class... ExtraArgs>
struct WebSTFX : public EffectSTFX<WebBase<Effect>, ExtraArgs...>, MaybeChanged {
// Inherit constructor(s)
using Super = EffectSTFX<WebBase<Effect>, ExtraArgs...>;
template<class... Args>
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<unsigned char> 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<WebMessage> queue = std::vector<WebMessage>(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<size_t> 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<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
using WebUILibraryEffect = LibraryEffect<Sample, WebUIHelper<EffectSTFX>::template WebSTFX, ExtraArgs...>;
}} // namespace