Commit 8017b3c9 authored by Morten Sorvig's avatar Morten Sorvig

Working port to WebAssembly and Emscripten

parent 1a48fdc8
Pipeline #5364 canceled with stages
......@@ -2,46 +2,64 @@ The Qt Quick Web Runtime
========================
The Qt Quick Web Runtime provides a JavaScript API for interpreting
QML and running Qt Quick in the browser. It is currently implemented
using Native Client (NaCl). This limits support to the Chrome browser.
QML and running Qt Quick in the browser.
Binary distributions of the runtime enables developing for Qt Quick
and QML in the Web without using a C++ compiler or Emscripten.
Building
------------------------
Build and deploy as a Qt for NaCl project: (use 'make install' to copy
over example files.)
/path/to/qtbase/bin/qmake && make install && /path/to/qtbase/bin/nacldeployqt qtquickruntime.bc
Build one of the examles:
- examples/web Simple web test case wtih QML editor
- examples/desktop A desktop build of the runtime
- examples/mock Web/mock build which does not depend on Qt
Distribution
------------------------
This will generate the required runtime files:
The Qt Quick Runtime distribution consists of the following files:
qtquick.js
qtquickruntime.wasm
qtquickruntime.js
qtloader.js
qtquickruntime.pexe
qtquickruntime.nmf
[also index.html, which we will not use]
The files in this repository are BSD-licensed. The license of the
runtime build depends on the license of the Qt build used.
Using
Usage
------------------------
Create the runtime object, create a DOM element, and set the source:
var runtime = new QtQuickRuntime()
var element = runtime.createElement()
runtime.setSource(source)
The runtime renders to a html container element, where it
will display a loading screen, followed by a canvas with
the Qt Quick Content.
The JavaScript API supports setting QML source code and providing
callbacks for handling QML code warnings and errors, as well as
controlling when the download of the qtquickruntime.wasm file starts.
See qtquickruntime.js for further documentation.
Usage example:
An usage example is also provided:
example/example.html
let qtQuick = new QtQuickRuntime({
container : document.getElementById("a_container"),
qmlStatusUpdate : function(status) {
console.log(status)
},
qmlWarningsUpdate: function(warnings) {
console.log(warnings)
}
});
qtQuick.setQmlSourceCode("qml_source_code")
qtQuick.load()
Binary distribution
Code Structure and Implementation
------------------------
The qt-quick-web-runtime-bin repo contains a LGPL-licensed binary distribution:
github.com/msorvig/qt-quick-web-runtime-bin
The runtime is provides a private C++ API, which is exported to JavaScript using Emscripten
functionaity. This exported API is then in turn used by the public JavaScript API, which
also abstracts over wasm module loading.
(inner -> outer layers)
If you are a Qt commercial license holder you can build your own binary using
your commercially licensed Qt, the BSD licensed Qt for NaCl port, and this BSD
licensed runtime.
Qt Quick / QML Reloader : qmlreloader.h
Qt Quick Runtime C++ API : qtquickruntime.h qt-quick-runtime.pri
Qt Quick Runtime Emscripten export : qtquickwebruntime.h qt-quick-web-runtime.pri
Qt Quick Runtime JavaScript API : qtquick.js
include ($$PWD/../../qt-quick-web-runtime.pri)
include ($$PWD/../../qt-quick-runtime.pri)
SOURCES += main.cpp
......@@ -5,8 +5,12 @@
int main(int argc, char **argv)
{
std::string qmlSource = R"QMLSOURCE(
erki
berki
import QtQuick 2.8
Text {
anchors.centerIn : parent
text : "Hello World"
color : foo // provoke a warning
}
)QMLSOURCE";
QtQuickRuntime runtime;
......@@ -25,4 +29,3 @@ int main(int argc, char **argv)
runtime.show();
runtime.exec();
}
<!--
Qt Quick Web Runtime example: Live Documentation
-->
<html>
<head>
<title>Qt Quick Web Runtime</title>
<script src="qtloader.js" type="text/javascript"></script>
<script src="qtquickruntime.js" type="text/javascript"></script>
</head>
<style>
.code {
background-color: rgb(64, 66, 68);
padding: 10px;
color: rgb(230, 230, 230);
}
.qt-embed {
height: 100px
}
.qt-container {
height: 100px
}
.qt-qml-errors {
background-color: DDDDDD;
color: 111111;
padding: 2;
font-size: x-small;
}
.qt-qml-warnings {
background-color: DDDDDD;
color: 111111;
padding: 2;
font-size: x-small;
}
body {
font-family : 'Open Sans', Arial, Helvetica, sans-serif;
}
pre:focus, textarea:focus {
outline: none;
}
</style>
<script type="text/javascript">
// Globals
var debug = false
var runtime = undefined
var element = undefined
var active = undefined
var qmlstatus = undefined
var warnings = undefined
// Utility
function $(elementId) {
return document.getElementById(elementId)
}
function removeChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
// App code
function init() {
if (debug)
console.log("init")
// create the runtime
var config = {}
config.resizeMode = "sizeViewToRootObject";
runtime = new QtQuickRuntime(config);
element = runtime.createElement();
// sign up for QML loading status and errors.
element.addEventListener("qmlloadend", onQmlStatusChange, true);
element.addEventListener("qmlwarnings", onQmlWarning, true);
}
function onCodeClicked()
{
if (debug)
console.log("onCodeClicked");
// Get html nodes
var qmlview = $("qmlview");
var qmlcode = $("qmlcode");
var quickview = $("quickview");
var code = qmlcode.innerHTML;
// bail out if the Qt Quick preview is already active
if (active === quickview)
return
active = quickview
// remove static content and add QtQuick content
// ### save static content
removeChildren(quickview)
quickview.appendChild(element);
// Create status and warning nodes
qmlstatus = document.createElement("div");
qmlstatus.innerHTML = "..."
qmlstatus.setAttribute("class", "qt-qml-errors");
qmlview.appendChild(qmlstatus)
warnings = document.createElement("div");
warnings.innerHTML = " "
warnings.setAttribute("class", "qt-qml-warnings");
qmlview.appendChild(warnings)
// Run source code
runtime.setData(code);
runtime.load();
}
function onCodeChanged()
{
if (debug)
console.log("onCodeChanged");
warnings.innerHTML = " "
var qmlcode = $("qmlcode");
var code = qmlcode.innerHTML;
runtime.setData(code);
}
function onQmlStatusChange(event)
{
if (event.detail == "OK") {
qmlstatus.innerHTML = "We're doing OK";
} else {
qmlstatus.innerHTML = event.detail
}
}
function onQmlWarning(event)
{
warnings.innerHTML += event.detail + "\n"
}
</script>
<body id="TOPLEVEL" onload="init()">
<div>
<div id="container"> </div>
</div>
<h1>MouseArea QML Type</h1>
A MouseArea is an invisible item that is typically used in conjunction with a visible item
in order to provide mouse handling for that item. By effectively acting as a proxy, the logic
for mouse handling can be contained within a MouseArea item.
<br>
<h3>Example Usage</h3>
The following example uses a MouseArea in a Rectangle that changes the Rectangle color to red
when clicked:
<p>
<div id="quickview">
<img src="example.png" id="quickview"></img>
</div>
<p>
<div class="code" id="qmlview" onclick="onCodeClicked()" oninput="onCodeChanged()">
<pre id="qmlcode" contenteditable="true">
import QtQuick 2.1
Rectangle {
width: 100; height: 100
color: "green"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button == Qt.RightButton)
parent.color = 'green';
else
parent.color = 'red';
}
}
}
</pre>
</div>
</body>
</html>
\ No newline at end of file
import QtQuick 2.8
import QtQuick.Controls 2.13
import QtQuick.Particles 2.13
import QtGraphicalEffects 1.13
Text {
text : "Hello World"
}
#ifdef MOCK_RUNTIME
int main(int argc, char **argv)
{
return 0;
}
#else
#include <QtGui/QGuiApplication>
int main(int argc, char **argv)
{
// The Qt Quick Runtime is instantiated and controlled via
// API exported to JavaScript in qtquickwebruntime.h.
QGuiApplication app(argc, argv);
return app.exec();
}
#endif
<!doctype html>
<meta charset="utf-8"/>
<html>
<head>
</head>
<script src='qtloader.js'></script>
<script src='qtquick.js'></script>
<script type='text/javascript'>
var g_qtQuick = undefined;
function onload() {
let log = document.querySelector("#qtlog");
g_qtQuick = new QtQuick({
container: document.querySelector("#qtcontent"),
onStatusChange: function(runState) {
// Maintain log ourput on state changes, log any errors
if (runState == g_qtQuick.RunState.Loading)
log.innerHTML = ""; // clear log on reload
let stateName = Object.keys(g_qtQuick.RunState).find(key => g_qtQuick.RunState[key] === runState)
log.innerHTML += ("Run State Change: " + stateName + "\n")
if (runState == g_qtQuick.RunState.Error)
log.innerHTML += g_qtQuick.errors();
},
onWarning: function(warning) {
// Log warnings as well
log.innerHTML += ("QML Warning: " + warning + "\n")
}
});
let qmlcode = document.querySelector("#qtqmlcode").value;
g_qtQuick.setSourceCode(qmlcode);
g_qtQuick.setContainer(document.querySelector("#qtcontent"));
g_qtQuick.load();
}
function onQmlCodeChange() {
let qmlcode = document.querySelector("#qtqmlcode").value;
g_qtQuick.setSourceCode(qmlcode);
}
</script>
<body onload="onload()">
<textarea id="qtqmlcode" style="width:320px; height:200px;" contenteditable="true" oninput="onQmlCodeChange()">
import QtQuick 2.8
Text {
anchors.centerIn : parent
text : "Hello World"
color : foo
}
</textarea>
<div id="qtcontent" style="width:320px; height:200px;"></div>
<pre id="qtlog"></pre>
<body>
</html>
#QT =
#include ($$PWD/../../qt-quick-runtime-mock.pri)
#DEFINES += MOCK_RUNTIME
#LIBS += -s NO_EXIT_RUNTIME
include ($$PWD/../../qt-quick-runtime.pri)
include ($$PWD/../../qt-quick-web-runtime.pri)
TARGET = qtquickruntime
CONFIG += release
SOURCES += main.cpp
CONFIG += c++11
INCLUDEPATH += $$PWD
OBJECTS_DIR = .obj
MOC_DIR = .moc
HEADERS += \
$$PWD/qtquickruntime.h
SOURCES += \
$$PWD/qtquickruntime_mock.cpp
QT += quick
CONFIG += c++11
INCLUDEPATH += $$PWD
OBJECTS_DIR = .obj
MOC_DIR = .moc
HEADERS += \
$$PWD/qtquickruntime.h \
$$PWD/qmlreloader.h
SOURCES += \
$$PWD/qtquickruntime.cpp \
$$PWD/qmlreloader.cpp
TEMPLATE = app
TARGET = qtquickruntime
QT += quick
CONFIG += c++11
INCLUDEPATH += $$PWD
......@@ -9,9 +5,9 @@ OBJECTS_DIR = .obj
MOC_DIR = .moc
HEADERS += \
$$PWD/qtquickruntime.h \
$$PWD/qmlreloader.h
$$PWD/qtquickwebruntime.h
SOURCES += \
$$PWD/qtquickruntime.cpp \
$$PWD/qmlreloader.cpp
$$PWD/qtquickwebruntime.cpp
QMAKE_POST_LINK += $$quote(cp $${PWD}/qtquick.js $${OUT_PWD}$$escape_expand(\\n\\t))
// This file implements the Qt Quick Runtime JavaScript API.
//
// Basic usage is
// qtQuick = new QtQuick({
// container: some_container_element
// onStatusChange: function(runState) {
// },
// onWarning: function(warning) {
// // Log warnings as well
// log.innerHTML += ("QML Warning: " + warning + "\n")
// }});
//.
//. qtQuick.setSourceCode(qmlcode);
//. g_qtQuick.load();
//
function QtQuick(config)
{
_config = config || {}
_runState = undefined;
_container = config["container"];
_placeholder = undefined;
_canvas = undefined;
_sourceCode = undefined;
_sourceUrl = undefined;
_qtLoader = undefined;
_quickRuntime = undefined
this._createQuickRuntimeInstance = function() {
if (this._quickRuntime !== undefined)
return;
// Create derived QtQuickRuntime class which overrides the
// error and warning handlers on the base QtQuickRuntime class.
var DerivedQtQuickRuntime = Module.QtQuickRuntime.extend("QtQuickRuntime", {
onStatusChange: function(status) {
this._setRunState(status == 0 ? this.RunState.Running : this.RunState.Error);
}.bind(this),
onWarning: function(warning) {
if (config["onWarning"] !== undefined)
config["onWarning"](warning);
}.bind(this)
});
this._quickRuntime = new DerivedQtQuickRuntime;
this._quickRuntime.show();
};
this._updateSourceCode = function() {
// This functions syncs the properties of the JS class with the
// C++ runtime, but only if the runtime module is ready
if (Module === undefined || Module.QtQuickRuntime === undefined) {
console.log("_updateSourceCode with null Module"); // TODO error or not?
return;
}
this._createQuickRuntimeInstance();
if (this._sourceCode != undefined)
this._quickRuntime.qmlSourceCode = this._sourceCode;
if (this._sourceUrl != undefined)
this._quickRuntime.qmlSourceUrl = this._sourceUrl;
}
this.RunState = {
Created : 1,
Loading : 2,
Running : 3,
Error : 4,
};
this._setRunState = function(runState) {
if (this._runState == runState)
return;
this._runState = runState;
if (config["onStatusChange"] !== undefined)
config["onStatusChange"](runState);
}
this.runState = function() {
if (this.RunState === undefined)
return this.RunState.Created;
return this._runState;
};
this.errors = function() {
var errors = [];
if (Module === undefined || Module.QtQuickRuntime === undefined)
return errors;
for (var i = 0; i < this._quickRuntime.errorCount(); ++i)
errors.push(this._quickRuntime.errorString(i));
return errors;
};
this.setSourceCode = function(sourceCode) {
this._setRunState(this.RunState.Loading);
this._sourceCode = sourceCode;
this._sourceUrl = undefined;
this._updateSourceCode();
}
this.setSourceUrl = function(sourceUrl) {
this._setRunState(this.RunState.Loading);
this._sourceUrl = sourceUrl;
this._sourceCode = undefined;
this._updateSourceCode();
}
this.setContainer = function(element) {
this._container = element;
}
this.setPlaceHolder = function(element) {
this._placeholder = element;
}
this.load = function() {
this._setRunState(this.RunState.Loading);
this._canvas = document.createElement("canvas");
this._canvas.id = "qtquickruntime-canvas";
this._canvas.style.width = "100%";
this._canvas.style.height = "100%";
this._container.appendChild(this._canvas);
if (this._placeholder === undefined) {
this._placeholder = document.createElement("div");
this._placeholder.innerHTML = "Loading";
this._placeholder.id = "qtquickruntime-placeholder";
this._placeholder.style.width = "100%";
this._placeholder.style.height = "100%";
}
this._container.appendChild(this._placeholder);
_qtLoader = QtLoader({
canvasElements: [this._canvas],
showLoader: function(loaderStatus) {
console.log("showLoader");
this._placeholder.style.display = 'block';
this._canvas.style.display = 'none';
}.bind(this),
showError: function(errorText) {
console.log("error " + errorText);
this._placeholder.style.display = 'block';
this._canvas.style.display = 'none';
}.bind(this),
showExit: function() {
console.log("exit");
// qtLoader.exitCode
// qtLoader.exitText
}.bind(this),
showCanvas: function() {
// Perform initial source code update and render. ### The Module object is not defined at this point
window.setTimeout(function() {
this._updateSourceCode();
}.bind(this), 100);
this._placeholder.style.display = 'none';
this._canvas.style.display = 'block';
}.bind(this),
});
_qtLoader.loadEmscriptenModule("qtquickruntime");
}
this.delete = function() {
this._quickRuntime.delete();
this._quickRuntime = undefined;
}
}
......@@ -55,18 +55,14 @@ QtQuickRuntime::QtQuickRuntime()
QQuickView::ResizeMode resizeMode = QQuickView::SizeRootObjectToView;
m_qmlReloader->setResizeMode(resizeMode);
QObject::connect(m_qmlReloader, &QmlReloader::statusChanged, [&](int status) {
if (bool(m_statusHandler))
m_statusHandler(status);