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("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(); 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(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("appendChild", m_window); + return; + } + m_window.set("className", "qt-window"); m_decoratedWindow.call("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("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("preventDefault"); - event.webEvent.call("stopPropagation"); + event.webEvent.call("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("setPointerCapture", event.pointerId); + if (event.isTargetedForQtElement()) + m_window.call("setPointerCapture", event.pointerId); + if ((window()->flags() & Qt::WindowDoesNotAcceptFocus) != Qt::WindowDoesNotAcceptFocus && window()->isTopLevel()) window()->requestActivate(); break; case EventType::PointerUp: - m_window.call("releasePointerCapture", event.pointerId); + if (event.isTargetedForQtElement()) + m_window.call("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("preventDefault"); - event.webEvent.call("stopPropagation"); + + if (eventAccepted) { + event.webEvent.call("preventDefault"); + event.webEvent.call("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 + +#include +#include + +using emscripten::val; +using emscripten::EM_VAL; + + +val createInputElemet(std::string subtype) +{ + val document = val::global("document"); + val input = document.call("createElement", std::string("input")); + input.set("type", subtype); + return input; +} + +EM_JS(EM_VAL, createCalendar, (), { + var calendar = document.createElement("calendar-date"); + calendar.innerHTML = ""; + 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(createInputElemet("date")); + m_colorPickerInput = std::make_unique(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 m_calendarInput; + std::unique_ptr 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"