summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorten Sørvig <[email protected]>2025-01-27 16:01:24 +0100
committerMorten Sørvig <[email protected]>2025-01-29 13:59:09 +0100
commita9bbcdd7bf0580cf7c0a8faa9779357758d45681 (patch)
tree70859a97c45a254c6424e8d65f302200ede92ebc
parentadce1fe9b5b707dfd0ef492af7a61e6f6f735a1d (diff)
wasm: support foreign windows
Add support for embedding native html elements using QWindow::fromWinId(). WId is an emscripten::val *, e.g. a pointer to val which holds a html element. The element can be created either from C++ using emscripten::val, or from JavaScript. User code owns the val * as usual for WId; ownership is not passed to the QWindow instance. Set QWasmWindow::m_window to be the native element when fromWinId() is used, and skip the rest of the QWasmWindow implementation in that case: We don't need to install event handlers or provide accessibility elements. Make key and pointer event handlers stop propagation only if the event was not accepted. This makes sure that input events reach the embedded native element. Limit setPointerCapture calls to when the event is targeted for Qt elements only. Determining the true target can be a bit tricky when shadow DOM is in use since the browsers may retarget the event. Use composedPath() to get the true event target. Task-number: QTBUG-128804 Task-number: QTBUG-128732 Change-Id: I5ce66e93bacb06abfd042916687cd45fc9588c51 Reviewed-by: Morten Johan Sørvig <[email protected]>
-rw-r--r--src/plugins/platforms/wasm/qwasmevent.cpp11
-rw-r--r--src/plugins/platforms/wasm/qwasmevent.h2
-rw-r--r--src/plugins/platforms/wasm/qwasmintegration.cpp15
-rw-r--r--src/plugins/platforms/wasm/qwasmintegration.h3
-rw-r--r--src/plugins/platforms/wasm/qwasmwindow.cpp41
-rw-r--r--src/plugins/platforms/wasm/qwasmwindow.h2
-rw-r--r--tests/manual/wasm/CMakeLists.txt1
-rw-r--r--tests/manual/wasm/foreignwindows/CMakeLists.txt12
-rw-r--r--tests/manual/wasm/foreignwindows/main.cpp92
9 files changed, 167 insertions, 12 deletions
diff --git a/src/plugins/platforms/wasm/qwasmevent.cpp b/src/plugins/platforms/wasm/qwasmevent.cpp
index 116655f884c..9684e30955d 100644
--- a/src/plugins/platforms/wasm/qwasmevent.cpp
+++ b/src/plugins/platforms/wasm/qwasmevent.cpp
@@ -101,6 +101,17 @@ Event::Event(EventType type, emscripten::val webEvent)
{
}
+bool Event::isTargetedForQtElement() const
+{
+ // Check event target via composedPath, which returns the true path even
+ // if the browser retargets the event for Qt's shadow DOM container. This
+ // is needed to avoid capturing the pointer in cases where foreign html
+ // elements are embedded inside Qt's shadow DOM.
+ emscripten::val path = webEvent.call<emscripten::val>("composedPath");
+ QString topElementClassName = QString::fromEcmaString(path[0]["className"]);
+ return topElementClassName.startsWith("qt-"); // .e.g. qt-window-canvas
+}
+
KeyEvent::KeyEvent(EventType type, emscripten::val event, QWasmDeadKeySupport *deadKeySupport) : Event(type, event)
{
const auto code = event["code"].as<std::string>();
diff --git a/src/plugins/platforms/wasm/qwasmevent.h b/src/plugins/platforms/wasm/qwasmevent.h
index 49429295660..19f112e81dc 100644
--- a/src/plugins/platforms/wasm/qwasmevent.h
+++ b/src/plugins/platforms/wasm/qwasmevent.h
@@ -56,6 +56,8 @@ struct Event
{
Event(EventType type, emscripten::val webEvent);
+ bool isTargetedForQtElement() const;
+
emscripten::val webEvent;
EventType type;
emscripten::val target() const { return webEvent["target"]; }
diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp
index e73f122ae3e..173131ad84a 100644
--- a/src/plugins/platforms/wasm/qwasmintegration.cpp
+++ b/src/plugins/platforms/wasm/qwasmintegration.cpp
@@ -178,17 +178,28 @@ bool QWasmIntegration::hasCapability(QPlatformIntegration::Capability cap) const
case RasterGLSurface: return false; // to enable this you need to fix qopenglwidget and quickwidget for wasm
case MultipleWindows: return true;
case WindowManagement: return true;
+ case ForeignWindows: return true;
case OpenGLOnRasterSurface: return true;
default: return QPlatformIntegration::hasCapability(cap);
}
}
-QPlatformWindow *QWasmIntegration::createPlatformWindow(QWindow *window) const
+QWasmWindow *QWasmIntegration::createWindow(QWindow *window, WId nativeHandle) const
{
auto *wasmScreen = QWasmScreen::get(window->screen());
QWasmCompositor *compositor = wasmScreen->compositor();
return new QWasmWindow(window, wasmScreen->deadKeySupport(), compositor,
- m_backingStores.value(window));
+ m_backingStores.value(window), nativeHandle);
+}
+
+QPlatformWindow *QWasmIntegration::createPlatformWindow(QWindow *window) const
+{
+ return createWindow(window, 0);
+}
+
+QPlatformWindow *QWasmIntegration::createForeignWindow(QWindow *window, WId nativeHandle) const
+{
+ return createWindow(window, nativeHandle);
}
QPlatformBackingStore *QWasmIntegration::createPlatformBackingStore(QWindow *window) const
diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h
index 99ccf333fb2..ae281d90557 100644
--- a/src/plugins/platforms/wasm/qwasmintegration.h
+++ b/src/plugins/platforms/wasm/qwasmintegration.h
@@ -43,6 +43,7 @@ public:
bool hasCapability(QPlatformIntegration::Capability cap) const override;
QPlatformWindow *createPlatformWindow(QWindow *window) const override;
+ QPlatformWindow *createForeignWindow(QWindow *window, WId nativeHandle) const override;
QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override;
#ifndef QT_NO_OPENGL
QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override;
@@ -84,6 +85,8 @@ public:
int touchPoints;
private:
+ QWasmWindow *createWindow(QWindow *, WId nativeHandle) const;
+
struct ScreenMapping {
emscripten::val emscriptenVal;
QWasmScreen *wasmScreen;
diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp
index 036de1514bd..dc092416bd8 100644
--- a/src/plugins/platforms/wasm/qwasmwindow.cpp
+++ b/src/plugins/platforms/wasm/qwasmwindow.cpp
@@ -48,7 +48,7 @@ QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::Windo
Q_GUI_EXPORT int qt_defaultDpiX();
QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport,
- QWasmCompositor *compositor, QWasmBackingStore *backingStore)
+ QWasmCompositor *compositor, QWasmBackingStore *backingStore, WId nativeHandle)
: QPlatformWindow(w),
m_compositor(compositor),
m_backingStore(backingStore),
@@ -65,6 +65,22 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport,
m_nonClientArea = std::make_unique<NonClientArea>(this, m_decoratedWindow);
m_nonClientArea->titleBar()->setTitle(window()->title());
+ // If we are wrapping a foregin window, a.k.a. a native html element then that element becomes
+ // the m_window element. In this case setting up event handlers and accessibility etc is not
+ // needed since that is (presumably) handled by the native html element.
+ //
+ // The WId is an emscripten::val *, owned by QWindow user code. We dereference and make
+ // a copy of the val here and don't strictly need it to be kept alive, but that's an
+ // implementation detail. The pointer will be dereferenced again if the window is destroyed
+ // and recreated.
+ if (nativeHandle) {
+ m_window = *(emscripten::val *)(nativeHandle);
+ m_winId = nativeHandle;
+ m_decoratedWindow.set("id", "qt-window-" + std::to_string(m_winId));
+ m_decoratedWindow.call<void>("appendChild", m_window);
+ return;
+ }
+
m_window.set("className", "qt-window");
m_decoratedWindow.call<void>("appendChild", m_window);
@@ -91,8 +107,8 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport,
const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface;
if (rendersTo2dContext)
m_context2d = m_canvas.call<emscripten::val>("getContext", emscripten::val("2d"));
- static int serialNo = 0;
- m_winId = ++serialNo;
+
+ m_winId = WId(&m_window);
m_decoratedWindow.set("id", "qt-window-" + std::to_string(m_winId));
emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas);
@@ -540,9 +556,10 @@ void QWasmWindow::commitParent(QWasmWindowTreeNode *parent)
void QWasmWindow::handleKeyEvent(const KeyEvent &event)
{
qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent";
- if (processKey(event))
+ if (processKey(event)) {
event.webEvent.call<void>("preventDefault");
- event.webEvent.call<void>("stopPropagation");
+ event.webEvent.call<void>("stopPropagation");
+ }
}
bool QWasmWindow::processKey(const KeyEvent &event)
@@ -667,14 +684,17 @@ void QWasmWindow::processPointer(const PointerEvent &event)
{
switch (event.type) {
case EventType::PointerDown:
- m_window.call<void>("setPointerCapture", event.pointerId);
+ if (event.isTargetedForQtElement())
+ m_window.call<void>("setPointerCapture", event.pointerId);
+
if ((window()->flags() & Qt::WindowDoesNotAcceptFocus)
!= Qt::WindowDoesNotAcceptFocus
&& window()->isTopLevel())
window()->requestActivate();
break;
case EventType::PointerUp:
- m_window.call<void>("releasePointerCapture", event.pointerId);
+ if (event.isTargetedForQtElement())
+ m_window.call<void>("releasePointerCapture", event.pointerId);
break;
default:
break;
@@ -683,8 +703,11 @@ void QWasmWindow::processPointer(const PointerEvent &event)
const bool eventAccepted = deliverPointerEvent(event);
if (!eventAccepted && event.type == EventType::PointerDown)
QGuiApplicationPrivate::instance()->closeAllPopups();
- event.webEvent.call<void>("preventDefault");
- event.webEvent.call<void>("stopPropagation");
+
+ if (eventAccepted) {
+ event.webEvent.call<void>("preventDefault");
+ event.webEvent.call<void>("stopPropagation");
+ }
}
bool QWasmWindow::deliverPointerEvent(const PointerEvent &event)
diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h
index f1789a6dab1..5d427f88efb 100644
--- a/src/plugins/platforms/wasm/qwasmwindow.h
+++ b/src/plugins/platforms/wasm/qwasmwindow.h
@@ -42,7 +42,7 @@ class QWasmWindow final : public QPlatformWindow,
{
public:
QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor,
- QWasmBackingStore *backingStore);
+ QWasmBackingStore *backingStore, WId nativeHandle);
~QWasmWindow() final;
static QWasmWindow *fromWindow(QWindow *window);
diff --git a/tests/manual/wasm/CMakeLists.txt b/tests/manual/wasm/CMakeLists.txt
index f2997937d7b..512583e50d7 100644
--- a/tests/manual/wasm/CMakeLists.txt
+++ b/tests/manual/wasm/CMakeLists.txt
@@ -11,4 +11,5 @@ add_subdirectory(localfonts)
add_subdirectory(qstdweb)
add_subdirectory(clipboard)
add_subdirectory(windowmanagement)
+add_subdirectory(foreignwindows)
endif()
diff --git a/tests/manual/wasm/foreignwindows/CMakeLists.txt b/tests/manual/wasm/foreignwindows/CMakeLists.txt
new file mode 100644
index 00000000000..57f270e1176
--- /dev/null
+++ b/tests/manual/wasm/foreignwindows/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(foreignwindows
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
diff --git a/tests/manual/wasm/foreignwindows/main.cpp b/tests/manual/wasm/foreignwindows/main.cpp
new file mode 100644
index 00000000000..3934d10e0c7
--- /dev/null
+++ b/tests/manual/wasm/foreignwindows/main.cpp
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtWidgets>
+
+#include <emscripten.h>
+#include <emscripten/val.h>
+
+using emscripten::val;
+using emscripten::EM_VAL;
+
+
+val createInputElemet(std::string subtype)
+{
+ val document = val::global("document");
+ val input = document.call<val>("createElement", std::string("input"));
+ input.set("type", subtype);
+ return input;
+}
+
+EM_JS(EM_VAL, createCalendar, (), {
+ var calendar = document.createElement("calendar-date");
+ calendar.innerHTML = "<calendar-month></calendar-month>";
+ return Emval.toHandle(calendar);
+});
+
+val createCallyElemet()
+{
+ static bool initializedCalendarComponent = []{
+ return EM_ASM_INT(
+ console.log("Loading calendar module");
+ var script = document.createElement('script');
+ script.src = "https://unpkg.com/cally";
+ script.type = "module";
+ document.head.appendChild(script);
+ console.log(script);
+ return true;
+ );
+ }();
+ Q_ASSERT(initializedCalendarComponent);
+
+ return val::take_ownership(createCalendar());
+}
+
+class ForeginWindowContainer : public QWidget
+{
+Q_OBJECT
+public:
+ ForeginWindowContainer() {
+
+ QCheckBox *test = new QCheckBox("Qt CheckBox");
+ test->setGeometry(20, 20, 150, 20);
+ test->setParent(this);
+
+ m_calendarInput = std::make_unique<val>(createInputElemet("date"));
+ m_colorPickerInput = std::make_unique<val>(createInputElemet("color"));
+
+ QWindow *calendarWindow = QWindow::fromWinId(WId(m_calendarInput.get()));
+ QWindow *colorPickerWindow = QWindow::fromWinId(WId(m_colorPickerInput.get()));
+
+ QWidget *calendarContainer = QWidget::createWindowContainer(calendarWindow, this);
+ calendarContainer->setGeometry(20, 50, 300, 40);
+
+ QWidget *colorPickerContainer = QWidget::createWindowContainer(colorPickerWindow, this);
+ colorPickerContainer->setGeometry(20, 90, 300, 40);
+
+ val callyCalendarElement = createCallyElemet();
+ QWindow *callyWindow =QWindow::fromWinId(WId(new val(callyCalendarElement)));
+ QWidget *callyContainer = QWidget::createWindowContainer(callyWindow, this);
+ callyContainer->setGeometry(20, 130, 300, 400);
+ }
+
+ ~ForeginWindowContainer() {
+ }
+
+private:
+ std::unique_ptr<val> m_calendarInput;
+ std::unique_ptr<val> m_colorPickerInput;
+};
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ QGuiApplication::styleHints()->setColorScheme(Qt::ColorScheme::Light);
+
+ ForeginWindowContainer container;
+ container.showNormal();
+
+ return app.exec();
+}
+
+#include "main.moc"