diff options
Diffstat (limited to 'src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp')
-rw-r--r-- | src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp | 460 |
1 files changed, 460 insertions, 0 deletions
diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp new file mode 100644 index 0000000000..00fa8fb236 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -0,0 +1,460 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindownonclientarea.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +WebImageButton::Callbacks::Callbacks() = default; +WebImageButton::Callbacks::Callbacks(std::function<void()> onInteraction, + std::function<void()> onClick) + : m_onInteraction(std::move(onInteraction)), m_onClick(std::move(onClick)) +{ + Q_ASSERT_X(!!m_onInteraction == !!m_onClick, Q_FUNC_INFO, + "Both callbacks need to be either null or non-null"); +} +WebImageButton::Callbacks::~Callbacks() = default; + +WebImageButton::Callbacks::Callbacks(Callbacks &&) = default; +WebImageButton::Callbacks &WebImageButton::Callbacks::operator=(Callbacks &&) = default; + +void WebImageButton::Callbacks::onInteraction() +{ + return m_onInteraction(); +} + +void WebImageButton::Callbacks::onClick() +{ + return m_onClick(); +} + +WebImageButton::WebImageButton() + : m_containerElement( + dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_imgElement(dom::document().call<emscripten::val>("createElement", emscripten::val("img"))) +{ + m_imgElement.set("draggable", false); + + m_containerElement["classList"].call<void>("add", emscripten::val("image-button")); + m_containerElement.call<void>("appendChild", m_imgElement); +} + +WebImageButton::~WebImageButton() = default; + +void WebImageButton::setCallbacks(Callbacks callbacks) +{ + if (callbacks) { + if (!m_webClickEventCallback) { + m_webMouseDownEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "pointerdown", [this](emscripten::val event) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + m_callbacks.onInteraction(); + }); + m_webClickEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "click", [this](emscripten::val event) { + m_callbacks.onClick(); + event.call<void>("stopPropagation"); + }); + } + } else { + m_webMouseDownEventCallback.reset(); + m_webClickEventCallback.reset(); + } + dom::syncCSSClassWith(m_containerElement, "action-button", !!callbacks); + m_callbacks = std::move(callbacks); +} + +void WebImageButton::setImage(std::string_view imageData, std::string_view format) +{ + m_imgElement.set("src", + "data:image/" + std::string(format) + ";base64," + std::string(imageData)); +} + +void WebImageButton::setVisible(bool visible) +{ + m_containerElement["style"].set("display", visible ? "flex" : "none"); +} + +Resizer::ResizerElement::ResizerElement(emscripten::val parentElement, Qt::Edges edges, + Resizer *resizer) + : m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_edges(edges), + m_resizer(resizer) +{ + Q_ASSERT_X(m_resizer, Q_FUNC_INFO, "Resizer cannot be null"); + + m_element["classList"].call<void>("add", emscripten::val("resize-outline")); + m_element["classList"].call<void>("add", emscripten::val(cssClassNameForEdges(edges))); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_resizer->onInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +Resizer::ResizerElement::~ResizerElement() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +Resizer::ResizerElement::ResizerElement(ResizerElement &&other) = default; + +bool Resizer::ResizerElement::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_resizer->startResize(m_edges, event); + return true; +} + +bool Resizer::ResizerElement::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->continueResize(event); + return true; +} + +bool Resizer::ResizerElement::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->finishResize(); + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + return true; +} + +Resizer::Resizer(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), m_windowElement(parentElement) +{ + Q_ASSERT_X(m_window, Q_FUNC_INFO, "Window must not be null"); + + constexpr std::array<int, 8> ResizeEdges = { Qt::TopEdge | Qt::LeftEdge, + Qt::TopEdge, + Qt::TopEdge | Qt::RightEdge, + Qt::LeftEdge, + Qt::RightEdge, + Qt::BottomEdge | Qt::LeftEdge, + Qt::BottomEdge, + Qt::BottomEdge | Qt::RightEdge }; + std::transform(std::begin(ResizeEdges), std::end(ResizeEdges), std::back_inserter(m_elements), + [parentElement, this](int edges) { + return std::make_unique<ResizerElement>(parentElement, + Qt::Edges::fromInt(edges), this); + }); +} + +Resizer::~Resizer() = default; + +ResizeConstraints Resizer::getResizeConstraints() { + const auto *window = m_window->window(); + const auto minShrink = QPoint(window->minimumWidth() - window->geometry().width(), + window->minimumHeight() - window->geometry().height()); + const auto maxGrow = QPoint(window->maximumWidth() - window->geometry().width(), + window->maximumHeight() - window->geometry().height()); + + const auto frameRect = + QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect")); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); + + return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; +} + +void Resizer::onInteraction() +{ + m_window->onNonClientAreaInteraction(); +} + +void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) +{ + Q_ASSERT_X(!m_currentResizeData, Q_FUNC_INFO, "Another resize in progress"); + + m_currentResizeData.reset(new ResizeData{ + .edges = resizeEdges, + .originInScreenCoords = dom::mapPoint( + event.target(), m_window->platformScreen()->element(), event.localPoint), + }); + + const auto resizeConstraints = getResizeConstraints(); + m_currentResizeData->minShrink = resizeConstraints.minShrink; + + m_currentResizeData->maxGrow = + QPoint(resizeConstraints.maxGrow.x(), + std::min(resizeEdges & Qt::Edge::TopEdge ? resizeConstraints.maxGrowTop : INT_MAX, + resizeConstraints.maxGrow.y())); + + m_currentResizeData->initialBounds = m_window->window()->geometry(); +} + +void Resizer::continueResize(const PointerEvent &event) +{ + const auto pointInScreen = + dom::mapPoint(event.target(), m_window->platformScreen()->element(), event.localPoint); + const auto amount = (pointInScreen - m_currentResizeData->originInScreenCoords).toPoint(); + const QPoint cappedGrowVector( + std::min(m_currentResizeData->maxGrow.x(), + std::max(m_currentResizeData->minShrink.x(), + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -amount.x() + : (m_currentResizeData->edges & Qt::Edge::RightEdge) + ? amount.x() + : 0)), + std::min(m_currentResizeData->maxGrow.y(), + std::max(m_currentResizeData->minShrink.y(), + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -amount.y() + : (m_currentResizeData->edges & Qt::Edge::BottomEdge) + ? amount.y() + : 0))); + + auto bounds = m_currentResizeData->initialBounds.adjusted( + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -cappedGrowVector.y() : 0, + (m_currentResizeData->edges & Qt::Edge::RightEdge) ? cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::BottomEdge) ? cappedGrowVector.y() : 0); + + m_window->window()->setGeometry(bounds); +} + +void Resizer::finishResize() +{ + Q_ASSERT_X(m_currentResizeData, Q_FUNC_INFO, "No resize in progress"); + m_currentResizeData.reset(); +} + +TitleBar::TitleBar(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), + m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_label(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))) +{ + m_icon = std::make_unique<WebImageButton>(); + m_icon->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + m_element.call<void>("appendChild", m_icon->htmlElement()); + m_element.set("className", "title-bar"); + + auto spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer["style"].set("width", "4px"); + m_element.call<void>("appendChild", spacer); + + m_label.set("className", "window-name"); + + m_element.call<void>("appendChild", m_label); + + spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer.set("className", "spacer"); + m_element.call<void>("appendChild", spacer); + + m_restore = std::make_unique<WebImageButton>(); + m_restore->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Restore), + "svg+xml"); + m_restore->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onRestoreClicked(); })); + + m_element.call<void>("appendChild", m_restore->htmlElement()); + + m_maximize = std::make_unique<WebImageButton>(); + m_maximize->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Maximize), + "svg+xml"); + m_maximize->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onMaximizeClicked(); })); + + m_element.call<void>("appendChild", m_maximize->htmlElement()); + + m_close = std::make_unique<WebImageButton>(); + m_close->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::X), "svg+xml"); + m_close->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onCloseClicked(); })); + + m_element.call<void>("appendChild", m_close->htmlElement()); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_window->onNonClientAreaInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + } + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); + m_doubleClickEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "dblclick", [this](emscripten::val event) { + if (onDoubleClick()) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +TitleBar::~TitleBar() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +void TitleBar::setTitle(const QString &title) +{ + m_label.set("innerText", emscripten::val(title.toStdString())); +} + +void TitleBar::setRestoreVisible(bool visible) +{ + m_restore->setVisible(visible); +} + +void TitleBar::setMaximizeVisible(bool visible) +{ + m_maximize->setVisible(visible); +} + +void TitleBar::setCloseVisible(bool visible) +{ + m_close->setVisible(visible); +} + +void TitleBar::setIcon(std::string_view imageData, std::string_view format) +{ + m_icon->setImage(imageData, format); +} + +void TitleBar::setWidth(int width) +{ + m_element["style"].set("width", std::to_string(width) + "px"); +} + +QRectF TitleBar::geometry() const +{ + return QRectF::fromDOMRect(m_element.call<emscripten::val>("getBoundingClientRect")); +} + +bool TitleBar::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_moveStartWindowPosition = m_window->window()->position(); + m_moveStartPoint = clipPointWithScreen(event.localPoint); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + const QPoint delta = (clipPointWithScreen(event.localPoint) - m_moveStartPoint).toPoint(); + + m_window->window()->setPosition(m_moveStartWindowPosition + delta); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onDoubleClick() +{ + m_window->onToggleMaximized(); + return true; +} + +QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const +{ + auto containerRect = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + const auto p = dom::mapPoint(m_element, m_window->parentNode()->containerElement(), + pointInTitleBarCoords); + + auto result = QPointF(qBound(0., qreal(p.x()), containerRect.width()), + qBound(0., qreal(p.y()), containerRect.height())); + return m_window->parent() ? result : m_window->platformScreen()->mapFromLocal(result).toPoint(); +} + +NonClientArea::NonClientArea(QWasmWindow *window, emscripten::val qtWindowElement) + : m_qtWindowElement(qtWindowElement), + m_resizer(std::make_unique<Resizer>(window, m_qtWindowElement)), + m_titleBar(std::make_unique<TitleBar>(window, m_qtWindowElement)) +{ + updateResizability(); +} + +NonClientArea::~NonClientArea() = default; + +void NonClientArea::onClientAreaWidthChange(int width) +{ + m_titleBar->setWidth(width); +} + +void NonClientArea::propagateSizeHints() +{ + updateResizability(); +} + +void NonClientArea::updateResizability() +{ + const auto resizeConstraints = m_resizer->getResizeConstraints(); + const bool nonResizable = resizeConstraints.minShrink.isNull() + && resizeConstraints.maxGrow.isNull() && resizeConstraints.maxGrowTop == 0; + dom::syncCSSClassWith(m_qtWindowElement, "no-resize", nonResizable); +} + +QT_END_NAMESPACE |