1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
16dc595aa6 Check for pending messages after .process() 2025-06-25 17:43:28 +01:00
f1ede7a2ca Working web UI 2025-06-25 17:35:39 +01:00
3 changed files with 192 additions and 10 deletions

View File

@ -308,9 +308,16 @@ struct Plugin : public clap_plugin {
}
inputBuffers.resize(0);
outputBuffers.resize(0);
if (plugin.effect.hasPendingWebMessage()) {
plugin.host->request_callback(plugin.host);
}
return CLAP_PROCESS_CONTINUE;
}
static void plugin_on_main_thread(const clap_plugin *obj) {}
static void plugin_on_main_thread(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
plugin.sendWebMessages();
}
// parameters
struct Param : public clap_param_info {
@ -609,6 +616,7 @@ struct Plugin : public clap_plugin {
if (!fillFromStream(stream, buffer)) return false;
StateReader storage{buffer};
plugin.effect.state(storage);
plugin.sendWebMessages(); // should already be on the main thread
return true;
}
@ -630,9 +638,7 @@ struct Plugin : public clap_plugin {
void sendWebMessages() {
if (!hostWebview1) return;
while (auto *m = effect.getPendingWebMessage()) {
LOG_EXPR("pending web message");
hostWebview1->send(host, m->bytes.data(), m->bytes.size());
LOG_EXPR(m->bytes.size());
m->sent();
}
}

View File

@ -3,29 +3,145 @@
<head>
<title>Generic STFX UI</title>
<style>
* {
box-sizing: border-box;
}
body {
display: grid;
grid-template-areas: "header" "params";
grid-template-rows: 3em 1fr;
margin: 0;
padding: 0;
width: 100vw;
max-width: 100vw;
height: 100vh;
max-height: 100vh;
overflow: hidden;
background: linear-gradient(#FFF, #EEE, #CCC);
color: #000;
text-align: center;
word-wrap: break-word;
font-family: Bahnschrift, 'DIN Alternate', 'Alte DIN 1451 Mittelschrift', 'D-DIN', 'OpenDin', 'Clear Sans', 'Barlow', 'Abel', 'Franklin Gothic Medium', system-ui, sans-serif;
/* no text selectable by default */
user-select: none;
}
output {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
font-weight: normal;
color: #000;
}
#header {
grid-area: header;
display: flex;
justify-content: center;
}
#params {
grid-area: params;
display: flex;
align-content: space-around;
justify-content: space-evenly;
flex-wrap: wrap;
padding: 1rem;
gap: 1rem;
}
.param-range {
display: grid;
width: 3.5rem;
grid-template-areas: "dial" "name";
grid-template-rows: 3.5rem 1fr;
}
.param-range-value {
position: relative;
grid-area: dial;
width: 3.5rem;
height: 3.5rem;
}
.param-range-dial {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
.param-range-text {
position: absolute;
top: 30%;
left: 0;
width: 100%;
}
.param-range-units {
position: absolute;
top: 60%;
left: 0;
width: 100%;
color: #666;
font-size: 0.85rem;
}
.param-range-name {
grid-area: name;
}
</style>
</head>
<body>
<h1>{name}</h1>
<ul>
<h1 id="header">{name}</h1>
<section id="params">
<template @foreach>
<li @if="${d => d.$type == 'ParamRange'}">{=}</li>
<label class="param-range" @if="${d => d.$type == 'ParamRange'}">
<div class="param-range-name">{name}</div>
<script>
function move(data, dx, dy) {
data.rangeUnit = Math.min(1, Math.max(0, data.rangeUnit - dy/250));
}
function press(data, count) {
if (count == 2) data.value = data.defaultValue;
}
</script>
<div class="param-range-value" $move="${move}" $press="${press}">
<canvas class="param-range-dial" $update="${drawDial}"></canvas>
<output class="param-range-text">{text}</output><br>
<div class="param-range-units">{textUnits}</div>
</div>
</label>
</template>
</ul>
</section>
<script>
function drawDial(data, canvas) {
let scale = window.devicePixelRatio;
let pixels = canvas.offsetWidth*scale;
if (canvas.width != pixels) canvas.width = pixels;
if (canvas.height != pixels) canvas.height = pixels;
let context = canvas.getContext('2d');
context.resetTransform();
context.clearRect(0, 0, pixels, pixels);
context.scale(pixels/2, pixels/2);
context.translate(1, 1);
context.beginPath();
context.ellipse(0, 0, 1, 1, 0, 0, Math.PI*2);
context.fillStyle = '#FFF';
context.fill();
context.beginPath();
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*(-1.249 + data.rangeUnit*1.499));
context.strokeStyle = '#000';
context.lineWidth = 0.2;
context.lineCap = 'round';
context.stroke();
}
</script>
<script src="cbor.min.js"></script>
<script src="matsui-bundle.min.js"></script>
<script>

View File

@ -94,14 +94,23 @@ struct WebUIHelper {
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");
}
@ -128,6 +137,7 @@ struct WebUIHelper {
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);
@ -187,6 +197,44 @@ struct WebUIHelper {
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)
@ -219,6 +267,11 @@ struct WebUIHelper {
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() {
@ -251,19 +304,26 @@ struct WebUIHelper {
if (cbor.isUtf8()) {
if (cbor.utf8View() == "ready") {
LOG_EXPR(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);
}
std::cout << "received " << size << " bytes from webview\n";
}
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: