Source: Thing.UE/Renderer/CEF/UWebView.js


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: 
        }
    }
}