diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1b294b370f..62cf4464eb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -843,6 +843,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_auto_night_mode_on" = "System"; "lng_settings_auto_night_warning" = "You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first."; "lng_settings_auto_night_disable" = "Disable"; +"lng_settings_font_family" = "Font family"; "lng_settings_color_title" = "Color preview"; "lng_settings_color_reply" = "Reply to your message"; @@ -5135,6 +5136,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channels_your_less" = "Show less"; "lng_channels_recommended" = "Recommended channels"; +"lng_font_box_title" = "Choose font family"; +"lng_font_default" = "Default"; +"lng_font_system" = "System font"; +"lng_font_not_found" = "Font not found."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index a112c478a8..828b2d0950 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -940,7 +940,7 @@ int BackgroundPreviewBox::textsTop() const { - st::historyPaddingBottom - (_service ? _service->height() : 0) - _text1->height() - - (forChannel() ? _text2->height() : 0); + - (forChannel() ? 0 : _text2->height()); } QRect BackgroundPreviewBox::radialRect() const { diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 917baf16eb..6c25de60ba 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -249,8 +249,6 @@ Application::~Application() { } void Application::run() { - style::internal::StartFonts(); - ThirdParty::start(); // Depends on OpenSSL on macOS, so on ThirdParty::start(). @@ -258,6 +256,10 @@ void Application::run() { _notifications = std::make_unique(); startLocalStorage(); + + style::SetCustomFont(settings().customFontFamily()); + style::internal::StartFonts(); + ValidateScale(); refreshGlobalProxy(); // Depends on app settings being read. diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index faa41445f0..939f4d0537 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -217,7 +217,8 @@ QByteArray Settings::serialize() const { + Serialize::stringSize(_callPlaybackDeviceId.current()) + Serialize::stringSize(_callCaptureDeviceId.current()) + Serialize::bytearraySize(ivPosition) - + Serialize::stringSize(noWarningExtensions); + + Serialize::stringSize(noWarningExtensions) + + Serialize::stringSize(_customFontFamily); auto result = QByteArray(); result.reserve(size); @@ -363,7 +364,8 @@ QByteArray Settings::serialize() const { << _callPlaybackDeviceId.current() << _callCaptureDeviceId.current() << ivPosition - << noWarningExtensions; + << noWarningExtensions + << _customFontFamily; } Ensures(result.size() == size); @@ -481,6 +483,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0); qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0; QByteArray ivPosition; + QString customFontFamily = _customFontFamily; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -766,6 +769,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { noWarningExtensions = QString(); stream >> *noWarningExtensions; } + if (!stream.atEnd()) { + stream >> customFontFamily; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -972,6 +978,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!ivPosition.isEmpty()) { _ivPosition = Deserialize(ivPosition); } + _customFontFamily = customFontFamily; } QString Settings::getSoundPath(const QString &key) const { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index cded2ec200..9ebd4887eb 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -871,6 +871,13 @@ public: _ivPosition = position; } + [[nodiscard]] QString customFontFamily() const { + return _customFontFamily; + } + void setCustomFontFamily(const QString &value) { + _customFontFamily = value; + } + [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); @@ -999,6 +1006,7 @@ private: rpl::variable _storiesClickTooltipHidden = false; rpl::variable _ttlVoiceClickTooltipHidden = false; WindowPosition _ivPosition; + QString _customFontFamily; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index d7c977df62..a373617f3b 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_preview_box.h" #include "boxes/download_path_box.h" #include "boxes/local_storage_box.h" +#include "ui/boxes/choose_font_box.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/fields/input_field.h" @@ -1603,6 +1604,52 @@ void SetupThemeSettings( }); } + const auto family = container->lifetime().make_state< + rpl::variable + >(settings->customFontFamily()); + auto label = family->value() | rpl::map([](QString family) { + return family.isEmpty() + ? tr::lng_font_default(tr::now) + : (family == style::SystemFontTag()) + ? tr::lng_font_system(tr::now) + : family; + }); + AddButtonWithLabel( + container, + tr::lng_settings_font_family(), + std::move(label), + st::settingsButton, + { &st::menuIconTranslate } + )->setClickedCallback([=] { + const auto save = [=](QString chosen) { + *family = chosen; + settings->setCustomFontFamily(chosen); + Local::writeSettings(); + Core::Restart(); + }; + + const auto theme = std::shared_ptr( + Window::Theme::DefaultChatThemeOn(container->lifetime())); + const auto generateBg = [=] { + const auto size = st::boxWidth; + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + auto p = QPainter(&result); + Window::SectionWidget::PaintBackground( + p, + theme.get(), + QSize(size, size * 3), + QRect(0, 0, size, size)); + p.end(); + + return result; + }; + controller->show( + Box(Ui::ChooseFontBox, generateBg, family->current(), save)); + }); + Ui::AddSkip(container, st::settingsCheckboxesSkip); } diff --git a/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp new file mode 100644 index 0000000000..1b2c375fa8 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp @@ -0,0 +1,888 @@ +/* +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 "ui/boxes/choose_font_box.h" + +#include "lang/lang_keys.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/ripple_animation.h" +#include "ui/layers/generic_box.h" +#include "ui/style/style_core_font.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/multi_select.h" +#include "ui/widgets/scroll_area.h" +#include "ui/cached_round_corners.h" +#include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_settings.h" +#include "styles/style_layers.h" +#include "styles/style_window.h" + +#include + +namespace Ui { +namespace { + +constexpr auto kMinTextWidth = 120; +constexpr auto kMaxTextWidth = 320; +constexpr auto kMaxTextLines = 3; + +struct PreviewRequest { + QString family; + QColor msgBg; + QColor msgShadow; + QColor replyBar; + QColor replyNameFg; + QColor textFg; + QImage bubbleTail; +}; + +class PreviewPainter { +public: + PreviewPainter(const QImage &bg, PreviewRequest request); + + QImage takeResult(); + +private: + void layout(); + + void paintBubble(QPainter &p); + void paintContent(QPainter &p); + void paintReply(QPainter &p); + void paintMessage(QPainter &p); + + void validateBubbleCache(); + + const PreviewRequest _request; + const style::owned_color _msgBg; + const style::owned_color _msgShadow; + + QFont _nameFont; + QFontMetricsF _nameMetrics; + int _nameFontHeight = 0; + QFont _textFont; + QFontMetricsF _textMetrics; + int _textFontHeight = 0; + + QString _nameText; + QString _replyText; + QString _messageText; + + int _boundingLimit = 0; + + QRect _replyRect; + QRect _name; + QRect _reply; + QRect _message; + QRect _content; + QRect _bubble; + QSize _outer; + + Ui::CornersPixmaps _bubbleCorners; + QPixmap _bubbleShadowBottomRight; + + QImage _result; + +}; + +class Selector final : public Ui::RpWidget { +public: + Selector( + not_null parent, + const QString &now, + rpl::producer filter, + rpl::producer<> submits, + Fn chosen, + Fn scrollTo); + + void initScroll(); + void setMinHeight(int height); + void selectSkip(Qt::Key direction, int pageSize); + + [[nodiscard]] auto scrollToRequests() const + -> rpl::producer { + return _scrollToRequests.events(); + } + +private: + struct Entry { + QString id; + QString key; + QString text; + QStringList keywords; + QImage cache; + std::unique_ptr check; + std::unique_ptr ripple; + int paletteVersion = 0; + }; + [[nodiscard]] static std::vector FullList(const QString &now); + + int resizeGetHeight(int newWidth) override; + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + [[nodiscard]] bool searching() const; + [[nodiscard]] int shownRowsCount() const; + [[nodiscard]] Entry &shownRowAt(int index); + [[nodiscard]] const Entry &shownRowAt(int index) const; + + void applyFilter(const QString &query); + void updateSelected(int selected); + void updatePressed(int pressed); + void updateRow(int index); + void updateRow(not_null row, int hint); + void addRipple(int index, QPoint position); + void validateCache(Entry &row); + void choose(Entry &row); + + const style::SettingsButton &_st; + std::vector _rows; + std::vector> _filtered; + QString _chosen; + int _selected = -1; + int _pressed = -1; + + Fn _callback; + rpl::event_stream _scrollToRequests; + + int _rowsSkip = 0; + int _rowHeight = 0; + int _minHeight = 0; + + QString _query; + QStringList _queryWords; + + rpl::lifetime _lifetime; + +}; + +Selector::Selector( + not_null parent, + const QString &now, + rpl::producer filter, + rpl::producer<> submits, + Fn chosen, + Fn scrollTo) +: RpWidget(parent) +, _st(st::settingsButton) +, _rows(FullList(now)) +, _chosen(now) +, _callback(std::move(chosen)) +, _rowsSkip(st::settingsInfoPhotoSkip) +, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) { + Expects(_chosen >= 0 && _chosen < _rows.size()); + + setMouseTracking(true); + + std::move(filter) | rpl::start_with_next([=](const QString &query) { + applyFilter(query); + }, _lifetime); + + std::move(submits) | rpl::filter([=] { + return searching() && !_filtered.empty(); + }) | rpl::start_with_next([=] { + choose(*_filtered.front()); + }, _lifetime); + + _scrollToRequests.events( + ) | rpl::start_with_next([=](Ui::ScrollToRequest request) { + scrollTo(request); + }, _lifetime); +} + +void Selector::applyFilter(const QString &query) { + if (_query == query) { + return; + } + _query = query; + + updateSelected(-1); + updatePressed(-1); + + _queryWords = TextUtilities::PrepareSearchWords(_query); + + const auto skip = []( + const QStringList &haystack, + const QStringList &needles) { + const auto find = []( + const QStringList &haystack, + const QString &needle) { + for (const auto &item : haystack) { + if (item.startsWith(needle)) { + return true; + } + } + return false; + }; + for (const auto &needle : needles) { + if (!find(haystack, needle)) { + return true; + } + } + return false; + }; + + _filtered.clear(); + if (!_queryWords.isEmpty()) { + _filtered.reserve(_rows.size()); + for (auto &row : _rows) { + if (!skip(row.keywords, _queryWords)) { + _filtered.push_back(&row); + } else { + row.ripple = nullptr; + } + } + } + + resizeToWidth(width()); + Ui::SendPendingMoveResizeEvents(this); + update(); +} + +void Selector::updateSelected(int selected) { + if (_selected == selected) { + return; + } + const auto was = (_selected >= 0); + updateRow(_selected); + _selected = selected; + updateRow(_selected); + const auto now = (_selected >= 0); + if (was != now) { + setCursor(now ? style::cur_pointer : style::cur_default); + } +} + +void Selector::updatePressed(int pressed) { + if (_pressed == pressed) { + return; + } else if (_pressed >= 0) { + if (auto &ripple = shownRowAt(_pressed).ripple) { + ripple->lastStop(); + } + } + updateRow(_pressed); + _pressed = pressed; + updateRow(_pressed); +} + +void Selector::updateRow(int index) { + if (index >= 0) { + update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight); + } +} + +void Selector::updateRow(not_null row, int hint) { + if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) { + updateRow(hint); + } else if (searching()) { + const auto i = ranges::find(_filtered, row); + if (i != end(_filtered)) { + updateRow(int(i - begin(_filtered))); + } + } else { + const auto index = int(row.get() - &_rows[0]); + Assert(index >= 0 && index < _rows.size()); + updateRow(index); + } +} + +void Selector::validateCache(Entry &row) { + const auto version = style::PaletteVersion(); + if (row.cache.isNull()) { + const auto ratio = style::DevicePixelRatio(); + row.cache = QImage( + QSize(width(), _rowHeight) * ratio, + QImage::Format_ARGB32_Premultiplied); + row.cache.setDevicePixelRatio(ratio); + } else if (row.paletteVersion == version) { + return; + } + row.cache.fill(Qt::transparent); + auto font = style::ResolveFont(row.id, 0, st::boxFontSize); + auto p = QPainter(&row.cache); + p.setFont(font); + p.setPen(st::windowFg); + + const auto textw = width() - _st.padding.left() - _st.padding.right(); + const auto metrics = QFontMetrics(font); + p.drawText( + _st.padding.left(), + _st.padding.top() + metrics.ascent(), + metrics.elidedText(row.text, Qt::ElideRight, textw)); +} + +bool Selector::searching() const { + return !_queryWords.isEmpty(); +} + +int Selector::shownRowsCount() const { + return searching() ? int(_filtered.size()) : int(_rows.size()); +} + +Selector::Entry &Selector::shownRowAt(int index) { + return searching() ? *_filtered[index] : _rows[index]; +} + +const Selector::Entry &Selector::shownRowAt(int index) const { + return const_cast(this)->shownRowAt(index); +} + +void Selector::setMinHeight(int height) { + _minHeight = height; + if (_minHeight > 0) { + resizeToWidth(width()); + } +} + +void Selector::initScroll() { + const auto index = [&] { + if (searching()) { + const auto i = ranges::find(_filtered, _chosen, &Entry::id); + if (i != end(_filtered)) { + return int(i - begin(_filtered)); + } + return -1; + } + const auto i = ranges::find(_rows, _chosen, &Entry::id); + Assert(i != end(_rows)); + return int(i - begin(_rows)); + }(); + if (index >= 0) { + const auto top = _rowsSkip + index * _rowHeight; + const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0); + _scrollToRequests.fire({ use, use + _minHeight }); + } +} + +int Selector::resizeGetHeight(int newWidth) { + const auto added = 2 * _rowsSkip; + return std::max(added + shownRowsCount() * _rowHeight, _minHeight); +} + +void Selector::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto rows = shownRowsCount(); + const auto clip = e->rect(); + const auto clipped = std::max(clip.y() - _rowsSkip, 0); + const auto from = std::min(clipped / _rowHeight, rows); + const auto till = std::min( + (clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight, + rows); + const auto active = (_pressed >= 0) ? _pressed : _selected; + for (auto i = from; i != till; ++i) { + auto &row = shownRowAt(i); + const auto y = _rowsSkip + i * _rowHeight; + const auto bg = (i == active) ? st::windowBgOver : st::windowBg; + const auto rect = QRect(0, y, width(), _rowHeight); + p.fillRect(rect, bg); + + if (row.ripple) { + row.ripple->paint(p, 0, y, width()); + if (row.ripple->empty()) { + row.ripple = nullptr; + } + } + + validateCache(row); + p.drawImage(0, y, row.cache); + + if (!row.check) { + row.check = std::make_unique( + st::defaultRadio, + (row.id == _chosen), + [=, row = &row] { updateRow(row, i); }); + } + row.check->paint( + p, + _st.iconLeft, + y + (_rowHeight - st::defaultRadio.diameter) / 2, + width()); + } +} + +void Selector::mouseMoveEvent(QMouseEvent *e) { + const auto y = e->y() - _rowsSkip; + const auto index = (y >= 0) ? (y / _rowHeight) : -1; + updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1); +} + +void Selector::mousePressEvent(QMouseEvent *e) { + updatePressed(_selected); + if (_pressed >= 0) { + addRipple(_pressed, e->pos()); + } +} + +void Selector::mouseReleaseEvent(QMouseEvent *e) { + const auto pressed = _pressed; + updatePressed(-1); + if (pressed == _selected) { + choose(shownRowAt(pressed)); + } +} + +void Selector::choose(Entry &row) { + const auto id = row.id; + if (_chosen != id) { + const auto i = ranges::find(_rows, _chosen, &Entry::id); + Assert(i != end(_rows)); + if (i->check) { + i->check->setChecked(false, anim::type::normal); + } + _chosen = id; + if (row.check) { + row.check->setChecked(true, anim::type::normal); + } + } + _callback(id); + initScroll(); +} + +void Selector::addRipple(int index, QPoint position) { + Expects(index >= 0 && index < shownRowsCount()); + + const auto row = &shownRowAt(index); + if (!row->ripple) { + row->ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RectMask({ width(), _rowHeight }), + [=] { updateRow(row, index); }); + } + row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight)); +} + +std::vector Selector::FullList(const QString &now) { + using namespace TextUtilities; + + auto database = QFontDatabase(); + auto families = database.families(); + auto result = std::vector(); + result.reserve(families.size() + 3); + const auto add = [&](const QString &text, const QString &id = {}) { + result.push_back({ + .id = id, + .text = text, + .keywords = PrepareSearchWords(text), + }); + }; + add(tr::lng_font_default(tr::now)); + add(tr::lng_font_system(tr::now), style::SystemFontTag()); + for (const auto &family : families) { + if (database.isScalable(family)) { + result.push_back({ .id = family }); + } + } + if (!ranges::contains(result, now, &Entry::id)) { + result.push_back({ .id = now }); + } + for (auto i = begin(result) + 2; i != end(result); ++i) { + i->key = TextUtilities::RemoveAccents(i->id).toLower(); + i->text = i->id; + i->keywords = TextUtilities::PrepareSearchWords(i->id); + } + ranges::sort(begin(result) + 2, end(result), std::less<>(), &Entry::key); + return result; +} + +[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) { + return { + .family = family, + .msgBg = st::msgInBg->c, + .msgShadow = st::msgInShadow->c, + .replyBar = st::msgInReplyBarColor->c, + .replyNameFg = st::msgInServiceFg->c, + .textFg = st::historyTextInFg->c, + .bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c), + }; +} + +PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request) +: _request(request) +, _msgBg(_request.msgBg) +, _msgShadow(_request.msgShadow) +, _nameFont(style::ResolveFont( + _request.family, + style::internal::FontSemibold, + st::fsize)) +, _nameMetrics(_nameFont) +, _nameFontHeight(base::SafeRound(_nameMetrics.height())) +, _textFont(style::ResolveFont(_request.family, 0, st::fsize)) +, _textMetrics(_textFont) +, _textFontHeight(base::SafeRound(_textMetrics.height())) { + layout(); + + const auto ratio = style::DevicePixelRatio(); + _result = QImage( + _outer * ratio, + QImage::Format_ARGB32_Premultiplied); + _result.setDevicePixelRatio(ratio); + + auto p = QPainter(&_result); + p.drawImage(0, 0, bg); + + p.translate(_bubble.topLeft()); + paintBubble(p); +} + +void PreviewPainter::paintBubble(QPainter &p) { + validateBubbleCache(); + const auto bubble = QRect(QPoint(), _bubble.size()); + const auto cornerShadow = _bubbleShadowBottomRight.size() + / _bubbleShadowBottomRight.devicePixelRatio(); + p.drawPixmap( + bubble.width() - cornerShadow.width(), + bubble.height() + st::msgShadow - cornerShadow.height(), + _bubbleShadowBottomRight); + Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners); + const auto &bubbleTail = _request.bubbleTail; + const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio(); + p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail); + p.fillRect( + -tail.width(), + bubble.height(), + tail.width() + bubble.width() - cornerShadow.width(), + st::msgShadow, + _request.msgShadow); + p.translate(_content.topLeft()); + const auto local = _content.translated(-_content.topLeft()); + p.setClipRect(local); + paintContent(p); +} + +void PreviewPainter::validateBubbleCache() { + if (!_bubbleCorners.p[0].isNull()) { + return; + } + const auto radius = st::bubbleRadiusLarge; + _bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color()); + _bubbleCorners.p[2] = {}; + _bubbleShadowBottomRight + = Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3]; +} + +void PreviewPainter::paintContent(QPainter &p) { + paintReply(p); + + p.translate(_message.topLeft()); + const auto local = _message.translated(-_message.topLeft()); + p.setClipRect(local); + paintMessage(p); +} + +void PreviewPainter::paintReply(QPainter &p) { + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(_request.replyBar); + + const auto outline = st::messageTextStyle.blockquote.outline; + const auto radius = st::messageTextStyle.blockquote.radius; + p.setOpacity(Ui::kDefaultOutline1Opacity); + p.setClipRect( + _replyRect.x(), + _replyRect.y(), + outline, + _replyRect.height()); + p.drawRoundedRect(_replyRect, radius, radius); + p.setOpacity(Ui::kDefaultBgOpacity); + p.setClipRect( + _replyRect.x() + outline, + _replyRect.y(), + _replyRect.width() - outline, + _replyRect.height()); + p.drawRoundedRect(_replyRect, radius, radius); + } + p.setOpacity(1.); + p.setClipping(false); + + p.setPen(_request.replyNameFg); + p.setFont(_nameFont); + const auto name = _nameMetrics.elidedText( + _nameText, + Qt::ElideRight, + _name.width()); + p.drawText(_name.x(), _name.y() + _nameMetrics.ascent(), name); + + p.setPen(_request.textFg); + p.setFont(_textFont); + const auto reply = _textMetrics.elidedText( + _replyText, + Qt::ElideRight, + _reply.width()); + p.drawText(_reply.x(), _reply.y() + _textMetrics.ascent(), reply); +} + +void PreviewPainter::paintMessage(QPainter &p) { + p.setPen(_request.textFg); + p.setFont(_textFont); + p.drawText(QRect(0, 0, _message.width(), _boundingLimit), _messageText); +} + +QImage PreviewPainter::takeResult() { + return std::move(_result); +} + +void PreviewPainter::layout() { + const auto skip = st::boxRowPadding.left(); + const auto minTextWidth = style::ConvertScale(kMinTextWidth); + const auto maxTextWidth = st::boxWidth + - 2 * skip + - st::msgPadding.left() + - st::msgPadding.right(); + _boundingLimit = 100 * maxTextWidth; + + const auto textSize = [&]( + const QFontMetricsF &metrics, + const QString &text, + int availableWidth, + bool oneline = false) { + const auto result = metrics.boundingRect( + QRect(0, 0, availableWidth, _boundingLimit), + (Qt::AlignLeft + | Qt::AlignTop + | (oneline ? Qt::TextSingleLine : Qt::TextWordWrap)), + text); + return QSize( + int(std::ceil(result.x() + result.width())), + int(std::ceil(result.y() + result.height()))); + }; + const auto naturalSize = [&]( + const QFontMetricsF &metrics, + const QString &text, + bool oneline = false) { + return textSize(metrics, text, _boundingLimit, oneline); + }; + + _nameText = tr::lng_settings_chat_message_reply_from(tr::now); + _replyText = tr::lng_background_text2(tr::now); + _messageText = tr::lng_background_text1(tr::now); + + const auto nameSize = naturalSize(_nameMetrics, _nameText, true); + const auto nameMaxWidth = nameSize.width(); + const auto replySize = naturalSize(_textMetrics, _replyText, true); + const auto replyMaxWidth = replySize.width(); + const auto messageSize = naturalSize(_textMetrics, _messageText); + const auto messageMaxWidth = messageSize.width(); + + const auto namePosition = QPoint( + st::historyReplyPadding.left(), + st::historyReplyPadding.top()); + const auto replyPosition = QPoint( + st::historyReplyPadding.left(), + (st::historyReplyPadding.top() + _nameFontHeight)); + const auto paddingRight = st::historyReplyPadding.right(); + + const auto wantedWidth = std::max({ + namePosition.x() + nameMaxWidth + paddingRight, + replyPosition.x() + replyMaxWidth + paddingRight, + messageMaxWidth + }); + + const auto messageWidth = std::clamp( + wantedWidth, + minTextWidth, + maxTextWidth); + const auto messageHeight = textSize( + _textMetrics, + _messageText, + maxTextWidth).height(); + + _replyRect = QRect( + st::msgReplyBarPos.x(), + st::historyReplyTop, + messageWidth, + (st::historyReplyPadding.top() + + _nameFontHeight + + _textFontHeight + + st::historyReplyPadding.bottom())); + + _name = QRect( + _replyRect.topLeft() + namePosition, + QSize(messageWidth - namePosition.x(), _nameFontHeight)); + _reply = QRect( + _replyRect.topLeft() + replyPosition, + QSize(messageWidth - replyPosition.x(), _textFontHeight)); + _message = QRect(0, 0, messageWidth, messageHeight); + + const auto replySkip = _replyRect.y() + + _replyRect.height() + + st::historyReplyBottom; + _message.moveTop(replySkip); + + _content = QRect(0, 0, messageWidth, replySkip + messageHeight); + + const auto msgPadding = st::msgPadding; + _bubble = _content.marginsAdded(msgPadding); + _content.moveTopLeft(-_bubble.topLeft()); + _bubble.moveTopLeft({}); + const auto bubbleShadow = st::msgShadow; + + _outer = QSize(st::boxWidth, st::boxWidth / 2); + + _bubble.moveTopLeft({ skip, std::max( + (_outer.height() - _bubble.height()) / 2, + st::msgMargin.top()) }); +} + +[[nodiscard]] QImage GeneratePreview( + const QImage &bg, + PreviewRequest request) { + return PreviewPainter(bg, request).takeResult(); +} + +[[nodiscard]] object_ptr MakePreview( + not_null parent, + Fn generatePreviewBg, + rpl::producer family) { + auto result = object_ptr(parent.get()); + const auto raw = result.data(); + + struct State { + QImage preview; + QImage bg; + QString family; + }; + const auto state = raw->lifetime().make_state(); + + state->bg = generatePreviewBg(); + style::PaletteChanged() | rpl::start_with_next([=] { + state->bg = generatePreviewBg(); + }, raw->lifetime()); + + rpl::combine( + rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()), + std::move(family) + ) | rpl::start_with_next([=](const auto &, QString family) { + state->family = family; + if (state->preview.isNull()) { + state->preview = GeneratePreview( + state->bg, + PrepareRequest(family)); + const auto ratio = state->preview.devicePixelRatio(); + raw->resize(state->preview.size() / int(ratio)); + } else { + const auto weak = Ui::MakeWeak(raw); + const auto request = PrepareRequest(family); + crl::async([=, bg = state->bg] { + crl::on_main([ + weak, + state, + preview = GeneratePreview(bg, request) + ]() mutable { + if (const auto strong = weak.data()) { + state->preview = std::move(preview); + const auto ratio = state->preview.devicePixelRatio(); + strong->resize( + strong->width(), + (state->preview.height() / int(ratio))); + strong->update(); + } + }); + }); + } + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).drawImage(0, 0, state->preview); + }, raw->lifetime()); + + return result; +} + +} // namespace + +void ChooseFontBox( + not_null box, + Fn generatePreviewBg, + const QString &family, + Fn save) { + box->setTitle(tr::lng_font_box_title()); + + struct State { + rpl::variable family; + rpl::variable query; + rpl::event_stream<> submits; + }; + const auto state = box->lifetime().make_state(State{ + .family = family, + }); + + const auto top = box->setPinnedToTopContent( + object_ptr(box)); + top->add(MakePreview(top, generatePreviewBg, state->family.value())); + const auto filter = top->add(object_ptr( + top, + st::defaultMultiSelect, + tr::lng_participant_filter())); + top->resizeToWidth(st::boxWidth); + + filter->setSubmittedCallback([=](Qt::KeyboardModifiers) { + state->submits.fire({}); + }); + filter->setQueryChangedCallback([=](const QString &query) { + state->query = query; + }); + filter->setCancelledCallback([=] { + filter->clearQuery(); + }); + + const auto chosen = [=](const QString &value) { + state->family = value; + filter->clearQuery(); + }; + const auto scrollTo = [=](Ui::ScrollToRequest request) { + box->scrollToY(request.ymin, request.ymax); + }; + const auto selector = box->addRow( + object_ptr( + box, + state->family.current(), + state->query.value(), + state->submits.events(), + chosen, + scrollTo), + QMargins()); + box->setMinHeight(st::boxMaxListHeight); + box->setMaxHeight(st::boxMaxListHeight); + + rpl::combine( + box->heightValue(), + top->heightValue() + ) | rpl::start_with_next([=](int box, int top) { + selector->setMinHeight(box - top); + }, selector->lifetime()); + + box->addButton(tr::lng_settings_save(), [=] { + if (state->family.current() == family) { + box->closeBox(); + return; + } + box->getDelegate()->show(Ui::MakeConfirmBox({ + .text = tr::lng_settings_need_restart(), + .confirmed = [=] { save(state->family.current()); }, + .confirmText = tr::lng_settings_restart_now(), + })); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + box->setFocusCallback([=] { + filter->setInnerFocus(); + }); + box->setInitScrollCallback([=] { + SendPendingMoveResizeEvents(box); + selector->initScroll(); + }); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/choose_font_box.h b/Telegram/SourceFiles/ui/boxes/choose_font_box.h new file mode 100644 index 0000000000..ed79f9bc40 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/choose_font_box.h @@ -0,0 +1,20 @@ +/* +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 + +namespace Ui { + +class GenericBox; + +void ChooseFontBox( + not_null box, + Fn generatePreviewBg, + const QString &family, + Fn save); + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 5c0d6e173e..61c9012c10 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -239,6 +239,8 @@ PRIVATE ui/boxes/calendar_box.h ui/boxes/choose_date_time.cpp ui/boxes/choose_date_time.h + ui/boxes/choose_font_box.cpp + ui/boxes/choose_font_box.h ui/boxes/choose_language_box.cpp ui/boxes/choose_language_box.h ui/boxes/choose_time.cpp diff --git a/Telegram/lib_ui b/Telegram/lib_ui index d944b4e4ef..ae5a61f7ae 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit d944b4e4ef94c7785bb987ab68d360cd0119b97a +Subproject commit ae5a61f7aeaa18eb4016d290c45be990c614a9a1