// 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 #include QT_BEGIN_NAMESPACE WebImageButton::Callbacks::Callbacks() = default; WebImageButton::Callbacks::Callbacks(std::function onInteraction, std::function 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("createElement", emscripten::val("div"))), m_imgElement(dom::document().call("createElement", emscripten::val("img"))) { m_imgElement.set("draggable", false); m_containerElement["classList"].call("add", emscripten::val("image-button")); m_containerElement.call("appendChild", m_imgElement); } WebImageButton::~WebImageButton() = default; void WebImageButton::setCallbacks(Callbacks callbacks) { if (callbacks) { if (!m_webClickEventCallback) { m_webMouseDownEventCallback = std::make_unique( m_containerElement, "pointerdown", [this](emscripten::val event) { event.call("preventDefault"); event.call("stopPropagation"); m_callbacks.onInteraction(); }); m_webClickEventCallback = std::make_unique( m_containerElement, "click", [this](emscripten::val event) { m_callbacks.onClick(); event.call("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("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("add", emscripten::val("resize-outline")); m_element["classList"].call("add", emscripten::val(cssClassNameForEdges(edges))); parentElement.call("appendChild", m_element); m_mouseDownEvent = std::make_unique( m_element, "pointerdown", [this](emscripten::val event) { if (!onPointerDown(*PointerEvent::fromWeb(event))) return; m_resizer->onInteraction(); event.call("preventDefault"); event.call("stopPropagation"); }); m_mouseMoveEvent = std::make_unique( m_element, "pointermove", [this](emscripten::val event) { if (onPointerMove(*PointerEvent::fromWeb(event))) event.call("preventDefault"); }); m_mouseUpEvent = std::make_unique( m_element, "pointerup", [this](emscripten::val event) { if (onPointerUp(*PointerEvent::fromWeb(event))) { event.call("preventDefault"); event.call("stopPropagation"); } }); } Resizer::ResizerElement::~ResizerElement() { m_element["parentElement"].call("removeChild", m_element); } Resizer::ResizerElement::ResizerElement(ResizerElement &&other) = default; bool Resizer::ResizerElement::onPointerDown(const PointerEvent &event) { m_element.call("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("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 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(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("getBoundingClientRect")); auto containerGeometry = QRectF::fromDOMRect(m_window->parentNode()->containerElement().call( "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("createElement", emscripten::val("div"))), m_label(dom::document().call("createElement", emscripten::val("div"))) { m_icon = std::make_unique(); m_icon->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); m_element.call("appendChild", m_icon->htmlElement()); m_element.set("className", "title-bar"); auto spacer = dom::document().call("createElement", emscripten::val("div")); spacer["style"].set("width", "4px"); m_element.call("appendChild", spacer); m_label.set("className", "window-name"); m_element.call("appendChild", m_label); spacer = dom::document().call("createElement", emscripten::val("div")); spacer.set("className", "spacer"); m_element.call("appendChild", spacer); m_restore = std::make_unique(); 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("appendChild", m_restore->htmlElement()); m_maximize = std::make_unique(); 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("appendChild", m_maximize->htmlElement()); m_close = std::make_unique(); 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("appendChild", m_close->htmlElement()); parentElement.call("appendChild", m_element); m_mouseDownEvent = std::make_unique( m_element, "pointerdown", [this](emscripten::val event) { if (!onPointerDown(*PointerEvent::fromWeb(event))) return; m_window->onNonClientAreaInteraction(); event.call("preventDefault"); event.call("stopPropagation"); }); m_mouseMoveEvent = std::make_unique( m_element, "pointermove", [this](emscripten::val event) { if (onPointerMove(*PointerEvent::fromWeb(event))) { event.call("preventDefault"); } }); m_mouseUpEvent = std::make_unique( m_element, "pointerup", [this](emscripten::val event) { if (onPointerUp(*PointerEvent::fromWeb(event))) { event.call("preventDefault"); event.call("stopPropagation"); } }); m_doubleClickEvent = std::make_unique( m_element, "dblclick", [this](emscripten::val event) { if (onDoubleClick()) { event.call("preventDefault"); event.call("stopPropagation"); } }); } TitleBar::~TitleBar() { m_element["parentElement"].call("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("getBoundingClientRect")); } bool TitleBar::onPointerDown(const PointerEvent &event) { m_element.call("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("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( "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(window, m_qtWindowElement)), m_titleBar(std::make_unique(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