Add choose font settings.

This commit is contained in:
John Preston 2024-04-23 17:28:35 +04:00
parent 25bd2b145b
commit 97ecc57be8
10 changed files with 986 additions and 6 deletions

View File

@ -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...";

View File

@ -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 {

View File

@ -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<Window::Notifications::System>();
startLocalStorage();
style::SetCustomFont(settings().customFontFamily());
style::internal::StartFonts();
ValidateScale();
refreshGlobalProxy(); // Depends on app settings being read.

View File

@ -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 {

View File

@ -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<bool> _storiesClickTooltipHidden = false;
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
WindowPosition _ivPosition;
QString _customFontFamily;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View File

@ -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<QString>
>(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<Ui::ChatTheme>(
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);
}

View File

@ -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 <QtGui/QFontDatabase>
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<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest)> scrollTo);
void initScroll();
void setMinHeight(int height);
void selectSkip(Qt::Key direction, int pageSize);
[[nodiscard]] auto scrollToRequests() const
-> rpl::producer<Ui::ScrollToRequest> {
return _scrollToRequests.events();
}
private:
struct Entry {
QString id;
QString key;
QString text;
QStringList keywords;
QImage cache;
std::unique_ptr<Ui::RadioView> check;
std::unique_ptr<Ui::RippleAnimation> ripple;
int paletteVersion = 0;
};
[[nodiscard]] static std::vector<Entry> 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<Entry*> row, int hint);
void addRipple(int index, QPoint position);
void validateCache(Entry &row);
void choose(Entry &row);
const style::SettingsButton &_st;
std::vector<Entry> _rows;
std::vector<not_null<Entry*>> _filtered;
QString _chosen;
int _selected = -1;
int _pressed = -1;
Fn<void(QString)> _callback;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
int _rowsSkip = 0;
int _rowHeight = 0;
int _minHeight = 0;
QString _query;
QStringList _queryWords;
rpl::lifetime _lifetime;
};
Selector::Selector(
not_null<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest)> 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<Entry*> 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<Selector*>(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<Ui::RadioView>(
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<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
[=] { updateRow(row, index); });
}
row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
}
std::vector<Selector::Entry> Selector::FullList(const QString &now) {
using namespace TextUtilities;
auto database = QFontDatabase();
auto families = database.families();
auto result = std::vector<Entry>();
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<Ui::RpWidget> MakePreview(
not_null<QWidget*> parent,
Fn<QImage()> generatePreviewBg,
rpl::producer<QString> family) {
auto result = object_ptr<Ui::RpWidget>(parent.get());
const auto raw = result.data();
struct State {
QImage preview;
QImage bg;
QString family;
};
const auto state = raw->lifetime().make_state<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<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save) {
box->setTitle(tr::lng_font_box_title());
struct State {
rpl::variable<QString> family;
rpl::variable<QString> query;
rpl::event_stream<> submits;
};
const auto state = box->lifetime().make_state<State>(State{
.family = family,
});
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
top->add(MakePreview(top, generatePreviewBg, state->family.value()));
const auto filter = top->add(object_ptr<Ui::MultiSelect>(
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<Selector>(
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

View File

@ -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<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save);
} // namespace Ui

View File

@ -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

@ -1 +1 @@
Subproject commit d944b4e4ef94c7785bb987ab68d360cd0119b97a
Subproject commit ae5a61f7aeaa18eb4016d290c45be990c614a9a1