Revision control
Copy as Markdown
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#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(EventType::PointerDown, 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(EventType::PointerMove, event)))
event.call<void>("preventDefault");
});
m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>(
m_element, "pointerup", [this](emscripten::val event) {
if (onPointerUp(PointerEvent(EventType::PointerUp, 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(EventType::PointerDown, 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(EventType::PointerMove, event))) {
event.call<void>("preventDefault");
}
});
m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>(
m_element, "pointerup", [this](emscripten::val event) {
if (onPointerUp(PointerEvent(EventType::PointerUp, 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