diff options
Diffstat (limited to 'src/plugins/platforms/wasm/qwasmwindow.cpp')
-rw-r--r-- | src/plugins/platforms/wasm/qwasmwindow.cpp | 890 |
1 files changed, 492 insertions, 398 deletions
diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 02298bb4a1..b8197c5113 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -3,388 +3,385 @@ #include <qpa/qwindowsysteminterface.h> #include <private/qguiapplication_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#include <QtCore/qfile.h> #include <QtGui/private/qwindow_p.h> -#include <QtGui/qopenglcontext.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <private/qpixmapcache_p.h> - +#include <QtGui/qopenglfunctions.h> +#include <QBuffer> + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmclipboard.h" +#include "qwasmintegration.h" +#include "qwasmkeytranslator.h" #include "qwasmwindow.h" +#include "qwasmwindowclientarea.h" #include "qwasmscreen.h" -#include "qwasmstylepixmaps_p.h" #include "qwasmcompositor.h" +#include "qwasmevent.h" #include "qwasmeventdispatcher.h" +#include "qwasmaccessibility.h" +#include "qwasmclipboard.h" #include <iostream> +#include <sstream> +#include <emscripten/val.h> -QT_BEGIN_NAMESPACE +#include <QtCore/private/qstdweb_p.h> -Q_GUI_EXPORT int qt_defaultDpiX(); +QT_BEGIN_NAMESPACE namespace { -// from commonstyle.cpp -static QPixmap cachedPixmapFromXPM(const char *const *xpm) -{ - QPixmap result; - const QString tag = QString::asprintf("xpm:0x%p", static_cast<const void *>(xpm)); - if (!QPixmapCache::find(tag, &result)) { - result = QPixmap(xpm); - QPixmapCache::insert(tag, result); - } - return result; -} - -QPalette makePalette() +QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) { - QPalette palette; - palette.setColor(QPalette::Active, QPalette::Highlight, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Active, QPalette::Base, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Inactive, QPalette::Highlight, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::Base, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::HighlightedText, - palette.color(QPalette::Inactive, QPalette::Window)); - - return palette; + if (flags.testFlag(Qt::WindowStaysOnTopHint)) + return QWasmWindowStack::PositionPreference::StayOnTop; + if (flags.testFlag(Qt::WindowStaysOnBottomHint)) + return QWasmWindowStack::PositionPreference::StayOnBottom; + return QWasmWindowStack::PositionPreference::Regular; } +} // namespace -void drawItemPixmap(QPainter *painter, const QRect &rect, int alignment, const QPixmap &pixmap) -{ - qreal scale = pixmap.devicePixelRatio(); - QSize size = pixmap.size() / scale; - int x = rect.x(); - int y = rect.y(); - int w = size.width(); - int h = size.height(); - if ((alignment & Qt::AlignVCenter) == Qt::AlignVCenter) - y += rect.size().height() / 2 - h / 2; - else if ((alignment & Qt::AlignBottom) == Qt::AlignBottom) - y += rect.size().height() - h; - if ((alignment & Qt::AlignRight) == Qt::AlignRight) - x += rect.size().width() - w; - else if ((alignment & Qt::AlignHCenter) == Qt::AlignHCenter) - x += rect.size().width() / 2 - w / 2; - - QRect aligned = QRect(x, y, w, h); - QRect inter = aligned.intersected(rect); - - painter->drawPixmap(inter.x(), inter.y(), pixmap, inter.x() - aligned.x(), - inter.y() - aligned.y(), inter.width() * scale, inter.height() * scale); -} -} +Q_GUI_EXPORT int qt_defaultDpiX(); -QWasmWindow::QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore) +QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, + QWasmCompositor *compositor, QWasmBackingStore *backingStore) : QPlatformWindow(w), m_window(w), m_compositor(compositor), - m_backingStore(backingStore) + m_backingStore(backingStore), + m_deadKeySupport(deadKeySupport), + m_document(dom::document()), + m_qtWindow(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_windowContents(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvasContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_a11yContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvas(m_document.call<emscripten::val>("createElement", emscripten::val("canvas"))) { - m_needsCompositor = w->surfaceType() != QSurface::OpenGLSurface; + m_qtWindow.set("className", "qt-window"); + m_qtWindow["style"].set("display", std::string("none")); + + m_nonClientArea = std::make_unique<NonClientArea>(this, m_qtWindow); + m_nonClientArea->titleBar()->setTitle(window()->title()); + + m_clientArea = std::make_unique<ClientArea>(this, compositor->screen(), m_windowContents); + + m_windowContents.set("className", "qt-window-contents"); + m_qtWindow.call<void>("appendChild", m_windowContents); + + m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content")); + + // Set contenteditable so that the canvas gets clipboard events, + // then hide the resulting focus frame. + m_canvas.set("contentEditable", std::string("true")); + m_canvas["style"].set("outline", std::string("none")); + + QWasmClipboard::installEventHandlers(m_canvas); + + // set inputMode to none to stop mobile keyboard opening + // when user clicks anywhere on the canvas. + m_canvas.set("inputMode", std::string("none")); + + // Hide the canvas from screen readers. + m_canvas.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); + + m_windowContents.call<void>("appendChild", m_canvasContainer); + + m_canvasContainer["classList"].call<void>("add", emscripten::val("qt-window-canvas-container")); + m_canvasContainer.call<void>("appendChild", m_canvas); + + m_canvasContainer.call<void>("appendChild", m_a11yContainer); + m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container")); + + 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 = ++serialNo; + m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); + emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); + + m_flags = window()->flags(); + + const auto pointerCallback = std::function([this](emscripten::val event) { + if (processPointer(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + m_pointerEnterCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerenter", pointerCallback); + m_pointerLeaveCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerleave", pointerCallback); + + m_wheelEventCallback = std::make_unique<qstdweb::EventCallback>( + m_qtWindow, "wheel", [this](emscripten::val event) { + if (processWheel(*WheelEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + const auto keyCallback = std::function([this](emscripten::val event) { + if (processKey(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + emscripten::val keyFocusWindow; + if (QWasmIntegration::get()->inputContext()) { + QWasmInputContext *wasmContext = + static_cast<QWasmInputContext *>(QWasmIntegration::get()->inputContext()); + // if there is an touchscreen input context, + // use that window for key input + keyFocusWindow = wasmContext->m_inputElement; + } else { + keyFocusWindow = m_qtWindow; + } - m_compositor->addWindow(this); + m_keyDownCallback = + std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keydown", keyCallback); + m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keyup", keyCallback); - // Pure OpenGL windows draw directly using egl, disable the compositor. - m_compositor->setEnabled(w->surfaceType() != QSurface::OpenGLSurface); + setParent(parent()); } QWasmWindow::~QWasmWindow() { - m_compositor->removeWindow(this); + emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); + m_canvasContainer.call<void>("removeChild", m_canvas); + m_context2d = emscripten::val::undefined(); + commitParent(nullptr); if (m_requestAnimationFrameId > -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); +#if QT_CONFIG(accessibility) + QWasmAccessibility::removeAccessibilityEnableButton(window()); +#endif } -void QWasmWindow::destroy() +QSurfaceFormat QWasmWindow::format() const { - if (m_backingStore) - m_backingStore->destroy(); + return window()->requestedFormat(); } -void QWasmWindow::initialize() +QWasmWindow *QWasmWindow::fromWindow(QWindow *window) { - QRect rect = windowGeometry(); - - QPlatformWindow::setGeometry(rect); - - const QSize minimumSize = windowMinimumSize(); - if (rect.width() > 0 || rect.height() > 0) { - rect.setWidth(qBound(1, rect.width(), 2000)); - rect.setHeight(qBound(1, rect.height(), 2000)); - } else if (minimumSize.width() > 0 || minimumSize.height() > 0) { - rect.setSize(minimumSize); - } - - setWindowState(window()->windowStates()); - setWindowFlags(window()->flags()); - setWindowTitle(window()->title()); - if (window()->isTopLevel()) - setWindowIcon(window()->icon()); - m_normalGeometry = rect; + return static_cast<QWasmWindow *>(window->handle()); } -QWasmScreen *QWasmWindow::platformScreen() const +void QWasmWindow::onRestoreClicked() { - return static_cast<QWasmScreen *>(window()->screen()->handle()); + window()->setWindowState(Qt::WindowNoState); } -void QWasmWindow::setGeometry(const QRect &rect) +void QWasmWindow::onMaximizeClicked() { - const QRect clientAreaRect = ([this, &rect]() { - if (!m_needsCompositor) - return rect; + window()->setWindowState(Qt::WindowMaximized); +} - const int captionHeight = window()->geometry().top() - window()->frameGeometry().top(); - const auto screenGeometry = screen()->geometry(); +void QWasmWindow::onToggleMaximized() +{ + window()->setWindowState(m_state.testFlag(Qt::WindowMaximized) ? Qt::WindowNoState + : Qt::WindowMaximized); +} - QRect result(rect); - result.moveTop(std::max(std::min(rect.y(), screenGeometry.bottom()), - screenGeometry.y() + captionHeight)); - return result; - })(); - bool shouldInvalidate = true; - if (!m_windowState.testFlag(Qt::WindowFullScreen) - && !m_windowState.testFlag(Qt::WindowMaximized)) { - shouldInvalidate = m_normalGeometry.size() != clientAreaRect.size(); - m_normalGeometry = clientAreaRect; - } - QWindowSystemInterface::handleGeometryChange(window(), clientAreaRect); - if (shouldInvalidate) - invalidate(); - else - m_compositor->requestUpdateWindow(this); +void QWasmWindow::onCloseClicked() +{ + window()->close(); } -void QWasmWindow::setVisible(bool visible) +void QWasmWindow::onNonClientAreaInteraction() { - if (visible) - applyWindowState(); - m_compositor->setVisible(this, visible); + requestActivateWindow(); + QGuiApplicationPrivate::instance()->closeAllPopups(); } -bool QWasmWindow::isVisible() +bool QWasmWindow::onNonClientEvent(const PointerEvent &event) { - return window()->isVisible(); + QPointF pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + return QWindowSystemInterface::handleMouseEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, event.mouseButtons, event.mouseButton, + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::NonClient), + event.modifiers); } -QMargins QWasmWindow::frameMargins() const +void QWasmWindow::initialize() { - int border = hasTitleBar() ? 4. * (qreal(qt_defaultDpiX()) / 96.0) : 0; - int titleBarHeight = hasTitleBar() ? titleHeight() : 0; + auto initialGeometry = QPlatformWindow::initialGeometry(window(), + windowGeometry(), defaultWindowSize, defaultWindowSize); + m_normalGeometry = initialGeometry; + + setWindowState(window()->windowStates()); + setWindowFlags(window()->flags()); + setWindowTitle(window()->title()); + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); - QMargins margins; - margins.setLeft(border); - margins.setRight(border); - margins.setTop(2*border + titleBarHeight); - margins.setBottom(border); + if (window()->isTopLevel()) + setWindowIcon(window()->icon()); + QPlatformWindow::setGeometry(m_normalGeometry); - return margins; +#if QT_CONFIG(accessibility) + // Add accessibility-enable button. The user can activate this + // button to opt-in to accessibility. + if (window()->isTopLevel()) + QWasmAccessibility::addAccessibilityEnableButton(window()); +#endif } -void QWasmWindow::raise() +QWasmScreen *QWasmWindow::platformScreen() const { - m_compositor->raise(this); - invalidate(); + return static_cast<QWasmScreen *>(window()->screen()->handle()); } -void QWasmWindow::lower() +void QWasmWindow::paint() { - m_compositor->lower(this); - invalidate(); + if (!m_backingStore || !isVisible() || m_context2d.isUndefined()) + return; + + auto image = m_backingStore->getUpdatedWebImage(this); + if (image.isUndefined()) + return; + m_context2d.call<void>("putImageData", image, emscripten::val(0), emscripten::val(0)); } -WId QWasmWindow::winId() const +void QWasmWindow::setZOrder(int z) { - return m_winid; + m_qtWindow["style"].set("zIndex", std::to_string(z)); } -void QWasmWindow::propagateSizeHints() +void QWasmWindow::setWindowCursor(QByteArray cssCursorName) { - QRect rect = windowGeometry(); - if (rect.size().width() < windowMinimumSize().width() - && rect.size().height() < windowMinimumSize().height()) { - rect.setSize(windowMinimumSize()); - setGeometry(rect); - } + m_windowContents["style"].set("cursor", emscripten::val(cssCursorName.constData())); } -bool QWasmWindow::startSystemResize(Qt::Edges edges) +void QWasmWindow::setGeometry(const QRect &rect) { - m_compositor->startResize(edges); + const auto margins = frameMargins(); - return true; -} + const QRect clientAreaRect = ([this, &rect, &margins]() { + if (m_state.testFlag(Qt::WindowFullScreen)) + return platformScreen()->geometry(); + if (m_state.testFlag(Qt::WindowMaximized)) + return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); -void QWasmWindow::injectMousePressed(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) -{ - Q_UNUSED(local); - Q_UNUSED(mods); + auto offset = rect.topLeft() - (!parent() ? screen()->geometry().topLeft() : QPoint()); - if (!hasTitleBar() || button != Qt::LeftButton) - return; + // In viewport + auto containerGeometryInViewport = + QRectF::fromDOMRect(parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")) + .toRect(); - if (const auto controlHit = titleBarHitTest(global)) - m_activeControl = *controlHit; + auto rectInViewport = QRect(containerGeometryInViewport.topLeft() + offset, rect.size()); - invalidate(); -} + QRect cappedGeometry(rectInViewport); + if (!parent()) { + // Clamp top level windows top position to the screen bounds + cappedGeometry.moveTop( + std::max(std::min(rectInViewport.y(), containerGeometryInViewport.bottom()), + containerGeometryInViewport.y() + margins.top())); + } + cappedGeometry.setSize( + cappedGeometry.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); + return QRect(QPoint(rect.x(), rect.y() + cappedGeometry.y() - rectInViewport.y()), + rect.size()); + })(); + m_nonClientArea->onClientAreaWidthChange(clientAreaRect.width()); -void QWasmWindow::injectMouseReleased(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) -{ - Q_UNUSED(local); - Q_UNUSED(mods); + const auto frameRect = + clientAreaRect + .adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) + .translated(!parent() ? -screen()->geometry().topLeft() : QPoint()); - if (!hasTitleBar() || button != Qt::LeftButton) - return; + m_qtWindow["style"].set("left", std::to_string(frameRect.left()) + "px"); + m_qtWindow["style"].set("top", std::to_string(frameRect.top()) + "px"); + m_canvasContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_canvasContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + m_a11yContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_a11yContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); - if (const auto controlHit = titleBarHitTest(global)) { - if (m_activeControl == *controlHit) { - switch (*controlHit) { - case SC_TitleBarCloseButton: - window()->close(); - break; - case SC_TitleBarMaxButton: - window()->setWindowState(Qt::WindowMaximized); - break; - case SC_TitleBarNormalButton: - window()->setWindowState(Qt::WindowNoState); - break; - case SC_None: - case SC_TitleBarLabel: - case SC_TitleBarSysMenu: - Q_ASSERT(false); // These types are not clickable - return; - } - } - } + // Important for the title flexbox to shrink correctly + m_windowContents["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); - m_activeControl = SC_None; + QSizeF canvasSize = clientAreaRect.size() * devicePixelRatio(); - invalidate(); -} + m_canvas.set("width", canvasSize.width()); + m_canvas.set("height", canvasSize.height()); -int QWasmWindow::titleHeight() const -{ - return 18. * (qreal(qt_defaultDpiX()) / 96.0);//dpiScaled(18.); + bool shouldInvalidate = true; + if (!m_state.testFlag(Qt::WindowFullScreen) && !m_state.testFlag(Qt::WindowMaximized)) { + shouldInvalidate = m_normalGeometry.size() != clientAreaRect.size(); + m_normalGeometry = clientAreaRect; + } + QWindowSystemInterface::handleGeometryChange(window(), clientAreaRect); + if (shouldInvalidate) + invalidate(); + else + m_compositor->requestUpdateWindow(this); } -int QWasmWindow::borderWidth() const +void QWasmWindow::setVisible(bool visible) { - return 4. * (qreal(qt_defaultDpiX()) / 96.0);// dpiScaled(4.); + // TODO(mikolajboc): isVisible()? + const bool nowVisible = m_qtWindow["style"]["display"].as<std::string>() == "block"; + if (visible == nowVisible) + return; + + m_compositor->requestUpdateWindow(this, QWasmCompositor::ExposeEventDelivery); + m_qtWindow["style"].set("display", visible ? "block" : "none"); + if (window()->isActive()) + m_canvas.call<void>("focus"); + if (visible) + applyWindowState(); } -QRegion QWasmWindow::resizeRegion() const +bool QWasmWindow::isVisible() const { - int border = borderWidth(); - QRegion result(window()->frameGeometry().adjusted(-border, -border, border, border)); - result -= window()->frameGeometry().adjusted(border, border, -border, -border); - - return result; + return window()->isVisible(); } -bool QWasmWindow::isPointOnTitle(QPoint globalPoint) const +QMargins QWasmWindow::frameMargins() const { - const auto pointInFrameCoords = globalPoint - windowFrameGeometry().topLeft(); - if (const auto titleRect = - getTitleBarControlRect(makeTitleBarOptions(), TitleBarControl::SC_TitleBarLabel)) { - return titleRect->contains(pointInFrameCoords); - } - return false; + const auto frameRect = + QRectF::fromDOMRect(m_qtWindow.call<emscripten::val>("getBoundingClientRect")); + const auto canvasRect = + QRectF::fromDOMRect(m_windowContents.call<emscripten::val>("getBoundingClientRect")); + return QMarginsF(canvasRect.left() - frameRect.left(), canvasRect.top() - frameRect.top(), + frameRect.right() - canvasRect.right(), + frameRect.bottom() - canvasRect.bottom()) + .toMargins(); } -bool QWasmWindow::isPointOnResizeRegion(QPoint point) const +void QWasmWindow::raise() { - // Certain windows, like undocked dock widgets, are both popups and dialogs. Those should be - // resizable. - if (windowIsPopupType(window()->flags())) - return false; - return (window()->maximumSize().isEmpty() || window()->minimumSize() != window()->maximumSize()) - && resizeRegion().contains(point); + bringToTop(); + invalidate(); + if (QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); } -Qt::Edges QWasmWindow::resizeEdgesAtPoint(QPoint point) const +void QWasmWindow::lower() { - const QPoint topLeft = window()->frameGeometry().topLeft() - QPoint(5, 5); - const QPoint bottomRight = window()->frameGeometry().bottomRight() + QPoint(5, 5); - const int gripAreaWidth = std::min(20, (bottomRight.y() - topLeft.y()) / 2); - - const QRect top(topLeft, QPoint(bottomRight.x(), topLeft.y() + gripAreaWidth)); - const QRect bottom(QPoint(topLeft.x(), bottomRight.y() - gripAreaWidth), bottomRight); - const QRect left(topLeft, QPoint(topLeft.x() + gripAreaWidth, bottomRight.y())); - const QRect right(QPoint(bottomRight.x() - gripAreaWidth, topLeft.y()), bottomRight); - - Q_ASSERT(!top.intersects(bottom)); - Q_ASSERT(!left.intersects(right)); - - Qt::Edges edges(top.contains(point) ? Qt::Edge::TopEdge : Qt::Edge(0)); - edges |= bottom.contains(point) ? Qt::Edge::BottomEdge : Qt::Edge(0); - edges |= left.contains(point) ? Qt::Edge::LeftEdge : Qt::Edge(0); - return edges | (right.contains(point) ? Qt::Edge::RightEdge : Qt::Edge(0)); + sendToBottom(); + invalidate(); } -std::optional<QRect> QWasmWindow::getTitleBarControlRect(const TitleBarOptions &tb, - TitleBarControl control) const +WId QWasmWindow::winId() const { - const auto leftToRightRect = getTitleBarControlRectLeftToRight(tb, control); - if (!leftToRightRect) - return std::nullopt; - return qApp->layoutDirection() == Qt::LeftToRight - ? leftToRightRect - : leftToRightRect->translated(2 * (tb.rect.right() - leftToRightRect->right()) - + leftToRightRect->width() - tb.rect.width(), - 0); + return m_winId; } -bool QWasmWindow::TitleBarOptions::hasControl(TitleBarControl control) const +void QWasmWindow::propagateSizeHints() { - return subControls.testFlag(control); + // setGeometry() will take care of minimum and maximum size constraints + setGeometry(windowGeometry()); + m_nonClientArea->propagateSizeHints(); } -std::optional<QRect> QWasmWindow::getTitleBarControlRectLeftToRight(const TitleBarOptions &tb, - TitleBarControl control) const +void QWasmWindow::setOpacity(qreal level) { - if (!tb.hasControl(control)) - return std::nullopt; - - const int controlMargin = 2; - const int controlHeight = tb.rect.height() - controlMargin * 2; - const int controlWidth = controlHeight; - const int delta = controlWidth + controlMargin; - int offsetRight = 0; - - switch (control) { - case SC_TitleBarLabel: { - const int leftOffset = tb.hasControl(SC_TitleBarSysMenu) ? delta : 0; - const int rightOffset = (tb.hasControl(SC_TitleBarCloseButton) ? delta : 0) - + ((tb.hasControl(SC_TitleBarMaxButton) || tb.hasControl(SC_TitleBarNormalButton)) - ? delta - : 0); - - return tb.rect.adjusted(leftOffset, 0, -rightOffset, 0); - } - case SC_TitleBarSysMenu: - return QRect(tb.rect.left() + controlMargin, tb.rect.top() + controlMargin, controlWidth, - controlHeight); - case SC_TitleBarCloseButton: - offsetRight = delta; - break; - case SC_TitleBarMaxButton: - case SC_TitleBarNormalButton: - offsetRight = delta + (tb.hasControl(SC_TitleBarCloseButton) ? delta : 0); - break; - case SC_None: - Q_ASSERT(false); - break; - }; - - return QRect(tb.rect.right() - offsetRight, tb.rect.top() + controlMargin, controlWidth, - controlHeight); + m_qtWindow["style"].set("opacity", qBound(0.0, level, 1.0)); } void QWasmWindow::invalidate() @@ -392,162 +389,170 @@ void QWasmWindow::invalidate() m_compositor->requestUpdateWindow(this); } -QWasmWindow::TitleBarControl QWasmWindow::activeTitleBarControl() const +void QWasmWindow::onActivationChanged(bool active) { - return m_activeControl; + dom::syncCSSClassWith(m_qtWindow, "inactive", !active); } -std::optional<QWasmWindow::TitleBarControl> -QWasmWindow::titleBarHitTest(const QPoint &globalPoint) const +void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) { - const auto pointInFrameCoords = globalPoint - windowFrameGeometry().topLeft(); - const auto options = makeTitleBarOptions(); + if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint) + || flags.testFlag(Qt::WindowStaysOnBottomHint) + != m_flags.testFlag(Qt::WindowStaysOnBottomHint)) { + onPositionPreferenceChanged(positionPreferenceFromWindowFlags(flags)); + } + m_flags = flags; + dom::syncCSSClassWith(m_qtWindow, "frameless", !hasFrame()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "has-shadow", hasShadow()); + dom::syncCSSClassWith(m_qtWindow, "has-title", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "transparent-for-input", + flags.testFlag(Qt::WindowTransparentForInput)); - static constexpr TitleBarControl Controls[] = { SC_TitleBarMaxButton, SC_TitleBarCloseButton, - SC_TitleBarNormalButton }; - auto found = std::find_if(std::begin(Controls), std::end(Controls), - [this, &pointInFrameCoords, &options](TitleBarControl control) { - auto controlRect = getTitleBarControlRect(options, control); - return controlRect && controlRect->contains(pointInFrameCoords); - }); - return found != std::end(Controls) ? *found : std::optional<TitleBarControl>(); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + m_nonClientArea->titleBar()->setCloseVisible(m_flags.testFlag(Qt::WindowCloseButtonHint)); } void QWasmWindow::setWindowState(Qt::WindowStates newState) { - const Qt::WindowStates oldState = m_windowState; - bool isActive = oldState.testFlag(Qt::WindowActive); + // Child windows can not have window states other than Qt::WindowActive + if (parent()) + newState &= Qt::WindowActive; + + const Qt::WindowStates oldState = m_state; if (newState.testFlag(Qt::WindowMinimized)) { newState.setFlag(Qt::WindowMinimized, false); qWarning("Qt::WindowMinimized is not implemented in wasm"); + window()->setWindowStates(newState); + return; } - // Always keep OpenGL apps fullscreen - if (!m_needsCompositor && !newState.testFlag(Qt::WindowFullScreen)) { - newState.setFlag(Qt::WindowFullScreen, true); - qWarning("Qt::WindowFullScreen must be set for OpenGL surfaces"); - } - - // Ignore WindowActive flag in comparison, as we want to preserve it either way - if ((newState & ~Qt::WindowActive) == (oldState & ~Qt::WindowActive)) + if (newState == oldState) return; - newState.setFlag(Qt::WindowActive, isActive); - + m_state = newState; m_previousWindowState = oldState; - m_windowState = newState; - if (isVisible()) { - applyWindowState(); + applyWindowState(); +} + +void QWasmWindow::setWindowTitle(const QString &title) +{ + m_nonClientArea->titleBar()->setTitle(title); +} + +void QWasmWindow::setWindowIcon(const QIcon &icon) +{ + const auto dpi = screen()->devicePixelRatio(); + auto pixmap = icon.pixmap(10 * dpi, 10 * dpi); + if (pixmap.isNull()) { + m_nonClientArea->titleBar()->setIcon( + Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + return; } + + QByteArray bytes; + QBuffer buffer(&bytes); + pixmap.save(&buffer, "png"); + m_nonClientArea->titleBar()->setIcon(bytes.toBase64().toStdString(), "png"); } void QWasmWindow::applyWindowState() { QRect newGeom; - if (m_windowState.testFlag(Qt::WindowFullScreen)) + const bool isFullscreen = m_state.testFlag(Qt::WindowFullScreen); + const bool isMaximized = m_state.testFlag(Qt::WindowMaximized); + if (isFullscreen) newGeom = platformScreen()->geometry(); - else if (m_windowState.testFlag(Qt::WindowMaximized)) - newGeom = platformScreen()->availableGeometry(); + else if (isMaximized) + newGeom = platformScreen()->availableGeometry().marginsRemoved(frameMargins()); else newGeom = normalGeometry(); - QWindowSystemInterface::handleWindowStateChanged(window(), m_windowState, m_previousWindowState); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "maximized", isMaximized); + + m_nonClientArea->titleBar()->setRestoreVisible(isMaximized); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + + if (isVisible()) + QWindowSystemInterface::handleWindowStateChanged(window(), m_state, m_previousWindowState); setGeometry(newGeom); } -void QWasmWindow::drawTitleBar(QPainter *painter) const +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) { - const auto tb = makeTitleBarOptions(); - if (const auto ir = getTitleBarControlRect(tb, SC_TitleBarLabel)) { - QColor left = tb.palette.highlight().color(); - QColor right = tb.palette.base().color(); - - QBrush fillBrush(left); - if (left != right) { - QPoint p1(tb.rect.x(), tb.rect.top() + tb.rect.height() / 2); - QPoint p2(tb.rect.right(), tb.rect.top() + tb.rect.height() / 2); - QLinearGradient lg(p1, p2); - lg.setColorAt(0, left); - lg.setColorAt(1, right); - fillBrush = lg; - } - - painter->fillRect(tb.rect, fillBrush); - painter->setPen(tb.palette.highlightedText().color()); - painter->drawText(ir->x() + 2, ir->y(), ir->width() - 2, ir->height(), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, - tb.titleBarOptionsString); - } + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; +} - if (const auto ir = getTitleBarControlRect(tb, SC_TitleBarCloseButton)) { - drawItemPixmap(painter, *ir, Qt::AlignCenter, - cachedPixmapFromXPM(qt_close_xpm).scaled(QSize(10, 10))); - } +bool QWasmWindow::processKey(const KeyEvent &event) +{ + constexpr bool ProceedToNativeEvent = false; + Q_ASSERT(event.type == EventType::KeyDown || event.type == EventType::KeyUp); - if (const auto ir = getTitleBarControlRect(tb, SC_TitleBarMaxButton)) { - drawItemPixmap(painter, *ir, Qt::AlignCenter, - cachedPixmapFromXPM(qt_maximize_xpm).scaled(QSize(10, 10))); - } + const auto clipboardResult = + QWasmIntegration::get()->getWasmClipboard()->processKeyboard(event); - if (const auto ir = getTitleBarControlRect(tb, SC_TitleBarNormalButton)) { - drawItemPixmap(painter, *ir, Qt::AlignCenter, - cachedPixmapFromXPM(qt_normalizeup_xpm).scaled(QSize(10, 10))); - } + using ProcessKeyboardResult = QWasmClipboard::ProcessKeyboardResult; + if (clipboardResult == ProcessKeyboardResult::NativeClipboardEventNeeded) + return ProceedToNativeEvent; - if (const auto ir = getTitleBarControlRect(tb, SC_TitleBarSysMenu)) { - if (!tb.windowIcon.isNull()) { - tb.windowIcon.paint(painter, *ir, Qt::AlignCenter); - } else { - drawItemPixmap(painter, *ir, Qt::AlignCenter, - cachedPixmapFromXPM(qt_menu_xpm).scaled(QSize(10, 10))); - } - } + const auto result = QWindowSystemInterface::handleKeyEvent( + 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, + event.modifiers, event.text); + return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + ? ProceedToNativeEvent + : result; } -QWasmWindow::TitleBarOptions QWasmWindow::makeTitleBarOptions() const +bool QWasmWindow::processPointer(const PointerEvent &event) { - int width = windowFrameGeometry().width(); - int border = borderWidth(); - - TitleBarOptions titleBarOptions; - - titleBarOptions.rect = QRect(border, border, width - 2 * border, titleHeight()); - titleBarOptions.flags = window()->flags(); - titleBarOptions.state = window()->windowState(); - - bool isMaximized = - titleBarOptions.state & Qt::WindowMaximized; // this gets reset when maximized + if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) + return false; - if (titleBarOptions.flags & (Qt::WindowTitleHint)) - titleBarOptions.subControls |= SC_TitleBarLabel; - if (titleBarOptions.flags & Qt::WindowMaximizeButtonHint) { - if (isMaximized) - titleBarOptions.subControls |= SC_TitleBarNormalButton; - else - titleBarOptions.subControls |= SC_TitleBarMaxButton; + switch (event.type) { + case EventType::PointerEnter: { + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + QWindowSystemInterface::handleEnterEvent( + window(), m_window->mapFromGlobal(pointInScreen), pointInScreen); + break; } - if (titleBarOptions.flags & Qt::WindowSystemMenuHint) { - titleBarOptions.subControls |= SC_TitleBarCloseButton; - titleBarOptions.subControls |= SC_TitleBarSysMenu; + case EventType::PointerLeave: + QWindowSystemInterface::handleLeaveEvent(window()); + break; + default: + break; } - titleBarOptions.palette = makePalette(); - - titleBarOptions.palette.setCurrentColorGroup( - QGuiApplication::focusWindow() == window() ? QPalette::Active : QPalette::Inactive); - - if (activeTitleBarControl() != SC_None) - titleBarOptions.subControls |= activeTitleBarControl(); + return false; +} - if (!window()->title().isEmpty()) - titleBarOptions.titleBarOptionsString = window()->title(); +bool QWasmWindow::processWheel(const WheelEvent &event) +{ + // Web scroll deltas are inverted from Qt deltas - negate. + const int scrollFactor = -([&event]() { + switch (event.deltaMode) { + case DeltaMode::Pixel: + return 1; + case DeltaMode::Line: + return 12; + case DeltaMode::Page: + return 20; + }; + })(); - titleBarOptions.windowIcon = window()->icon(); + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); - return titleBarOptions; + return QWindowSystemInterface::handleWheelEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, (event.delta * scrollFactor).toPoint(), + (event.delta * scrollFactor).toPoint(), event.modifiers, Qt::NoScrollPhase, + Qt::MouseEventNotSynthesized, event.webkitDirectionInvertedFromDevice); } QRect QWasmWindow::normalGeometry() const @@ -565,19 +570,36 @@ void QWasmWindow::requestUpdate() m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); } +bool QWasmWindow::hasFrame() const +{ + return !m_flags.testFlag(Qt::FramelessWindowHint); +} + +bool QWasmWindow::hasBorder() const +{ + return hasFrame() && !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::SubWindow) + && !windowIsPopupType(m_flags) && !parent(); +} + bool QWasmWindow::hasTitleBar() const { - Qt::WindowFlags flags = window()->flags(); - return !(m_windowState & Qt::WindowFullScreen) - && flags.testFlag(Qt::WindowTitleHint) - && !(windowIsPopupType(flags)) - && m_needsCompositor; + return hasBorder() && m_flags.testFlag(Qt::WindowTitleHint); +} + +bool QWasmWindow::hasShadow() const +{ + return hasBorder() && !m_flags.testFlag(Qt::NoDropShadowWindowHint); +} + +bool QWasmWindow::hasMaximizeButton() const +{ + return !m_state.testFlag(Qt::WindowMaximized) && m_flags.testFlag(Qt::WindowMaximizeButtonHint); } bool QWasmWindow::windowIsPopupType(Qt::WindowFlags flags) const { if (flags.testFlag(Qt::Tool)) - return false; // Qt::Tool has the Popup bit set but isn't + return false; // Qt::Tool has the Popup bit set but isn't an actual Popup window return (flags.testFlag(Qt::Popup)); } @@ -590,18 +612,90 @@ void QWasmWindow::requestActivateWindow() return; } - if (window()->isTopLevel()) - raise(); + raise(); + setAsActiveNode(); + + if (!QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); + QPlatformWindow::requestActivateWindow(); } bool QWasmWindow::setMouseGrabEnabled(bool grab) { - if (grab) - m_compositor->setCapture(this); - else - m_compositor->releaseCapture(); - return true; + Q_UNUSED(grab); + return false; +} + +bool QWasmWindow::windowEvent(QEvent *event) +{ + switch (event->type()) { + case QEvent::WindowBlocked: + m_qtWindow["classList"].call<void>("add", emscripten::val("blocked")); + return false; // Propagate further + case QEvent::WindowUnblocked:; + m_qtWindow["classList"].call<void>("remove", emscripten::val("blocked")); + return false; // Propagate further + default: + return QPlatformWindow::windowEvent(event); + } +} + +void QWasmWindow::setMask(const QRegion ®ion) +{ + if (region.isEmpty()) { + m_qtWindow["style"].set("clipPath", emscripten::val("")); + return; + } + + std::ostringstream cssClipPath; + cssClipPath << "path('"; + for (const auto &rect : region) { + const auto cssRect = rect.adjusted(0, 0, 1, 1); + cssClipPath << "M " << cssRect.left() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.bottom() << " "; + cssClipPath << "L " << cssRect.left() << " " << cssRect.bottom() << " z "; + } + cssClipPath << "')"; + m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); +} + +void QWasmWindow::setParent(const QPlatformWindow *) +{ + commitParent(parentNode()); +} + +std::string QWasmWindow::canvasSelector() const +{ + return "!qtwindow" + std::to_string(m_winId); +} + +emscripten::val QWasmWindow::containerElement() +{ + return m_windowContents; +} + +QWasmWindowTreeNode *QWasmWindow::parentNode() +{ + if (parent()) + return static_cast<QWasmWindow *>(parent()); + return platformScreen(); +} + +QWasmWindow *QWasmWindow::asWasmWindow() +{ + return this; +} + +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) +{ + if (previous) + previous->containerElement().call<void>("removeChild", m_qtWindow); + if (current) + current->containerElement().call<void>("appendChild", m_qtWindow); + QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference); } QT_END_NAMESPACE |