diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 0f2bda6f13..94512e35fa 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "iv/markdown/iv_markdown_article.h" +#include "iv/markdown/iv_markdown_article_scroll_forwarder.h" #include "iv/markdown/iv_markdown_common.h" #include "iv/markdown/iv_markdown_prepare.h" #include "iv/iv_cached_media.h" @@ -344,15 +345,22 @@ public: protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + bool eventHook(QEvent *e) override; private: void requestRepaint(QRect rect); + [[nodiscard]] Iv::Markdown::MarkdownArticle *scrollTarget(); const not_null _session; std::shared_ptr _mediaRuntime; Iv::Markdown::MarkdownArticle _article; std::unique_ptr _theme; std::unique_ptr _style; + Iv::Markdown::MarkdownArticleScrollForwarder _scrollForwarder; int _paletteVersion = -1; bool _hasArticle = false; @@ -1049,6 +1057,7 @@ ComposeAiRichBody::ComposeAiRichBody( , _style(std::make_unique(session->colorIndicesValue())) { _style->apply(_theme.get()); _paletteVersion = _style->paletteVersion(); + setAttribute(Qt::WA_AcceptTouchEvents); const auto weak = base::make_weak(this); _article.setTextRepaintCallbacks( @@ -1076,6 +1085,7 @@ ComposeAiRichBody::~ComposeAiRichBody() { } void ComposeAiRichBody::setPage(std::shared_ptr page) { + _scrollForwarder.reset(scrollTarget()); const auto richLimits = Iv::ResolveRichMessageLimits(_session); auto prepared = Iv::Markdown::TryPrepareNativeInstantView({ .richPage = page, @@ -1105,6 +1115,39 @@ void ComposeAiRichBody::requestRepaint(QRect rect) { }); } +Iv::Markdown::MarkdownArticle *ComposeAiRichBody::scrollTarget() { + return _hasArticle ? &_article : nullptr; +} + +void ComposeAiRichBody::wheelEvent(QWheelEvent *e) { + _scrollForwarder.handleWheel(scrollTarget(), e, QPoint()); +} + +void ComposeAiRichBody::mousePressEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMousePress(scrollTarget(), e, QPoint())) { + RpWidget::mousePressEvent(e); + } +} + +void ComposeAiRichBody::mouseMoveEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMouseMove(scrollTarget(), e, QPoint())) { + RpWidget::mouseMoveEvent(e); + } +} + +void ComposeAiRichBody::mouseReleaseEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMouseRelease(scrollTarget(), e, QPoint())) { + RpWidget::mouseReleaseEvent(e); + } +} + +bool ComposeAiRichBody::eventHook(QEvent *e) { + if (_scrollForwarder.handleTouchHook(scrollTarget(), this, e, QPoint())) { + return true; + } + return RpWidget::eventHook(e); +} + int ComposeAiRichBody::resizeGetHeight(int newWidth) { return (_hasArticle && newWidth > 0) ? _article.resizeGetHeight(newWidth) diff --git a/Telegram/SourceFiles/boxes/create_ai_box.cpp b/Telegram/SourceFiles/boxes/create_ai_box.cpp index b8edaeadeb..4d9f1b41ed 100644 --- a/Telegram/SourceFiles/boxes/create_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/create_ai_box.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_cached_media.h" #include "iv/iv_rich_page.h" #include "iv/markdown/iv_markdown_article.h" +#include "iv/markdown/iv_markdown_article_scroll_forwarder.h" #include "iv/markdown/iv_markdown_common.h" #include "iv/markdown/iv_markdown_prepare.h" #include "lang/lang_keys.h" @@ -84,6 +85,11 @@ public: private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + bool eventHook(QEvent *e) override; void paintArticle(Painter &p, QRect clip); void requestArticleRepaint(QRect articleRect); @@ -91,6 +97,7 @@ private: [[nodiscard]] int controlRowHeight() const; [[nodiscard]] QRect articleRect() const; + [[nodiscard]] Iv::Markdown::MarkdownArticle *scrollTarget(); const not_null _session; const object_ptr _selector; @@ -100,6 +107,7 @@ private: Iv::Markdown::MarkdownArticle _article; std::unique_ptr _theme; std::unique_ptr _style; + Iv::Markdown::MarkdownArticleScrollForwarder _scrollForwarder; int _articleHeight = 0; int _paletteVersion = -1; bool _hasArticle = false; @@ -128,6 +136,7 @@ ResponseIsland::ResponseIsland( , _style(std::make_unique(session->colorIndicesValue())) { _style->apply(_theme.get()); _paletteVersion = _style->paletteVersion(); + setAttribute(Qt::WA_AcceptTouchEvents); _selector->setMarkedText(SelectorTitle(language)); _selector->setClickHandlerFilter([=](const auto &...) { @@ -210,6 +219,52 @@ QRect ResponseIsland::articleRect() const { return QRect(0, top, width(), _articleHeight); } +Iv::Markdown::MarkdownArticle *ResponseIsland::scrollTarget() { + return _hasArticle ? &_article : nullptr; +} + +void ResponseIsland::wheelEvent(QWheelEvent *e) { + _scrollForwarder.handleWheel(scrollTarget(), e, articleRect().topLeft()); +} + +void ResponseIsland::mousePressEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMousePress( + scrollTarget(), + e, + articleRect().topLeft())) { + RpWidget::mousePressEvent(e); + } +} + +void ResponseIsland::mouseMoveEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMouseMove( + scrollTarget(), + e, + articleRect().topLeft())) { + RpWidget::mouseMoveEvent(e); + } +} + +void ResponseIsland::mouseReleaseEvent(QMouseEvent *e) { + if (!_scrollForwarder.handleMouseRelease( + scrollTarget(), + e, + articleRect().topLeft())) { + RpWidget::mouseReleaseEvent(e); + } +} + +bool ResponseIsland::eventHook(QEvent *e) { + if (_scrollForwarder.handleTouchHook( + scrollTarget(), + this, + e, + articleRect().topLeft())) { + return true; + } + return RpWidget::eventHook(e); +} + void ResponseIsland::paintArticle(Painter &p, QRect clip) { if (!_hasArticle) { return; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.cpp new file mode 100644 index 0000000000..2c873d73ed --- /dev/null +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.cpp @@ -0,0 +1,207 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "iv/markdown/iv_markdown_article_scroll_forwarder.h" + +#include "base/qt/qt_common_adapters.h" +#include "base/algorithm.h" +#include "iv/markdown/iv_markdown_article.h" + +#include +#include +#include +#include + +#include + +namespace Iv::Markdown { +namespace { + +[[nodiscard]] QPoint LocalPosition(QWheelEvent *e) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return e->position().toPoint(); +#else // Qt >= 6.0 + return e->pos(); +#endif // Qt >= 6.0 +} + +} // namespace + +void MarkdownArticleScrollForwarder::handleWheel( + MarkdownArticle *article, + QWheelEvent *e, + QPoint articleTopLeft) { + if (!article) { + (void)_scrollDirectionLock.update(e->phase(), {}); + e->ignore(); + return; + } + const auto delta = Ui::ScrollDeltaF(e); + const auto locked = _scrollDirectionLock.update(e->phase(), delta); + const auto horizontal = locked + ? (*locked == Qt::Horizontal) + : (std::abs(delta.x()) > std::abs(delta.y())); + const auto local = LocalPosition(e) - articleTopLeft; + if (!article->horizontalScrollHit(local).scrollable) { + e->ignore(); + return; + } + if (horizontal) { + (void)article->consumeHorizontalScroll( + local, + int(std::round(delta.x()))); + e->accept(); + } else { + e->ignore(); + } +} + +bool MarkdownArticleScrollForwarder::handleMousePress( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft) { + if (!article || e->button() != Qt::LeftButton) { + return false; + } + if (!article->beginHorizontalScroll(e->pos() - articleTopLeft, false)) { + return false; + } + _mouseDrag = true; + e->accept(); + return true; +} + +bool MarkdownArticleScrollForwarder::handleMouseMove( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft) { + if (!_mouseDrag) { + return false; + } + if (article) { + (void)article->updateHorizontalScroll(e->pos() - articleTopLeft); + } + e->accept(); + return true; +} + +bool MarkdownArticleScrollForwarder::handleMouseRelease( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft) { + if (!_mouseDrag || e->button() != Qt::LeftButton) { + return false; + } + _mouseDrag = false; + if (article) { + (void)article->updateHorizontalScroll(e->pos() - articleTopLeft); + article->endHorizontalScroll(); + } + e->accept(); + return true; +} + +bool MarkdownArticleScrollForwarder::handleTouchHook( + MarkdownArticle *article, + QWidget *widget, + QEvent *e, + QPoint articleTopLeft) { + if (e->type() != QEvent::TouchBegin + && e->type() != QEvent::TouchUpdate + && e->type() != QEvent::TouchEnd + && e->type() != QEvent::TouchCancel) { + return false; + } + const auto touch = static_cast(e); + if (touch->device()->type() != base::TouchDevice::TouchScreen) { + return false; + } + return handleTouch(article, widget, touch, articleTopLeft); +} + +bool MarkdownArticleScrollForwarder::handleTouch( + MarkdownArticle *article, + QWidget *widget, + QTouchEvent *e, + QPoint articleTopLeft) { + const auto wasActive = _touchScroll; + if (e->type() == QEvent::TouchCancel) { + _pendingTouchPoint = std::nullopt; + if (!base::take(_touchScroll)) { + return false; + } + if (article) { + article->endHorizontalScroll(); + } + e->accept(); + return true; + } + if (!article || e->touchPoints().isEmpty()) { + return wasActive; + } + const auto local = widget->mapFromGlobal( + e->touchPoints().cbegin()->screenPos().toPoint()) - articleTopLeft; + switch (e->type()) { + case QEvent::TouchBegin: { + _pendingTouchPoint = std::nullopt; + const auto hit = article->horizontalScrollHit(local); + _touchScroll = hit.overScrollbar + && article->beginHorizontalScroll(local, false); + if (!_touchScroll && hit.overViewport) { + _pendingTouchPoint = local; + } + if (_touchScroll) { + e->accept(); + } + } break; + case QEvent::TouchUpdate: + if (_touchScroll) { + (void)article->updateHorizontalScroll(local); + e->accept(); + } else if (_pendingTouchPoint) { + const auto delta = local - *_pendingTouchPoint; + if (delta.manhattanLength() < QApplication::startDragDistance()) { + break; + } + const auto horizontal = (std::abs(delta.x()) > std::abs(delta.y())); + if (!horizontal) { + _pendingTouchPoint = std::nullopt; + break; + } + _touchScroll = article->beginHorizontalScroll( + *base::take(_pendingTouchPoint), + true); + if (_touchScroll) { + (void)article->updateHorizontalScroll(local); + e->accept(); + } + } + break; + case QEvent::TouchEnd: + _pendingTouchPoint = std::nullopt; + if (base::take(_touchScroll)) { + article->endHorizontalScroll(); + e->accept(); + } + break; + default: + break; + } + return wasActive || _touchScroll; +} + +void MarkdownArticleScrollForwarder::reset(MarkdownArticle *article) { + const auto mouse = base::take(_mouseDrag); + const auto touch = base::take(_touchScroll); + if (article && (mouse || touch)) { + article->endHorizontalScroll(); + } + _pendingTouchPoint = std::nullopt; + _scrollDirectionLock.reset(); +} + +} // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.h new file mode 100644 index 0000000000..e73ee30370 --- /dev/null +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_scroll_forwarder.h @@ -0,0 +1,64 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/ui_utility.h" + +#include +#include + +class QEvent; +class QMouseEvent; +class QTouchEvent; +class QWheelEvent; +class QWidget; + +namespace Iv::Markdown { + +class MarkdownArticle; + +class MarkdownArticleScrollForwarder final { +public: + void handleWheel( + MarkdownArticle *article, + QWheelEvent *e, + QPoint articleTopLeft); + [[nodiscard]] bool handleMousePress( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft); + [[nodiscard]] bool handleMouseMove( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft); + [[nodiscard]] bool handleMouseRelease( + MarkdownArticle *article, + QMouseEvent *e, + QPoint articleTopLeft); + [[nodiscard]] bool handleTouchHook( + MarkdownArticle *article, + QWidget *widget, + QEvent *e, + QPoint articleTopLeft); + void reset(MarkdownArticle *article); + +private: + [[nodiscard]] bool handleTouch( + MarkdownArticle *article, + QWidget *widget, + QTouchEvent *e, + QPoint articleTopLeft); + + Ui::ScrollDirectionLock _scrollDirectionLock; + std::optional _pendingTouchPoint; + bool _mouseDrag = false; + bool _touchScroll = false; + +}; + +} // namespace Iv::Markdown diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index d6ef6bc2b9..51c3149139 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -48,6 +48,8 @@ PRIVATE iv/markdown/iv_markdown_article_layout_structure.h iv/markdown/iv_markdown_article_paint.cpp iv/markdown/iv_markdown_article_paint.h + iv/markdown/iv_markdown_article_scroll_forwarder.cpp + iv/markdown/iv_markdown_article_scroll_forwarder.h iv/markdown/iv_markdown_article_selection.cpp iv/markdown/iv_markdown_article_selection.h iv/markdown/iv_markdown_article_text.cpp