966 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			966 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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 "base/event_filter.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_boxes.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(Painter &p);
 | 
						|
	void paintContent(Painter &p);
 | 
						|
	void paintReply(Painter &p);
 | 
						|
	void paintMessage(Painter &p);
 | 
						|
 | 
						|
	void validateBubbleCache();
 | 
						|
 | 
						|
	const PreviewRequest _request;
 | 
						|
	const style::owned_color _msgBg;
 | 
						|
	const style::owned_color _msgShadow;
 | 
						|
 | 
						|
	style::owned_font _nameFontOwned;
 | 
						|
	style::font _nameFont;
 | 
						|
	style::TextStyle _nameStyle;
 | 
						|
	style::owned_font _textFontOwned;
 | 
						|
	style::font _textFont;
 | 
						|
	style::TextStyle _textStyle;
 | 
						|
 | 
						|
	Ui::Text::String _nameText;
 | 
						|
	Ui::Text::String _replyText;
 | 
						|
	Ui::Text::String _messageText;
 | 
						|
 | 
						|
	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, anim::type)> scrollTo);
 | 
						|
 | 
						|
	void initScroll(anim::type animated);
 | 
						|
	void setMinHeight(int height);
 | 
						|
	void selectSkip(Qt::Key direction);
 | 
						|
 | 
						|
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 leaveEventHook(QEvent *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);
 | 
						|
 | 
						|
	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;
 | 
						|
 | 
						|
	std::optional<QPoint> _lastGlobalPoint;
 | 
						|
	bool _selectedByKeyboard = false;
 | 
						|
 | 
						|
	Fn<void(QString)> _callback;
 | 
						|
	Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;
 | 
						|
 | 
						|
	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, anim::type)> scrollTo)
 | 
						|
: RpWidget(parent)
 | 
						|
, _st(st::settingsButton)
 | 
						|
, _rows(FullList(now))
 | 
						|
, _chosen(now)
 | 
						|
, _callback(std::move(chosen))
 | 
						|
, _scrollTo(std::move(scrollTo))
 | 
						|
, _rowsSkip(st::settingsInfoPhotoSkip)
 | 
						|
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
 | 
						|
	setMouseTracking(true);
 | 
						|
 | 
						|
	std::move(filter) | rpl::start_with_next([=](const QString &query) {
 | 
						|
		applyFilter(query);
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	std::move(
 | 
						|
		submits
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		if (_selected >= 0) {
 | 
						|
			choose(shownRowAt(_selected));
 | 
						|
		} else if (searching() && !_filtered.empty()) {
 | 
						|
			choose(*_filtered.front());
 | 
						|
		}
 | 
						|
	}, _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);
 | 
						|
	}
 | 
						|
	if (_selectedByKeyboard) {
 | 
						|
		const auto top = (_selected > 0)
 | 
						|
			? (_rowsSkip + _selected * _rowHeight)
 | 
						|
			: 0;
 | 
						|
		const auto bottom = (_selected > 0)
 | 
						|
			? (top + _rowHeight)
 | 
						|
			: _selected
 | 
						|
			? 0
 | 
						|
			: _rowHeight;
 | 
						|
		_scrollTo({ top, bottom }, anim::type::instant);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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.paletteVersion = version;
 | 
						|
	row.cache.fill(Qt::transparent);
 | 
						|
	auto owned = style::owned_font(row.id, 0, st::boxFontSize);
 | 
						|
	const auto font = owned.font();
 | 
						|
	auto p = QPainter(&row.cache);
 | 
						|
	p.setFont(font);
 | 
						|
	p.setPen(st::windowFg);
 | 
						|
 | 
						|
	const auto textw = width() - _st.padding.left() - _st.padding.right();
 | 
						|
	const auto textt = (_rowHeight - font->height) / 2.;
 | 
						|
	p.drawText(
 | 
						|
		_st.padding.left(),
 | 
						|
		textt + font->ascent,
 | 
						|
		font->elided(row.text, 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];
 | 
						|
}
 | 
						|
 | 
						|
void Selector::setMinHeight(int height) {
 | 
						|
	_minHeight = height;
 | 
						|
	if (_minHeight > 0) {
 | 
						|
		resizeToWidth(width());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Selector::selectSkip(Qt::Key key) {
 | 
						|
	const auto count = shownRowsCount();
 | 
						|
	if (key == Qt::Key_Down) {
 | 
						|
		if (_selected + 1 < count) {
 | 
						|
			_selectedByKeyboard = true;
 | 
						|
			updateSelected(_selected + 1);
 | 
						|
		}
 | 
						|
	} else if (key == Qt::Key_Up) {
 | 
						|
		if (_selected >= 0) {
 | 
						|
			_selectedByKeyboard = true;
 | 
						|
			updateSelected(_selected - 1);
 | 
						|
		}
 | 
						|
	} else if (key == Qt::Key_PageDown) {
 | 
						|
		const auto change = _minHeight / _rowHeight;
 | 
						|
		if (_selected + 1 < count) {
 | 
						|
			_selectedByKeyboard = true;
 | 
						|
			updateSelected(std::min(_selected + change, count - 1));
 | 
						|
		}
 | 
						|
	} else if (key == Qt::Key_PageUp) {
 | 
						|
		const auto change = _minHeight / _rowHeight;
 | 
						|
		if (_selected > 0) {
 | 
						|
			_selectedByKeyboard = true;
 | 
						|
			updateSelected(std::max(_selected - change, 0));
 | 
						|
		} else if (!_selected) {
 | 
						|
			_selectedByKeyboard = true;
 | 
						|
			updateSelected(-1);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Selector::initScroll(anim::type animated) {
 | 
						|
	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);
 | 
						|
		_scrollTo({ use, use + _minHeight }, animated);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
	if (!rows) {
 | 
						|
		p.setFont(st::normalFont);
 | 
						|
		p.setPen(st::windowSubTextFg);
 | 
						|
		p.drawText(
 | 
						|
			QRect(0, 0, width(), height() * 2 / 3),
 | 
						|
			tr::lng_font_not_found(tr::now),
 | 
						|
			style::al_center);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	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::langsRadio,
 | 
						|
				(row.id == _chosen),
 | 
						|
				[=, row = &row] { updateRow(row, i); });
 | 
						|
		}
 | 
						|
		row.check->paint(
 | 
						|
			p,
 | 
						|
			_st.iconLeft,
 | 
						|
			y + (_rowHeight - st::langsRadio.diameter) / 2,
 | 
						|
			width());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Selector::leaveEventHook(QEvent *e) {
 | 
						|
	_lastGlobalPoint = std::nullopt;
 | 
						|
	if (!_selectedByKeyboard) {
 | 
						|
		updateSelected(-1);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Selector::mouseMoveEvent(QMouseEvent *e) {
 | 
						|
	if (!_lastGlobalPoint) {
 | 
						|
		_lastGlobalPoint = e->globalPos();
 | 
						|
		if (_selectedByKeyboard) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
	} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
 | 
						|
		return;
 | 
						|
	} else {
 | 
						|
		_lastGlobalPoint = e->globalPos();
 | 
						|
	}
 | 
						|
	_selectedByKeyboard = false;
 | 
						|
	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);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const auto animated = searching()
 | 
						|
		? anim::type::instant
 | 
						|
		: anim::type::normal;
 | 
						|
	_callback(id);
 | 
						|
	initScroll(animated);
 | 
						|
}
 | 
						|
 | 
						|
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 });
 | 
						|
		}
 | 
						|
	}
 | 
						|
	auto nowIt = ranges::find(result, now, &Entry::id);
 | 
						|
	if (nowIt == end(result)) {
 | 
						|
		result.push_back({ .id = now });
 | 
						|
		nowIt = end(result) - 1;
 | 
						|
	}
 | 
						|
	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);
 | 
						|
	}
 | 
						|
	auto skip = 2;
 | 
						|
	if (nowIt - begin(result) >= skip) {
 | 
						|
		std::swap(result[2], *nowIt);
 | 
						|
		++skip;
 | 
						|
	}
 | 
						|
	ranges::sort(
 | 
						|
		begin(result) + skip, 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)
 | 
						|
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
 | 
						|
, _nameFont(_nameFontOwned.font())
 | 
						|
, _nameStyle(st::semiboldTextStyle)
 | 
						|
, _textFontOwned(_request.family, 0, st::fsize)
 | 
						|
, _textFont(_textFontOwned.font())
 | 
						|
, _textStyle(st::defaultTextStyle) {
 | 
						|
	_nameStyle.font = _nameFont;
 | 
						|
	_textStyle.font = _textFont;
 | 
						|
 | 
						|
	layout();
 | 
						|
 | 
						|
	const auto ratio = style::DevicePixelRatio();
 | 
						|
	_result = QImage(
 | 
						|
		_outer * ratio,
 | 
						|
		QImage::Format_ARGB32_Premultiplied);
 | 
						|
	_result.setDevicePixelRatio(ratio);
 | 
						|
 | 
						|
	auto p = Painter(&_result);
 | 
						|
	p.drawImage(0, 0, bg);
 | 
						|
 | 
						|
	p.translate(_bubble.topLeft());
 | 
						|
	paintBubble(p);
 | 
						|
}
 | 
						|
 | 
						|
void PreviewPainter::paintBubble(Painter &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(Painter &p) {
 | 
						|
	paintReply(p);
 | 
						|
 | 
						|
	p.translate(_message.topLeft());
 | 
						|
	const auto local = _message.translated(-_message.topLeft());
 | 
						|
	p.setClipRect(local);
 | 
						|
	paintMessage(p);
 | 
						|
}
 | 
						|
 | 
						|
void PreviewPainter::paintReply(Painter &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);
 | 
						|
	_nameText.drawLeftElided(
 | 
						|
		p,
 | 
						|
		_name.x(),
 | 
						|
		_name.y(),
 | 
						|
		_name.width(),
 | 
						|
		_outer.width());
 | 
						|
 | 
						|
	p.setPen(_request.textFg);
 | 
						|
	_replyText.drawLeftElided(
 | 
						|
		p,
 | 
						|
		_reply.x(),
 | 
						|
		_reply.y(),
 | 
						|
		_reply.width(),
 | 
						|
		_outer.width());
 | 
						|
}
 | 
						|
 | 
						|
void PreviewPainter::paintMessage(Painter &p) {
 | 
						|
	p.setPen(_request.textFg);
 | 
						|
	_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
 | 
						|
	_nameText = Ui::Text::String(
 | 
						|
		_nameStyle,
 | 
						|
		tr::lng_settings_chat_message_reply_from(tr::now));
 | 
						|
	_replyText = Ui::Text::String(
 | 
						|
		_textStyle,
 | 
						|
		tr::lng_background_text2(tr::now));
 | 
						|
	_messageText = Ui::Text::String(
 | 
						|
		_textStyle,
 | 
						|
		tr::lng_background_text1(tr::now),
 | 
						|
		kDefaultTextOptions,
 | 
						|
		st::msgMinWidth / 2);
 | 
						|
 | 
						|
	const auto namePosition = QPoint(
 | 
						|
		st::historyReplyPadding.left(),
 | 
						|
		st::historyReplyPadding.top());
 | 
						|
	const auto replyPosition = QPoint(
 | 
						|
		st::historyReplyPadding.left(),
 | 
						|
		(st::historyReplyPadding.top() + _nameFont->height));
 | 
						|
	const auto paddingRight = st::historyReplyPadding.right();
 | 
						|
 | 
						|
	const auto wantedWidth = std::max({
 | 
						|
		namePosition.x() + _nameText.maxWidth() + paddingRight,
 | 
						|
		replyPosition.x() + _replyText.maxWidth() + paddingRight,
 | 
						|
		_messageText.maxWidth()
 | 
						|
	});
 | 
						|
 | 
						|
	const auto messageWidth = std::clamp(
 | 
						|
		wantedWidth,
 | 
						|
		minTextWidth,
 | 
						|
		maxTextWidth);
 | 
						|
	const auto messageHeight = _messageText.countHeight(messageWidth);
 | 
						|
 | 
						|
	_replyRect = QRect(
 | 
						|
		st::msgReplyBarPos.x(),
 | 
						|
		st::historyReplyTop,
 | 
						|
		messageWidth,
 | 
						|
		(st::historyReplyPadding.top()
 | 
						|
			+ _nameFont->height
 | 
						|
			+ _textFont->height
 | 
						|
			+ st::historyReplyPadding.bottom()));
 | 
						|
 | 
						|
	_name = QRect(
 | 
						|
		_replyRect.topLeft() + namePosition,
 | 
						|
		QSize(messageWidth - namePosition.x(), _nameFont->height));
 | 
						|
	_reply = QRect(
 | 
						|
		_replyRect.topLeft() + replyPosition,
 | 
						|
		QSize(messageWidth - replyPosition.x(), _textFont->height));
 | 
						|
	_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({});
 | 
						|
 | 
						|
	_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,
 | 
						|
			anim::type animated) {
 | 
						|
		box->scrollTo(request, animated);
 | 
						|
	};
 | 
						|
	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);
 | 
						|
 | 
						|
	base::install_event_filter(filter, [=](not_null<QEvent*> e) {
 | 
						|
		if (e->type() == QEvent::KeyPress) {
 | 
						|
			const auto key = static_cast<QKeyEvent*>(e.get())->key();
 | 
						|
			if (key == Qt::Key_Up
 | 
						|
				|| key == Qt::Key_Down
 | 
						|
				|| key == Qt::Key_PageUp
 | 
						|
				|| key == Qt::Key_PageDown) {
 | 
						|
				selector->selectSkip(Qt::Key(key));
 | 
						|
				return base::EventFilterResult::Cancel;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return base::EventFilterResult::Continue;
 | 
						|
	});
 | 
						|
 | 
						|
	rpl::combine(
 | 
						|
		box->heightValue(),
 | 
						|
		top->heightValue()
 | 
						|
	) | rpl::start_with_next([=](int box, int top) {
 | 
						|
		selector->setMinHeight(box - top);
 | 
						|
	}, selector->lifetime());
 | 
						|
 | 
						|
	const auto apply = [=](QString chosen) {
 | 
						|
		if (chosen == family) {
 | 
						|
			box->closeBox();
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		box->getDelegate()->show(Ui::MakeConfirmBox({
 | 
						|
			.text = tr::lng_settings_need_restart(),
 | 
						|
			.confirmed = [=] { save(chosen); },
 | 
						|
			.confirmText = tr::lng_settings_restart_now(),
 | 
						|
		}));
 | 
						|
	};
 | 
						|
	const auto refreshButtons = [=](QString chosen) {
 | 
						|
		box->clearButtons();
 | 
						|
		// Doesn't fit in most languages.
 | 
						|
		//if (!chosen.isEmpty()) {
 | 
						|
		//	box->addLeftButton(tr::lng_background_reset_default(), [=] {
 | 
						|
		//		apply(QString());
 | 
						|
		//	});
 | 
						|
		//}
 | 
						|
		box->addButton(tr::lng_settings_save(), [=] {
 | 
						|
			apply(chosen);
 | 
						|
		});
 | 
						|
		box->addButton(tr::lng_cancel(), [=] {
 | 
						|
			box->closeBox();
 | 
						|
		});
 | 
						|
	};
 | 
						|
	state->family.value(
 | 
						|
	) | rpl::start_with_next(refreshButtons, box->lifetime());
 | 
						|
 | 
						|
	box->setFocusCallback([=] {
 | 
						|
		filter->setInnerFocus();
 | 
						|
	});
 | 
						|
	box->setInitScrollCallback([=] {
 | 
						|
		SendPendingMoveResizeEvents(box);
 | 
						|
		selector->initScroll(anim::type::instant);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Ui
 |