1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

WebBrowserComponent: Add native integration helper Javascript library

This commit is contained in:
attila 2023-09-16 16:02:33 +02:00 committed by Anthony Nicholls
parent 5f638157f7
commit ec92ce82b6
3 changed files with 503 additions and 0 deletions

View file

@ -0,0 +1,112 @@
if (
typeof window.__JUCE__ !== "undefined" &&
typeof window.__JUCE__.getAndroidUserScripts !== "undefined" &&
typeof window.inAndroidUserScriptEval === "undefined"
) {
window.inAndroidUserScriptEval = true;
eval(window.__JUCE__.getAndroidUserScripts());
delete window.inAndroidUserScriptEval;
}
{
if (typeof window.__JUCE__ === "undefined") {
console.warn(
"The 'window.__JUCE__' object is undefined." +
" Native integration features will not work." +
" Defining a placeholder 'window.__JUCE__' object."
);
window.__JUCE__ = {
postMessage: function () {},
};
}
if (typeof window.__JUCE__.initialisationData === "undefined") {
window.__JUCE__.initialisationData = {
__juce__platform: [],
__juce__functions: [],
__juce__registeredGlobalEventIds: [],
__juce__sliders: [],
__juce__toggles: [],
__juce__comboBoxes: [],
};
}
class ListenerList {
constructor() {
this.listeners = new Map();
this.listenerId = 0;
}
addListener(fn) {
const newListenerId = this.listenerId++;
this.listeners.set(newListenerId, fn);
return newListenerId;
}
removeListener(id) {
if (this.listeners.has(id)) {
this.listeners.delete(id);
}
}
callListeners(payload) {
for (const [, value] of this.listeners) {
value(payload);
}
}
}
class EventListenerList {
constructor() {
this.eventListeners = new Map();
}
addEventListener(eventId, fn) {
if (!this.eventListeners.has(eventId))
this.eventListeners.set(eventId, new ListenerList());
const id = this.eventListeners.get(eventId).addListener(fn);
return [eventId, id];
}
removeEventListener([eventId, id]) {
if (this.eventListeners.has(eventId)) {
this.eventListeners.get(eventId).removeListener(id);
}
}
emitEvent(eventId, object) {
if (this.eventListeners.has(eventId))
this.eventListeners.get(eventId).callListeners(object);
}
}
class Backend {
constructor() {
this.listeners = new EventListenerList();
}
addEventListener(eventId, fn) {
return this.listeners.addEventListener(eventId, fn);
}
removeEventListener([eventId, id]) {
this.listeners.removeEventListener(eventId, id);
}
emitEvent(eventId, object) {
window.__JUCE__.postMessage(
JSON.stringify({ eventId: eventId, payload: object })
);
}
emitByBackend(eventId, object) {
this.listeners.emitEvent(eventId, JSON.parse(object));
}
}
if (typeof window.__JUCE__.backend === "undefined")
window.__JUCE__.backend = new Backend();
}

View file

@ -0,0 +1,387 @@
import "./check_native_interop.js";
class PromiseHandler {
constructor() {
this.lastPromiseId = 0;
this.promises = new Map();
window.__JUCE__.backend.addEventListener(
"__juce__complete",
({ promiseId, result }) => {
if (this.promises.has(promiseId)) {
this.promises.get(promiseId).resolve(result);
this.promises.delete(promiseId);
}
}
);
}
createPromise() {
const promiseId = this.lastPromiseId++;
const result = new Promise((resolve, reject) => {
this.promises.set(promiseId, { resolve: resolve, reject: reject });
});
return [promiseId, result];
}
}
const promiseHandler = new PromiseHandler();
/**
* Returns a function object that calls a function registered on the JUCE backend and forwards all
* parameters to it.
*
* The provided name should be the same as the name argument passed to
* WebBrowserComponent::Options.withNativeFunction() on the backend.
*
* @param {String} name
*/
function getNativeFunction(name) {
if (!window.__JUCE__.initialisationData.__juce__functions.includes(name))
console.warn(
`Creating native function binding for '${name}', which is unknown to the backend`
);
const f = function () {
const [promiseId, result] = promiseHandler.createPromise();
window.__JUCE__.backend.emitEvent("__juce__invoke", {
name: name,
params: Array.prototype.slice.call(arguments),
resultId: promiseId,
});
return result;
};
return f;
}
//==============================================================================
class ListenerList {
constructor() {
this.listeners = new Map();
this.listenerId = 0;
}
addListener(fn) {
const newListenerId = this.listenerId++;
this.listeners.set(newListenerId, fn);
return newListenerId;
}
removeListener(id) {
if (this.listeners.has(id)) {
this.listeners.delete(id);
}
}
callListeners(payload) {
for (const [, value] of this.listeners) {
value(payload);
}
}
}
const BasicControl_valueChangedEventId = "valueChanged";
const BasicControl_propertiesChangedId = "propertiesChanged";
class SliderState {
constructor(name) {
if (!window.__JUCE__.initialisationData.__juce__sliders.includes(name))
console.warn(
"Creating SliderState for '" +
name +
"', which is unknown to the backend"
);
this.name = name;
this.identifier = "__juce__slider" + this.name;
this.scaledValue = 0;
this.properties = {
start: 0,
end: 1,
skew: 1,
name: "",
label: "",
numSteps: 100,
interval: 0,
};
this.valueChangedEvent = new ListenerList();
this.propertiesChangedEvent = new ListenerList();
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
this.handleEvent(event)
);
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: "requestInitialUpdate",
});
}
setNormalisedValue(newValue) {
this.scaledValue = this.snapToLegalValue(
this.normalisedToScaledValue(newValue)
);
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: BasicControl_valueChangedEventId,
value: this.scaledValue,
});
}
sliderDragStarted() {}
sliderDragEnded() {}
handleEvent(event) {
if (event.eventType == BasicControl_valueChangedEventId) {
this.scaledValue = event.value;
this.valueChangedEvent.callListeners();
}
if (event.eventType == BasicControl_propertiesChangedId) {
// eslint-disable-next-line no-unused-vars
let { eventType: _, ...rest } = event;
this.properties = rest;
this.propertiesChangedEvent.callListeners();
}
}
getScaledValue() {
return this.scaledValue;
}
getNormalisedValue() {
return Math.pow(
(this.scaledValue - this.properties.start) /
(this.properties.end - this.properties.start),
this.properties.skew
);
}
normalisedToScaledValue(normalisedValue) {
return (
Math.pow(normalisedValue, 1 / this.properties.skew) *
(this.properties.end - this.properties.start) +
this.properties.start
);
}
snapToLegalValue(value) {
const interval = this.properties.interval;
if (interval == 0) return value;
const start = this.properties.start;
const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val));
return clamp(
start + interval * Math.floor((value - start) / interval + 0.5),
this.properties.start,
this.properties.end
);
}
}
const sliderStates = new Map();
for (const sliderName of window.__JUCE__.initialisationData.__juce__sliders)
sliderStates.set(sliderName, new SliderState(sliderName));
/**
* Returns a SliderState object that is connected to the backend WebSliderRelay object that was
* created with the same name argument.
*
* To register a WebSliderRelay object create one with the right name and add it to the
* WebBrowserComponent::Options struct using withOptionsFrom.
*
* @param {String} name
*/
function getSliderState(name) {
if (!sliderStates.has(name)) sliderStates.set(name, new SliderState(name));
return sliderStates.get(name);
}
class ToggleState {
constructor(name) {
if (!window.__JUCE__.initialisationData.__juce__toggles.includes(name))
console.warn(
"Creating ToggleState for '" +
name +
"', which is unknown to the backend"
);
this.name = name;
this.identifier = "__juce__toggle" + this.name;
this.value = false;
this.properties = {
name: "",
};
this.valueChangedEvent = new ListenerList();
this.propertiesChangedEvent = new ListenerList();
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
this.handleEvent(event)
);
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: "requestInitialUpdate",
});
}
getValue() {
return this.value;
}
setValue(newValue) {
this.value = newValue;
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: BasicControl_valueChangedEventId,
value: this.value,
});
}
handleEvent(event) {
if (event.eventType == BasicControl_valueChangedEventId) {
this.value = event.value;
this.valueChangedEvent.callListeners();
}
if (event.eventType == BasicControl_propertiesChangedId) {
// eslint-disable-next-line no-unused-vars
let { eventType: _, ...rest } = event;
this.properties = rest;
this.propertiesChangedEvent.callListeners();
}
}
}
const toggleStates = new Map();
for (const name of window.__JUCE__.initialisationData.__juce__toggles)
toggleStates.set(name, new ToggleState(name));
/**
* Returns a ToggleState object that is connected to the backend WebToggleButtonRelay object that was
* created with the same name argument.
*
* To register a WebToggleButtonRelay object create one with the right name and add it to the
* WebBrowserComponent::Options struct using withOptionsFrom.
*
* @param {String} name
*/
function getToggleState(name) {
if (!toggleStates.has(name)) toggleStates.set(name, new ToggleState(name));
return toggleStates.get(name);
}
class ComboBoxState {
constructor(name) {
if (!window.__JUCE__.initialisationData.__juce__comboBoxes.includes(name))
console.warn(
"Creating ComboBoxState for '" +
name +
"', which is unknown to the backend"
);
this.name = name;
this.identifier = "__juce__comboBox" + this.name;
this.value = 0.0;
this.properties = {
name: "",
choices: [],
};
this.valueChangedEvent = new ListenerList();
this.propertiesChangedEvent = new ListenerList();
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
this.handleEvent(event)
);
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: "requestInitialUpdate",
});
}
getChoiceIndex() {
return Math.round(this.value * (this.properties.choices.length - 1));
}
setChoiceIndex(index) {
const numItems = this.properties.choices.length;
this.value = numItems > 1 ? index / (numItems - 1) : 0.0;
window.__JUCE__.backend.emitEvent(this.identifier, {
eventType: BasicControl_valueChangedEventId,
value: this.value,
});
}
handleEvent(event) {
if (event.eventType == BasicControl_valueChangedEventId) {
this.value = event.value;
this.valueChangedEvent.callListeners();
}
if (event.eventType == BasicControl_propertiesChangedId) {
// eslint-disable-next-line no-unused-vars
let { eventType: _, ...rest } = event;
this.properties = rest;
this.propertiesChangedEvent.callListeners();
}
}
}
const comboBoxStates = new Map();
for (const name of window.__JUCE__.initialisationData.__juce__comboBoxes)
comboBoxStates.set(name, new ComboBoxState(name));
/**
* Returns a ComboBoxState object that is connected to the backend WebComboBoxRelay object that was
* created with the same name argument.
*
* To register a WebComboBoxRelay object create one with the right name and add it to the
* WebBrowserComponent::Options struct using withOptionsFrom.
*
* @param {String} name
*/
function getComboBoxState(name) {
if (!comboBoxStates.has(name))
comboBoxStates.set(name, new ComboBoxState(name));
return comboBoxStates.get(name);
}
/**
* Appends a platform-specific prefix to the path to ensure that a request sent to this address will
* be received by the backend's ResourceProvider.
* @param {String} path
*/
function getBackendResourceAddress(path) {
const platform =
window.__JUCE__.initialisationData.__juce__platform.length > 0
? window.__JUCE__.initialisationData.__juce__platform[0]
: "";
if (platform == "windows" || platform == "android")
return "https://juce.backend/" + path;
if (platform == "macos" || platform == "ios" || platform == "linux")
return "juce://juce.backend/" + path;
console.warn(
"getBackendResourceAddress() called, but no JUCE native backend is detected."
);
return path;
}
export {
getNativeFunction,
getSliderState,
getToggleState,
getComboBoxState,
getBackendResourceAddress,
};

View file

@ -0,0 +1,4 @@
{
"name": "juce-framework-frontend",
"version": "7.0.7"
}