const UImageTextureResource = require("../../Resources/UImageTextureResource");
const UUtils = require("../../UUtils");
const UWebViewPool = require("./UWebViewPool");
const VDomEventCaptureComponent = require("../../cef/scripts/vm/VDomEventCaptureComponent");
/**
* @description 基于CEF内核驱动WebView组件
* <br> 用于将HTML页面渲染为引擎中的可渲染目标(RenderTarget),可作为Texture设置到任意可渲染对象上
* @class THING.UE.UWebView
*/
module.exports = class UWebView extends VDomEventCaptureComponent {
static MOUSE_WHEEL_SENSITIVITY = -0.01;
static MOUSE_BUTTON_FLAGS = {
0: true,
1: true,
2: true
};
static ALLOW_WINDOW_POPUP = false;
static WEBVIEW_ID = 0;
#URL;
#id;
#web;
#texture;
#windowContainer = [];
#pickLocation;
#regionNode;
#interactive;
#blurBackground;
#enableMouseInput;
#enableKeyboardInput;
#resolution;
#domScale;
#pooled = false;
#complete = null;
#bNeedReload = false;
constructor(params = {}) {
super(params);
this.#blurBackground = false;
this.#bNeedReload = false;
this.#regionNode = null;
this.#enableMouseInput = false;
this.#enableKeyboardInput = false;
this.#id = UWebView.WEBVIEW_ID++;
this.#URL = this.#parseURL(params.url);
this.#resolution = (params.resolution != null) ? params.resolution : [1920, 1080];
this.#resolution[0] = params.domWidth ? params.domWidth : this.#resolution[0];
this.#resolution[1] = params.domWidth ? params.domWidth : this.#resolution[1];
this.#domScale = params.domScale ? params.domScale : 1;
this.#interactive = params.interactive ? params.interactive : false;
this.#pooled = (params.pooled != null) ? params.pooled : false;
this.#complete = (params.complete != null) ? params.complete : null;
if (!this.#pooled) {
this.#web = UWebViewPool.pop();
this.#web.CEF.WebBrowser_0.OnFrameLoadEnd = (url) => {
this.#complete && this.#complete(url);
if (this.#renderableNode && this.#renderableNode instanceof require('../../USprite')) {
this.#renderableNode.getNode().MarkRenderStateDirty();
}
};
this.#web.CEF.WebBrowser_0.OnWebBrowserRequestError = (error) => {
console.error(`webview request error : ${error}`);
};
// this.#web.CEF.WebBrowser_0.SetWindowPopupSettings(UWebView.ALLOW_WINDOW_POPUP);
this.resource = new UImageTextureResource({
data: UE4.ObjectPool.GetOrCreate(UE4.TextureRenderTarget2D, this.#web.GetTexture().Pointer)
});
this.resource.setWrapS('Clamp');
this.resource.setWrapT('Clamp');
}
else {
this.#updateWebViewTask();
}
}
dispose(params = {
forceDestroy: false
}) {
if (this.#pooled) {
UWebViewPool.releaseTask(this.#id);
} else {
if (this.#web) {
if (params.forceDestroy) {
UWebViewPool.dispose(this.#web);
}
else {
UWebViewPool.push(this.#web);
}
}
}
}
get #renderableNode() {
if (this.#regionNode) {
let children = this.#regionNode.getChildren();
if (children.length > 0) {
let renderableNode = children[0];
return renderableNode;
}
}
return null;
}
get #webBrowser() {
if (this.#web) {
return this.#web.CEF.WebBrowser_0;
}
return undefined;
}
#reload() {
if (this.#bNeedReload) {
this.#web.SetDrawSize(this.#resolution[0], this.#resolution[1]);
this.#web.SetUrl(this.#URL);
this.#bNeedReload = false;
}
}
set domWidth(value) {
if (this.#resolution[0] !== value) {
this.#bNeedReload = true;
}
this.#resolution[0] = value;
this.#reload();
}
get domWidth() {
return this.#resolution[0];
}
set domHeight(value) {
if (this.#resolution[1] !== value) {
this.#bNeedReload = true;
}
this.#resolution[1] = value;
this.#reload();
}
get domHeight() {
return this.#resolution[1];
}
set domScale(value) {
this.#domScale = value;
console.warn(`domScale does not work in ThingUE.`);
}
get domScale() {
return this.#domScale;
}
set regionNode(value) {
// if (this.#regionNode == value) {
// return;
// }
this.#regionNode = value;
let children = this.#regionNode.getChildren();
if (children.length > 0) {
let renderableNode = children[0];
// special material for webview region object
{
const USprite = require('../../USprite');
if (renderableNode instanceof USprite) {
renderableNode.renderer.renderLayer = THING.UE.URenderLayer.WebView;
renderableNode.renderer.bHitProxy = true;
}
else {
renderableNode.renderer.renderLayer = THING.UE.URenderLayer.WebView;
renderableNode.renderer.bHitProxy = false;
// TODO: use new THING.UE.UMaterialResource() replaced.
renderableNode.material = THING.Utils.createObject('MaterialResource', {
specail: 'WebView',
blendMode: 'Translucent'
// ,sideType: 'TwoSided'
});
}
}
renderableNode.getStyle().setImage('Map', this.getResource());
// initialize interactive object
if (this.#interactive) {
this.#setInteractive(renderableNode);
}
}
}
get regionNode() {
return this.#regionNode;
}
set interactive(value) {
this.#interactive = value;
if (this.#interactive && this.#regionNode) {
let children = this.#regionNode.getChildren();
if (children.length > 0) {
let renderableNode = children[0];
if (this.#interactive) {
this.#setInteractive(renderableNode);
}
}
}
}
get interactive() {
return this.#interactive;
}
/**
* allow popup new window. default is false
*/
set popup(value) {
if (this.#web && this.#web.CEF) {
this.#web.CEF.WebBrowser_0.SetWindowPopupSettings(value);
}
}
get popup() {
if (this.#web && this.CEF) {
return this.#web.CEF.WebBrowser_0.bAllowPopupWindow;
}
return false;
}
set url(value) {
this.#URL = this.#parseURL(value);
if (!this.#pooled) {
this.#web.SetDrawSize(this.#resolution[0], this.#resolution[1]);
this.#web.SetUrl(this.#URL);
}
else {
this.#updateWebViewTask();
}
}
get url() {
this.#URL = this.#web.GetUrl();
return this.#URL;
}
get blurBackground() {
return this.#blurBackground;
}
set blurBackground(value) {
this.#blurBackground = value;
if (this.regionNode) {
}
}
// DEPRECATED : use interactive field
/**
* enable mouse input (click, down, up, move, ...).
* after setWindow , webview active mouse input automatically
*/
set mouseInput(value) {
this.#enableMouseInput = value;
}
// DEPRECATED : use interactive field
/**
* enable keybaord input
*/
set keyboardInput(value) {
this.#enableKeyboardInput = value;
}
// DEPRECATED : use domWidth and domHeight
set resolution(value) {
this.#resolution = value;
if (!this.#pooled) {
this.#web.SetDrawSize(value[0], value[1]);
}
else {
this.#updateWebViewTask();
}
}
// DEPRECATED
getURL() {
console.warn(`use variable url instead.`);
this.#URL = this.#web.GetUrl();
return this.#URL;
}
// DEPRECATED
setURL(url) {
console.warn(`use variable url instead.`);
this.#URL = this.#parseURL(url);
this.#web.SetUrl(this.#URL);
}
// DEPRECATED
/**
* deferred render large number of webview, callback via task.complete
* @param {Array<Object>} urlTasks
*/
setURLs(urlTasks) {
UWebViewPool.runTasks(urlTasks);
}
// DEPRECATED
setResolution(resolution) {
console.warn(`use variable resolution instead.`);
this.#resolution = resolution;
this.#web.SetDrawSize(resolution[0], resolution[1]);
}
getTexture() {
this.#texture = this.#web.GetTexture();
return this.#texture;
}
getResource(border = null) {
let formatBorder = (unformatted, defaultValue, fullSizePx = 1.0) => {
if (unformatted == null) {
return defaultValue;
}
else if (unformatted.endsWith && unformatted.endsWith('px')) {
let px = parseFloat(unformatted);
if (px != NaN) {
return px / fullSizePx;
}
else {
return defaultValue;
}
}
else if (unformatted.endsWith && unformatted.endsWith('%')) {
let percent = parseFloat(unformatted);
if (percent != NaN) {
return percent / 100.0;
}
else {
return defaultValue;
}
}
else {
let value = parseFloat(unformatted);
if (value != NaN) {
return value;
}
else {
return defaultValue;
}
}
};
if (border) {
let left = formatBorder(border.left, 0.0, this.#resolution[0]);
let right = formatBorder(border.right, 1.0, this.#resolution[0]);
let top = formatBorder(border.top, 0.0, this.#resolution[1]);
let bottom = formatBorder(border.bottom, 1.0, this.#resolution[1]);
this.resource.border = [left, top, right, bottom];
}
return this.resource;
}
draw() {
if (!this.#pooled) {
this.#web.Draw();
}
else {
this.#updateWebViewTask();
}
}
setDrawOptions(drawEveryFrame = true) {
this.#web.SetDrawOptions(drawEveryFrame);
}
showDevTools() {
if (this.#web) {
this.#web.CEF.WebBrowser_0.ShowDevTools();
}
}
#updateWebViewTask() {
UWebViewPool.runTask({
id: this.#id,
url: this.#URL,
resolution: this.#resolution,
complete: this.#complete,
startPos: [0, 0]
});
}
#interactiveFilter(pickedObject) {
if (!pickedObject) {
return {
hit: false
}
}
if (!pickedObject.node) {
return {
hit: false
}
}
if (!pickedObject.node.getChildren()) {
return {
hit: false
}
}
let interactiveObjcet = pickedObject.node.getChildren()[0];
interactiveObjcet = interactiveObjcet ? interactiveObjcet.getChildren()[0] : null;
return this.#insideWindow(interactiveObjcet);
}
// TODO: spr1ngd : refactor
#setInteractive(interactiveObject) {
this.#interactive = true;
if (this.#windowContainer.indexOf(interactiveObject) < 0) {
this.#windowContainer.push(interactiveObject);
}
app.on('mousemove', ev => {
if (!this.#interactive) {
return;
}
let hitInfo = this.#interactiveFilter(ev.object);
if (hitInfo.hit) {
const USprite = require("../../USprite");
if (hitInfo.object instanceof USprite) {
this.#pickLocation = hitInfo.object.getNode().pickUV();
} else {
this.#pickLocation = UE4.UnrealUtils.pickUV();
}
let style = hitInfo.object.style;
if (style.hasBorder) {
let left = style.border[0];
let top = style.border[1];
let right = style.border[2];
let bottom = style.border[3];
let relocateX = (right - left) * this.#pickLocation.X + left;
let relocateY = (bottom - top) * this.#pickLocation.Y + top;
this.#pickLocation = new UE4.Vector2(relocateX, relocateY);
}
this.#enableInnerInput();
}
else {
this.#pickLocation = new UE4.Vector2(-1, -1);
this.#disableInnerInput();
}
this.#handleMouseMove([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
});
app.on('mousedown', ev => {
if (!this.#interactive) {
return;
}
if (this.#pickLocation) {
this.#handleMouseDown([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
}
});
app.on('mouseup', ev => {
if (!this.#interactive) {
return;
}
if (this.#pickLocation) {
this.#handleMouseUp([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
}
});
app.on('wheel', ev => {
if (!this.#interactive) {
return;
}
if (this.#pickLocation) {
this.#handleMouseWheel([this.#pickLocation.X, this.#pickLocation.Y], ev.deltaY, ev.button);
}
});
}
// DEPRECATED : use #setInteractive
/**
* set webview container , allow mouse pick
* @param {Thing} thingObject
*/
setWindow(thingObject) {
if (this.regionNode === undefined) {
this.regionNode = thingObject;
}
if (this.#windowContainer.indexOf(thingObject) < 0) {
this.#windowContainer.push(thingObject);
}
this.mouseInput = true;
app.on('mousemove', ev => {
if (!this.#enableMouseInput) {
return;
}
let hitInfo = this.#insideWindow(ev.object);
if (hitInfo.hit) {
if (hitInfo.object.type === 'Marker') {
this.#pickLocation = hitInfo.object.body.getRenderableNode().getNode().pickUV();
} else {
this.#pickLocation = UE4.UnrealUtils.pickUV();
}
let style = hitInfo.object.body.getRenderableNode().style;
if (style.hasBorder) {
let left = style.border[0];
let top = style.border[1];
let right = style.border[2];
let bottom = style.border[3];
let relocateX = (right - left) * this.#pickLocation.X + left;
let relocateY = (bottom - top) * this.#pickLocation.Y + top;
this.#pickLocation = new UE4.Vector2(relocateX, relocateY);
}
this.#enableInnerInput();
}
else {
this.#pickLocation = new UE4.Vector2(-1, -1);
this.#disableInnerInput();
}
this.#handleMouseMove([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
});
app.on('mousedown', ev => {
if (!this.#enableMouseInput) {
return;
}
if (this.#pickLocation) {
this.#handleMouseDown([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
}
});
app.on('mouseup', ev => {
if (!this.#enableMouseInput) {
return;
}
if (this.#pickLocation) {
this.#handleMouseUp([this.#pickLocation.X, this.#pickLocation.Y], ev.button);
}
});
app.on('wheel', ev => {
if (!this.#enableMouseInput) {
return;
}
if (this.#pickLocation) {
this.#handleMouseWheel([this.#pickLocation.X, this.#pickLocation.Y], ev.deltaY, ev.button);
}
});
}
#insideWindow(pickObject) {
for (let idx = 0; idx < this.#windowContainer.length; idx++) {
let container = this.#windowContainer[idx];
if (pickObject === container) {
return {
hit: true,
object: container
};
}
}
return {
hit: false
};
}
/**
*
* @param {Array<Number>} pivot
*/
setWindowOptions(pivot = [0.5, 0.5]) {
this.#web.SetWindowOptions(
new UE4.Vector2(pivot[0], pivot[1])
);
}
/**
* bind a field for js post data to ue
* @param {String} name
*/
bindObject(name) {
this.#web.CEF.WebBrowser_0.BindUObject(name, this.#web.CEF.WebBrowser_0);
}
/**
* exeucte a piece of code in current webview environment, callback with json string.
* @param {String} javascript snippet
* @param {Callback<String>} callback
*/
executeJavascript(js, callback = null) {
this.#web.CEF.OnJavascriptCallback = (result) => {
callback && callback(result);
}
this.#web.CEF.ExecuteJavascript(js);
}
/**
* add listener in current webview environment, callback with json string.
* @param {Callback<String>} callback
*/
onJavascriptCallback(callback = null) {
this.#web.CEF.OnJavascriptCallback = (result) => {
callback && callback(result);
}
}
#handleMouseDown(position, buttonId = 0) {
if (!this.#handleFilter(position, buttonId)) {
return;
}
this.#webBrowser.HandleMouseDown(new UE4.Vector2(position[0], position[1]), buttonId);
}
#handleMouseUp(position, buttonId = 0) {
if (!this.#handleFilter(position, buttonId)) {
return;
}
this.#webBrowser.HandleMouseUp(new UE4.Vector2(position[0], position[1]), buttonId);
}
#handleMouseMove(position, buttonId = 0) {
if (!this.#handleFilter(position, buttonId)) {
return;
}
this.#webBrowser.HandleMouseMove(new UE4.Vector2(position[0], position[1]), buttonId);
}
#handleMouseDoubleClick(position, buttonId = 0) {
if (!this.#handleFilter(position, buttonId)) {
return;
}
this.#webBrowser.HandleMouseDoubleClick(new UE4.Vector2(position[0], position[1]), buttonId);
}
#handleMouseWheel(position, delta = 0.0, buttonId = 0) {
if (!this.#handleFilter(position, buttonId)) {
return;
}
this.#webBrowser.HandleMouseWheel(new UE4.Vector2(position[0], position[1]), delta * UWebView.MOUSE_WHEEL_SENSITIVITY, buttonId);
}
#handleFilter(position, buttonId) {
if (position[0] < 0 || position[1] < 0) {
return false;
}
if (UWebView.MOUSE_BUTTON_FLAGS[buttonId] != true) {
return false;
}
return true;
}
#enableInnerInput() {
app.camera.enable = false;
}
#disableInnerInput() {
app.camera.enable = true;
}
/**
* if url is relative path, convert to full path
* @param {*} url
*/
#parseURL(url) {
return (url != null)
? (url.startsWith('.'))
? UUtils.relativePathToFullPath(url, true)
: url
: '';
}
#markerRenderStateDirty() {
if (!this.regionNode) {
return;
}
const USprite = require("../../USprite");
if (this.regionNode.node instanceof USprite) {
}
else {
// THING.Plane
// TODO:
}
}
}