diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e81947dda..a9e84983a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -37,6 +37,10 @@ include(cmake/td_scheme.cmake) include(cmake/td_ui.cmake) include(cmake/generate_appdata_changelog.cmake) +if (DESKTOP_APP_TEST_APPS) + include(cmake/tests.cmake) +endif() + if (WIN32) include(cmake/generate_midl.cmake) generate_midl(Telegram ${src_loc} diff --git a/Telegram/SourceFiles/tests/test_main.cpp b/Telegram/SourceFiles/tests/test_main.cpp new file mode 100644 index 000000000..cd57022c4 --- /dev/null +++ b/Telegram/SourceFiles/tests/test_main.cpp @@ -0,0 +1,219 @@ +/* +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 "tests/test_main.h" + +#include "base/invoke_queued.h" +#include "base/integration.h" +#include "ui/effects/animations.h" +#include "ui/widgets/rp_window.h" +#include "ui/emoji_config.h" +#include "ui/painter.h" + +#include +#include +#include +#include +#include + +#include + +namespace Test { + +bool App::notifyOrInvoke(QObject *receiver, QEvent *e) { + if (e->type() == base::InvokeQueuedEvent::Type()) { + static_cast(e)->invoke(); + return true; + } + return QApplication::notify(receiver, e); +} + +bool App::nativeEventFilter( + const QByteArray &eventType, + void *message, + native_event_filter_result *result) { + registerEnterFromEventLoop(); + return false; +} + +void App::checkForEmptyLoopNestingLevel() { + // _loopNestingLevel == _eventNestingLevel means that we had a + // native event in a nesting loop that didn't get a notify() call + // after. That means we already have exited the nesting loop and + // there must not be any postponed calls with that nesting level. + if (_loopNestingLevel == _eventNestingLevel) { + Assert(_postponedCalls.empty() + || _postponedCalls.back().loopNestingLevel < _loopNestingLevel); + Assert(!_previousLoopNestingLevels.empty()); + + _loopNestingLevel = _previousLoopNestingLevels.back(); + _previousLoopNestingLevels.pop_back(); + } +} + +void App::postponeCall(FnMut &&callable) { + Expects(callable != nullptr); + Expects(_eventNestingLevel >= _loopNestingLevel); + + checkForEmptyLoopNestingLevel(); + _postponedCalls.push_back({ + _loopNestingLevel, + std::move(callable) + }); +} + +void App::processPostponedCalls(int level) { + while (!_postponedCalls.empty()) { + auto &last = _postponedCalls.back(); + if (last.loopNestingLevel != level) { + break; + } + auto taken = std::move(last); + _postponedCalls.pop_back(); + taken.callable(); + } +} + +void App::incrementEventNestingLevel() { + ++_eventNestingLevel; +} + +void App::decrementEventNestingLevel() { + Expects(_eventNestingLevel >= _loopNestingLevel); + + if (_eventNestingLevel == _loopNestingLevel) { + _loopNestingLevel = _previousLoopNestingLevels.back(); + _previousLoopNestingLevels.pop_back(); + } + const auto processTillLevel = _eventNestingLevel - 1; + processPostponedCalls(processTillLevel); + checkForEmptyLoopNestingLevel(); + _eventNestingLevel = processTillLevel; + + Ensures(_eventNestingLevel >= _loopNestingLevel); +} + +void App::registerEnterFromEventLoop() { + Expects(_eventNestingLevel >= _loopNestingLevel); + + if (_eventNestingLevel > _loopNestingLevel) { + _previousLoopNestingLevels.push_back(_loopNestingLevel); + _loopNestingLevel = _eventNestingLevel; + } +} + +bool App::notify(QObject *receiver, QEvent *e) { + if (QThread::currentThreadId() != _mainThreadId) { + return notifyOrInvoke(receiver, e); + } + + const auto wrap = createEventNestingLevel(); + if (e->type() == QEvent::UpdateRequest) { + const auto weak = QPointer(receiver); + _widgetUpdateRequests.fire({}); + if (!weak) { + return true; + } + } + return notifyOrInvoke(receiver, e); +} + +rpl::producer<> App::widgetUpdateRequests() const { + return _widgetUpdateRequests.events(); +} + +void BaseIntegration::enterFromEventLoop(FnMut &&method) { + app().customEnterFromEventLoop(std::move(method)); +} + +bool BaseIntegration::logSkipDebug() { + return true; +} + +void BaseIntegration::logMessageDebug(const QString &message) { +} + +void BaseIntegration::logMessage(const QString &message) { +} + +void UiIntegration::postponeCall(FnMut &&callable) { + app().postponeCall(std::move(callable)); +} + +void UiIntegration::registerLeaveSubscription(not_null widget) { +} + +void UiIntegration::unregisterLeaveSubscription(not_null widget) { +} + +QString UiIntegration::emojiCacheFolder() { + return QDir().currentPath() + "/tests/" + name() + "/emoji"; +} + +QString UiIntegration::openglCheckFilePath() { + return QDir().currentPath() + "/tests/" + name() + "/opengl"; +} + +QString UiIntegration::angleBackendFilePath() { + return QDir().currentPath() + "/test/" + name() + "/angle"; +} + +} // namespace Test + +int main(int argc, char *argv[]) { + using namespace Test; + + auto app = App(argc, argv); + app.installNativeEventFilter(&app); + + const auto ratio = app.devicePixelRatio(); + const auto useRatio = std::clamp(qCeil(ratio), 1, 3); + style::SetDevicePixelRatio(useRatio); + + const auto screen = App::primaryScreen(); + const auto dpi = screen->logicalDotsPerInch(); + const auto basePair = screen->handle()->logicalBaseDpi(); + const auto baseMiddle = (basePair.first + basePair.second) * 0.5; + const auto screenExact = dpi / baseMiddle; + const auto screenScale = int(base::SafeRound(screenExact * 20)) * 5; + const auto chosen = std::clamp( + screenScale, + style::kScaleMin, + style::MaxScaleForRatio(useRatio)); + + BaseIntegration base(argc, argv); + base::Integration::Set(&base); + + UiIntegration ui; + Ui::Integration::Set(&ui); + + InvokeQueued(&app, [=] { + new Ui::Animations::Manager(); + style::StartManager(chosen); + + Ui::Emoji::Init(); + + const auto window = new Ui::RpWindow(); + window->setGeometry( + { scale(100), scale(100), scale(800), scale(600) }); + window->show(); + + window->setMinimumSize({ scale(240), scale(320) }); + + test(window, window->body()); + }); + + return app.exec(); +} + +namespace crl { + +rpl::producer<> on_main_update_requests() { + return Test::app().widgetUpdateRequests(); +} + +} // namespace crl diff --git a/Telegram/SourceFiles/tests/test_main.h b/Telegram/SourceFiles/tests/test_main.h new file mode 100644 index 000000000..b0bbafb2f --- /dev/null +++ b/Telegram/SourceFiles/tests/test_main.h @@ -0,0 +1,110 @@ +/* +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 "base/basic_types.h" +#include "base/integration.h" +#include "ui/style/style_core_scale.h" +#include "ui/integration.h" + +#include +#include + +#include +#include +#include +#include + +namespace Ui { +class RpWidget; +class RpWindow; +} // namespace Ui + +namespace Test { + +[[nodiscard]] QString name(); + +void test(not_null window, not_null widget); + +[[nodiscard]] inline int scale(int value) { + return style::ConvertScale(value); +}; + +class App final : public QApplication, public QAbstractNativeEventFilter { +public: + using QApplication::QApplication; + + template + auto customEnterFromEventLoop(Callable &&callable) { + registerEnterFromEventLoop(); + const auto wrap = createEventNestingLevel(); + return callable(); + } + + void postponeCall(FnMut &&callable); + + [[nodiscard]] rpl::producer<> widgetUpdateRequests() const; + +private: + struct PostponedCall { + int loopNestingLevel = 0; + FnMut callable; + }; + + auto createEventNestingLevel() { + incrementEventNestingLevel(); + return gsl::finally([=] { decrementEventNestingLevel(); }); + } + + void checkForEmptyLoopNestingLevel(); + void processPostponedCalls(int level); + void incrementEventNestingLevel(); + void decrementEventNestingLevel(); + void registerEnterFromEventLoop(); + + bool notifyOrInvoke(QObject *receiver, QEvent *e); + bool notify(QObject *receiver, QEvent *e) override; + bool nativeEventFilter( + const QByteArray &eventType, + void *message, + native_event_filter_result *result) override; + + rpl::event_stream<> _widgetUpdateRequests; + Qt::HANDLE _mainThreadId = QThread::currentThreadId(); + int _eventNestingLevel = 0; + int _loopNestingLevel = 0; + std::vector _previousLoopNestingLevels; + std::vector _postponedCalls; + +}; + +[[nodiscard]] inline App &app() { + return *static_cast(QCoreApplication::instance()); +} + +class BaseIntegration final : public base::Integration { +public: + using Integration::Integration; + + void enterFromEventLoop(FnMut &&method); + bool logSkipDebug(); + void logMessageDebug(const QString &message); + void logMessage(const QString &message); +}; + +class UiIntegration final : public Ui::Integration { +public: + void postponeCall(FnMut &&callable); + void registerLeaveSubscription(not_null widget); + void unregisterLeaveSubscription(not_null widget); + QString emojiCacheFolder(); + QString openglCheckFilePath(); + QString angleBackendFilePath(); +}; + +} // namespace Test diff --git a/Telegram/SourceFiles/tests/test_text.cpp b/Telegram/SourceFiles/tests/test_text.cpp new file mode 100644 index 000000000..688041cf5 --- /dev/null +++ b/Telegram/SourceFiles/tests/test_text.cpp @@ -0,0 +1,113 @@ +/* +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 "tests/test_main.h" + +#include "base/invoke_queued.h" +#include "base/integration.h" +#include "ui/effects/animations.h" +#include "ui/text/text.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/rp_window.h" +#include "ui/painter.h" + +#include +#include +#include +#include + +namespace Test { + +QString name() { + return u"text"_q; +} + +void test(not_null window, not_null body) { + auto text = new Ui::Text::String(scale(64)); + + const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d"); + const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e"); + const auto hebrew = QString() + QChar(1506) + QChar(1460) + QChar(1489); + + auto data = TextWithEntities(); + data.append( + u"Lorem ipsu7m dolor sit amet, "_q + ).append(Ui::Text::Bold( + u"consectetur adipiscing: "_q + + hebrew + + u" elit, sed do eiusmod tempor incididunt test"_q + )).append(Ui::Text::Wrapped(Ui::Text::Bold( + u". ut labore et dolore magna aliqua."_q + + like + + dislike + + u"Ut enim ad minim veniam"_q + ), EntityType::Italic)).append( + u", quis nostrud exercitation ullamco laboris nisi ut aliquip ex \ +ea commodo consequat. Duis aute irure dolor in reprehenderit in \ +voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ +Excepteur sint occaecat cupidatat non proident, sunt in culpa \ +qui officia deserunt mollit anim id est laborum."_q +).append(u"\n\n"_q).append(hebrew).append("\n\n").append( + "Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeu\ +fugiatnullapariaturExcepteursintoccaecatcupidatatnonproident, sunt in culpa \ +qui officia deserunt mollit anim id est laborum. \ +Duisauteiruredolorinreprehenderitinvoluptate."); + data.append(data); + //data.append("hi\n\nguys"); + text->setMarkedText(st::defaultTextStyle, data); + + body->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(body); + auto hq = PainterHighQualityEnabler(p); + + const auto width = body->width(); + const auto height = body->height(); + + p.fillRect(clip, QColor(255, 255, 255)); + + const auto border = QColor(0, 128, 0, 16); + auto skip = scale(20); + p.fillRect(0, 0, skip, height, border); + p.fillRect(skip, 0, width - skip, skip, border); + p.fillRect(skip, height - skip, width - skip, skip, border); + p.fillRect(width - skip, skip, skip, height - skip * 2, border); + + const auto inner = body->rect().marginsRemoved( + { skip, skip, skip, skip }); + + p.fillRect(QRect{ + inner.x(), + inner.y(), + inner.width(), + text->countHeight(inner.width()) + }, QColor(128, 0, 0, 16)); + + auto widths = text->countLineWidths(inner.width()); + auto top = 0; + for (const auto width : widths) { + p.fillRect(QRect{ + inner.x(), + inner.y() + top, + width, + st::defaultTextStyle.font->height + }, QColor(0, 0, 128, 16)); + top += st::defaultTextStyle.font->height; + } + + text->draw(p, { + .position = inner.topLeft(), + .availableWidth = inner.width(), + }); + + //const auto to = QRectF( + // inner.marginsRemoved({ 0, inner.height() / 2, 0, 0 })); + //const auto t = u"hi\n\nguys"_q; + //p.drawText(to, t); + }, body->lifetime()); +} + +} // namespace Test diff --git a/Telegram/cmake/tests.cmake b/Telegram/cmake/tests.cmake new file mode 100644 index 000000000..4f2e5651b --- /dev/null +++ b/Telegram/cmake/tests.cmake @@ -0,0 +1,44 @@ +# 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 + +add_executable(test_text WIN32) +init_target(test_text "(tests)") + +target_include_directories(test_text PRIVATE ${src_loc}) + +nice_target_sources(test_text ${src_loc} +PRIVATE + tests/test_main.cpp + tests/test_main.h + tests/test_text.cpp +) + +nice_target_sources(test_text ${res_loc} +PRIVATE + qrc/emoji_1.qrc + qrc/emoji_2.qrc + qrc/emoji_3.qrc + qrc/emoji_4.qrc + qrc/emoji_5.qrc + qrc/emoji_6.qrc + qrc/emoji_7.qrc + qrc/emoji_8.qrc +) + +target_link_libraries(test_text +PRIVATE + desktop-app::lib_base + desktop-app::lib_crl + desktop-app::lib_ui + desktop-app::external_qt + desktop-app::external_qt_static_plugins +) + +set_target_properties(test_text PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +add_dependencies(Telegram test_text) + +target_prepare_qrc(test_text) diff --git a/cmake b/cmake index 7b11e62e2..af4353744 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 7b11e62e2a40a3dab7f039d4953f1514c73cb6d5 +Subproject commit af43537447ebdc2a241c0cdad615e794a840665a