diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 56e4a3014f..a5fd302dde 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7182,6 +7182,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_photo_editor_menu_flip" = "Flip"; "lng_photo_editor_menu_duplicate" = "Duplicate"; +"lng_photo_editor_text_style_plain" = "Plain"; +"lng_photo_editor_text_style_framed" = "Framed"; +"lng_photo_editor_text_style_semi_transparent" = "Semi-Transparent"; + "lng_photo_editor_crop_original" = "Original"; "lng_photo_editor_crop_square" = "Square"; "lng_photo_editor_crop_free" = "Free"; diff --git a/Telegram/SourceFiles/editor/color_picker.cpp b/Telegram/SourceFiles/editor/color_picker.cpp index cd19a75a06..411169079a 100644 --- a/Telegram/SourceFiles/editor/color_picker.cpp +++ b/Telegram/SourceFiles/editor/color_picker.cpp @@ -570,6 +570,17 @@ void ColorPicker::storeCurrentBrush() { _toolBrushes[ToolIndex(_brush.tool)] = _brush; } +void ColorPicker::setColor(const QColor &color) { + _brush.color = color; + storeCurrentBrush(); + updateColorButtonColor(color, true); + if (_paletteVisible) { + rebuildPalette(); + } else { + _colorButton->update(); + } +} + void ColorPicker::updateColorButtonColor(const QColor &color, bool animated) { const auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid(); const auto from = hasValid ? colorButtonColor() : color; diff --git a/Telegram/SourceFiles/editor/color_picker.h b/Telegram/SourceFiles/editor/color_picker.h index 63b0211b2f..d7e80654fa 100644 --- a/Telegram/SourceFiles/editor/color_picker.h +++ b/Telegram/SourceFiles/editor/color_picker.h @@ -30,6 +30,7 @@ public: void moveLine(const QPoint &position); void setCanvasRect(const QRect &rect); void setVisible(bool visible); + void setColor(const QColor &color); bool preventHandleKeyPress() const; rpl::producer saveBrushRequests() const; diff --git a/Telegram/SourceFiles/editor/editor_paint.cpp b/Telegram/SourceFiles/editor/editor_paint.cpp index 7553983a4d..836e511b36 100644 --- a/Telegram/SourceFiles/editor/editor_paint.cpp +++ b/Telegram/SourceFiles/editor/editor_paint.cpp @@ -67,10 +67,13 @@ Paint::Paint( _scene->setBlurSource(std::move(blurSource)); { - const auto shortSide = std::min(imageSize.width(), imageSize.height()); + constexpr auto kDefaultFontSizeDivisor = 15.; + const auto shortSide = std::min( + imageSize.width(), + imageSize.height()); _scene->setTextDefaults( QColor(255, 255, 255), - shortSide / 15.f, + shortSide / kDefaultFontSizeDivisor, int(TextStyle::Plain)); } @@ -247,6 +250,14 @@ void Paint::createTextItem() { _scene->createTextAtCenter(); } +void Paint::setTextColor(const QColor &color) { + _scene->setTextColor(color); +} + +rpl::producer Paint::textColorRequests() const { + return _scene->textColorRequests(); +} + void Paint::handleMimeData(const QMimeData *data) { const auto add = [&](QImage image) { if (image.isNull()) { diff --git a/Telegram/SourceFiles/editor/editor_paint.h b/Telegram/SourceFiles/editor/editor_paint.h index e2d586aaba..5540b60abc 100644 --- a/Telegram/SourceFiles/editor/editor_paint.h +++ b/Telegram/SourceFiles/editor/editor_paint.h @@ -42,6 +42,9 @@ public: void updateUndoState(); void createTextItem(); + void setTextColor(const QColor &color); + + [[nodiscard]] rpl::producer textColorRequests() const; void handleMimeData(const QMimeData *data); void paintImage(QPainter &p, const QPixmap &image) const; diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index 27d864dc4f..786e6794e5 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -151,7 +151,7 @@ struct BrushState { const auto tool = ToolFromSerialized(entryTool); const auto index = ToolIndex(tool); if (version == kBrushesVersion && size > 0) { - result.brushes[index].sizeRatio = size / float(kPrecision); + result.brushes[index].sizeRatio = size / float64(kPrecision); } if (color.isValid()) { result.brushes[index].color = color; @@ -344,6 +344,7 @@ PhotoEditor::PhotoEditor( _colorPicker->saveBrushRequests( ) | rpl::on_next([=](const Brush &brush) { _content->applyBrush(brush); + _content->setTextColor(brush.color); _brushTool = brush.tool; _brushes[ToolIndex(brush.tool)] = brush; @@ -353,6 +354,11 @@ PhotoEditor::PhotoEditor( Core::App().saveSettingsDelayed(); } }, lifetime()); + + _content->textColorRequests( + ) | rpl::on_next([=](const QColor &color) { + _colorPicker->setColor(color); + }, lifetime()); } void PhotoEditor::keyPressEvent(QKeyEvent *e) { diff --git a/Telegram/SourceFiles/editor/photo_editor_content.cpp b/Telegram/SourceFiles/editor/photo_editor_content.cpp index f9197cde6e..38b0b16c86 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_content.cpp @@ -166,6 +166,14 @@ void PhotoEditorContent::createTextItem() { _paint->createTextItem(); } +void PhotoEditorContent::setTextColor(const QColor &color) { + _paint->setTextColor(color); +} + +rpl::producer PhotoEditorContent::textColorRequests() const { + return _paint->textColorRequests(); +} + bool PhotoEditorContent::handleKeyPress(not_null e) const { return false; } diff --git a/Telegram/SourceFiles/editor/photo_editor_content.h b/Telegram/SourceFiles/editor/photo_editor_content.h index 39c411358c..95bb3ee439 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.h +++ b/Telegram/SourceFiles/editor/photo_editor_content.h @@ -32,6 +32,9 @@ public: void applyMode(const PhotoEditorMode &mode); void applyBrush(const Brush &brush); void createTextItem(); + void setTextColor(const QColor &color); + + [[nodiscard]] rpl::producer textColorRequests() const; void applyAspectRatio(float64 ratio); void save(PhotoModifications &modifications); diff --git a/Telegram/SourceFiles/editor/photo_editor_controls.cpp b/Telegram/SourceFiles/editor/photo_editor_controls.cpp index 21b6b01b38..e0985021a4 100644 --- a/Telegram/SourceFiles/editor/photo_editor_controls.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_controls.cpp @@ -206,9 +206,10 @@ class TextToolButton final : public Ui::AbstractButton { public: TextToolButton(not_null parent) : AbstractButton(parent) { + constexpr auto kSizeShrink = 6; resize( - st::photoEditorStickersButton.width, - st::photoEditorStickersButton.height); + st::photoEditorStickersButton.width - kSizeShrink, + st::photoEditorStickersButton.height - kSizeShrink); events( ) | rpl::on_next([=](not_null event) { if (event->type() == QEvent::Enter diff --git a/Telegram/SourceFiles/editor/scene/scene.cpp b/Telegram/SourceFiles/editor/scene/scene.cpp index 306c63e8b7..f0ac32d829 100644 --- a/Telegram/SourceFiles/editor/scene/scene.cpp +++ b/Telegram/SourceFiles/editor/scene/scene.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "editor/scene/scene_item_line.h" #include "editor/scene/scene_item_sticker.h" #include "editor/scene/scene_item_text.h" -#include "ui/emoji_config.h" +#include "editor/scene/scene_emoji_document.h" #include "ui/image/image_prepare.h" #include "ui/rp_widget.h" #include "styles/style_editor.h" @@ -22,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include -#include namespace Editor { namespace { @@ -108,6 +107,13 @@ bool SkipMouseEvent(not_null event) { return event->isAccepted() || (event->button() == Qt::RightButton); } +constexpr auto kPaddingFactor = 0.4; +constexpr auto kMaxWidthFactor = 0.8; +constexpr auto kMinWidthFactor = 0.16; +constexpr auto kIdealWidthExtra = 2; +constexpr auto kDefaultFontSizeDivisor = 15.; +constexpr auto kScaleThreshold = 0.01; + class TextEditProxy final : public QGraphicsTextItem { public: using QGraphicsTextItem::QGraphicsTextItem; @@ -141,7 +147,7 @@ private: const auto cb = std::exchange(callback, nullptr); onFinish = nullptr; onCancel = nullptr; - QTimer::singleShot(0, cb); + crl::on_main(cb); } }; @@ -352,21 +358,30 @@ void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { _canvas->handleMouseMoveEvent(event); } -void Scene::applyBrush(const QColor &color, float size, Brush::Tool tool) { +void Scene::applyBrush(const QColor &color, float64 size, Brush::Tool tool) { _canvas->applyBrush(color, size, tool); - for (auto *item : selectedItems()) { - if (item->type() == ItemText::Type) { - static_cast(item)->setColor(color); - } - } } -void Scene::setTextDefaults(const QColor &color, float fontSize, int style) { +void Scene::setTextDefaults( + const QColor &color, + float64 fontSize, + int style) { _textColor = color; _textFontSize = fontSize; _textStyle = style; } +void Scene::setTextColor(const QColor &color) { + _textColor = color; + if (_textEdit.proxy) { + _textEdit.proxy->setDefaultTextColor(color); + } +} + +rpl::producer Scene::textColorRequests() const { + return _textColorRequests.events(); +} + void Scene::setBlurSource(Fn source) { _blurSource = std::move(source); } @@ -495,24 +510,19 @@ void Scene::restore(SaveState state) { cancelDrawing(); } -void Scene::createTextAtCenter() { - if (_textEdit.proxy) { - return; - } - - clearSelection(); - cancelDrawing(); - - auto *proxy = new TextEditProxy(); +void Scene::setupTextProxy( + QGraphicsTextItem *proxy, + const QColor &color, + float64 fontSize) { proxy->setTextInteractionFlags(Qt::TextEditorInteraction); - proxy->setDefaultTextColor(_textColor); + proxy->setDefaultTextColor(color); auto *emojiDoc = new EmojiDocument(proxy); emojiDoc->setDocumentMargin(0); proxy->setDocument(emojiDoc); auto font = QFont(); - font.setPixelSize(int(_textFontSize)); + font.setPixelSize(int(fontSize)); font.setWeight(QFont::DemiBold); proxy->setFont(font); @@ -521,19 +531,33 @@ void Scene::createTextAtCenter() { option.setAlignment(Qt::AlignCenter); emojiDoc->setDefaultTextOption(option); } +} +void Scene::createTextAtCenter() { + if (_textEdit.proxy) { + return; + } + + clearSelection(); + cancelDrawing(); + + _textEdit.proxy.reset(new TextEditProxy()); + const auto proxy = _textEdit.proxy.get(); + setupTextProxy(proxy, _textColor, _textFontSize); + + const auto emojiDoc = proxy->document(); const auto shortSide = std::min( sceneRect().width(), sceneRect().height()); - const auto padding = int(_textFontSize * 0.4); - const auto maxTextWidth = int(shortSide * 0.8) - 2 * padding; - const auto minTextWidth = int(shortSide * 0.16) - 2 * padding; + const auto padding = int(_textFontSize * kPaddingFactor); + const auto maxTextWidth = int(shortSide * kMaxWidthFactor) - 2 * padding; + const auto minTextWidth = int(shortSide * kMinWidthFactor) - 2 * padding; const auto sceneCenter = sceneRect().center(); const auto adjustWidth = [=] { emojiDoc->setTextWidth(maxTextWidth); const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto width = std::clamp( - ideal + 2, + ideal + kIdealWidthExtra, minTextWidth, maxTextWidth); proxy->setTextWidth(width); @@ -553,14 +577,16 @@ void Scene::createTextAtCenter() { views().first()->setFocus(); } - proxy->onFinish = crl::guard(this, [=] { + const auto raw = static_cast(proxy); + raw->onFinish = crl::guard(this, [=] { finishTextEditing(true); }); - proxy->onCancel = crl::guard(this, [=] { + raw->onCancel = crl::guard(this, [=] { finishTextEditing(false); }); - _textEdit = { .item = nullptr, .proxy = proxy }; + _textEdit.item.reset(); + _textColorRequests.fire_copy(_textColor); } void Scene::startTextEditing(ItemText *item) { @@ -573,40 +599,26 @@ void Scene::startTextEditing(ItemText *item) { cancelDrawing(); - auto *proxy = new TextEditProxy(); - proxy->setTextInteractionFlags(Qt::TextEditorInteraction); - proxy->setDefaultTextColor(item->color()); - - auto *emojiDoc = new EmojiDocument(proxy); - emojiDoc->setDocumentMargin(0); - proxy->setDocument(emojiDoc); - - auto font = QFont(); - font.setPixelSize(int(item->fontSize())); - font.setWeight(QFont::DemiBold); - proxy->setFont(font); - - { - auto option = emojiDoc->defaultTextOption(); - option.setAlignment(Qt::AlignCenter); - emojiDoc->setDefaultTextOption(option); - } + _textEdit.proxy.reset(new TextEditProxy()); + const auto proxy = _textEdit.proxy.get(); + setupTextProxy(proxy, item->color(), item->fontSize()); proxy->setPlainText(item->text()); - ReplaceEmoji(emojiDoc); + ReplaceEmoji(proxy->document()); + const auto emojiDoc = proxy->document(); const auto shortSide = std::min( sceneRect().width(), sceneRect().height()); - const auto padding = int(item->fontSize() * 0.4); - const auto maxTextWidth = int(shortSide * 0.8) - 2 * padding; - const auto minTextWidth = int(shortSide * 0.16) - 2 * padding; + const auto padding = int(item->fontSize() * kPaddingFactor); + const auto maxTextWidth = int(shortSide * kMaxWidthFactor) - 2 * padding; + const auto minTextWidth = int(shortSide * kMinWidthFactor) - 2 * padding; const auto anchor = item->scenePos(); const auto adjustWidth = [=] { emojiDoc->setTextWidth(maxTextWidth); const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto width = std::clamp( - ideal + 2, + ideal + kIdealWidthExtra, minTextWidth, maxTextWidth); proxy->setTextWidth(width); @@ -623,7 +635,7 @@ void Scene::startTextEditing(ItemText *item) { const auto scale = item->editScale(); proxy->setRotation(item->rotation()); - if (std::abs(scale - 1.) > 0.01) { + if (std::abs(scale - 1.) > kScaleThreshold) { proxy->setScale(scale); } @@ -637,14 +649,19 @@ void Scene::startTextEditing(ItemText *item) { item->setVisible(false); - proxy->onFinish = crl::guard(this, [=] { + const auto raw = static_cast(proxy); + raw->onFinish = crl::guard(this, [=] { finishTextEditing(true); }); - proxy->onCancel = crl::guard(this, [=] { + raw->onCancel = crl::guard(this, [=] { finishTextEditing(false); }); - _textEdit = { .item = item, .proxy = proxy }; + const auto it = _itemsByPointer.find(item); + _textEdit.item = (it != end(_itemsByPointer)) + ? it->second + : std::weak_ptr(); + _textColorRequests.fire_copy(item->color()); } void Scene::finishTextEditing(bool save) { @@ -658,15 +675,19 @@ void Scene::finishTextEditing(bool save) { const auto proxyRect = _textEdit.proxy->boundingRect(); const auto proxyCenter = _textEdit.proxy->pos() + QPointF(proxyRect.width() / 2., proxyRect.height() / 2.); - auto *existingItem = _textEdit.item; + const auto lockedItem = _textEdit.item.lock(); + auto *existingItem = lockedItem + ? static_cast(lockedItem.get()) + : (ItemText*)(nullptr); - const auto proxy = static_cast(_textEdit.proxy); - proxy->onFinish = nullptr; - proxy->onCancel = nullptr; - QGraphicsScene::removeItem(proxy); - delete proxy; + const auto raw = static_cast(_textEdit.proxy.get()); + raw->onFinish = nullptr; + raw->onCancel = nullptr; + QGraphicsScene::removeItem(_textEdit.proxy.get()); _textEdit.proxy = nullptr; - _textEdit.item = nullptr; + _textEdit.item.reset(); + + const auto defaultStyle = static_cast(_textStyle); if (!text.isEmpty()) { if (existingItem) { @@ -678,7 +699,7 @@ void Scene::finishTextEditing(bool save) { text, _textFontSize, imageSize, - TextStyle::Plain); + defaultStyle); const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.; const auto handleInflate = int( std::ceil(st::photoEditorItemHandleSize / zoom)); @@ -697,7 +718,7 @@ void Scene::finishTextEditing(bool save) { text, _textColor, _textFontSize, - TextStyle::Plain, + defaultStyle, imageSize, std::move(data)); addItem(item); @@ -713,17 +734,15 @@ void Scene::finishTextEditing(bool save) { Scene::~Scene() { if (_textEdit.proxy) { - const auto proxy = static_cast(_textEdit.proxy); - proxy->onFinish = nullptr; - proxy->onCancel = nullptr; - QGraphicsScene::removeItem(proxy); - delete proxy; + const auto raw = static_cast( + _textEdit.proxy.get()); + raw->onFinish = nullptr; + raw->onCancel = nullptr; + QGraphicsScene::removeItem(_textEdit.proxy.get()); _textEdit.proxy = nullptr; } - // Prevent destroying by scene of all items. QGraphicsScene::removeItem(_canvas.get()); for (const auto &item : items()) { - // Scene loses ownership of an item. QGraphicsScene::removeItem(item.get()); } } diff --git a/Telegram/SourceFiles/editor/scene/scene.h b/Telegram/SourceFiles/editor/scene/scene.h index 6024c0d9aa..c069e8de7e 100644 --- a/Telegram/SourceFiles/editor/scene/scene.h +++ b/Telegram/SourceFiles/editor/scene/scene.h @@ -31,9 +31,9 @@ public: Scene(const QRectF &rect); ~Scene(); - void applyBrush(const QColor &color, float size, Brush::Tool tool); + void applyBrush(const QColor &color, float64 size, Brush::Tool tool); void setBlurSource(Fn source); - void setTextDefaults(const QColor &color, float fontSize, int style); + void setTextDefaults(const QColor &color, float64 fontSize, int style); [[nodiscard]] std::vector items( Qt::SortOrder order = Qt::DescendingOrder) const; @@ -51,6 +51,9 @@ public: void startTextEditing(ItemText *item); void createTextAtCenter(); + void setTextColor(const QColor &color); + + [[nodiscard]] rpl::producer textColorRequests() const; [[nodiscard]] bool hasUndo() const; [[nodiscard]] bool hasRedo() const; @@ -69,6 +72,10 @@ protected: private: void removeIf(Fn proj); void finishTextEditing(bool save); + void setupTextProxy( + QGraphicsTextItem *proxy, + const QColor &color, + float64 fontSize); const std::shared_ptr _canvas; const std::shared_ptr _lastZ; @@ -82,15 +89,16 @@ private: int _itemNumber = 0; QColor _textColor; - float _textFontSize = 0.f; - int _textStyle = 3; + float64 _textFontSize = 0.; + int _textStyle = 0; struct { - ItemText *item = nullptr; - QGraphicsTextItem *proxy = nullptr; + std::weak_ptr item; + base::unique_qptr proxy; } _textEdit; rpl::event_stream<> _addsItem, _removesItem; + rpl::event_stream _textColorRequests; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/editor/scene/scene_emoji_document.cpp b/Telegram/SourceFiles/editor/scene/scene_emoji_document.cpp new file mode 100644 index 0000000000..df04598343 --- /dev/null +++ b/Telegram/SourceFiles/editor/scene/scene_emoji_document.cpp @@ -0,0 +1,132 @@ +/* +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 "editor/scene/scene_emoji_document.h" + +#include "ui/emoji_config.h" +#include "ui/painter.h" + +#include +#include + +namespace Editor { + +EmojiDocument::EmojiDocument(QObject *parent) +: QTextDocument(parent) { +} + +QVariant EmojiDocument::loadResource(int type, const QUrl &name) { + if (type != QTextDocument::ImageResource + || name.scheme() != u"emoji"_q) { + return QTextDocument::loadResource(type, name); + } + const auto i = _cache.find(name); + if (i != _cache.end()) { + return i->second; + } + auto result = QVariant(); + if (const auto emoji = Ui::Emoji::FromUrl(name.toDisplayString())) { + const auto factor = style::DevicePixelRatio(); + const auto logical = QFontMetrics(defaultFont()).height(); + const auto source = Ui::Emoji::GetSizeLarge(); + auto image = QImage( + QSize(logical, logical) * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + const auto sourceLogical = source / float64(factor); + const auto scale = logical / sourceLogical; + p.scale(scale, scale); + Ui::Emoji::Draw(p, emoji, source, 0, 0); + } + result = QVariant(QPixmap::fromImage(std::move(image))); + } + _cache.emplace(name, result); + return result; +} + +void ReplaceEmoji(QTextDocument *doc) { + QSignalBlocker blocker(doc); + const auto fontHeight = QFontMetrics(doc->defaultFont()).height(); + auto cursor = QTextCursor(doc); + auto block = doc->begin(); + while (block.isValid()) { + auto text = block.text(); + auto start = text.constData(); + auto end = start + text.size(); + auto ch = start; + while (ch < end) { + auto emojiLength = 0; + const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength); + if (!emoji || emojiLength <= 0) { + ++ch; + continue; + } + const auto pos = block.position() + int(ch - start); + cursor.setPosition(pos); + cursor.setPosition( + pos + emojiLength, + QTextCursor::KeepAnchor); + + auto format = QTextImageFormat(); + format.setName(emoji->toUrl()); + format.setWidth(fontHeight); + format.setHeight(fontHeight); + format.setVerticalAlignment( + QTextCharFormat::AlignBaseline); + cursor.insertImage(format); + + block = doc->findBlock(pos); + text = block.text(); + start = text.constData(); + end = start + text.size(); + ch = start + (pos - block.position()) + 1; + continue; + } + block = block.next(); + } +} + +QString RecoverTextFromDocument(QTextDocument *doc) { + auto result = QString(); + auto block = doc->begin(); + while (block.isValid()) { + if (block != doc->begin()) { + result += '\n'; + } + auto it = block.begin(); + while (!it.atEnd()) { + const auto fragment = it.fragment(); + if (!fragment.isValid()) { + ++it; + continue; + } + const auto text = fragment.text(); + const auto format = fragment.charFormat(); + for (const auto &ch : text) { + if (ch == QChar::ObjectReplacementCharacter) { + if (format.isImageFormat()) { + const auto name = format.toImageFormat().name(); + if (const auto emoji = Ui::Emoji::FromUrl(name)) { + result += emoji->text(); + continue; + } + } + } + result += ch; + } + ++it; + } + block = block.next(); + } + return result; +} + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/scene/scene_emoji_document.h b/Telegram/SourceFiles/editor/scene/scene_emoji_document.h new file mode 100644 index 0000000000..eff28e4907 --- /dev/null +++ b/Telegram/SourceFiles/editor/scene/scene_emoji_document.h @@ -0,0 +1,26 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include + +namespace Editor { + +class EmojiDocument final : public QTextDocument { +public: + explicit EmojiDocument(QObject *parent = nullptr); + QVariant loadResource(int type, const QUrl &name) override; + +private: + std::map _cache; +}; + +void ReplaceEmoji(QTextDocument *doc); +[[nodiscard]] QString RecoverTextFromDocument(QTextDocument *doc); + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/scene/scene_item_text.cpp b/Telegram/SourceFiles/editor/scene/scene_item_text.cpp index b9a1b7ae0c..a0dc900105 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_text.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_text.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "editor/scene/scene_item_text.h" #include "editor/scene/scene.h" +#include "editor/scene/scene_emoji_document.h" #include "lang/lang_keys.h" #include "ui/emoji_config.h" #include "ui/painter.h" @@ -26,9 +27,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { namespace { -constexpr auto kPaddingFactor = 0.4f; -constexpr auto kMaxWidthFactor = 0.8f; +constexpr auto kPaddingFactor = 0.4; +constexpr auto kMaxWidthFactor = 0.8; constexpr auto kMinContentWidth = 20; +constexpr auto kBrightnessFramedThreshold = 0.721; +constexpr auto kBrightnessSemiTransparentThreshold = 0.25; +constexpr auto kSemiTransparentAlpha = 0x99; +constexpr auto kCornerRadiusFactor = 1. / 3.; +constexpr auto kLinePadHFactor = 1. / 3.; +constexpr auto kLinePadVFactor = 1. / 8.; +constexpr auto kMergeRadiusFactor = 1.5; +constexpr auto kLineShiftFactor = 1. / 7.; struct LayoutMetrics { int contentWidth = 0; @@ -37,22 +46,22 @@ struct LayoutMetrics { int textMaxWidth = 0; }; -QFont TextFont(float fontSize) { +QFont TextFont(float64 fontSize) { auto font = QFont(); font.setPixelSize(std::max(int(fontSize), 1)); font.setWeight(QFont::DemiBold); return font; } -float ComputeBrightness(const QColor &color) { - return (color.red() * 0.2126f - + color.green() * 0.7152f - + color.blue() * 0.0722f) / 255.f; +float64 ComputeBrightness(const QColor &color) { + return (color.red() * 0.2126 + + color.green() * 0.7152 + + color.blue() * 0.0722) / 255.; } LayoutMetrics ComputeMetrics( const QString &text, - float fontSize, + float64 fontSize, const QSize &imageSize, TextStyle style) { const auto hasBackground = (style == TextStyle::Framed) @@ -96,33 +105,33 @@ LayoutMetrics ComputeMetrics( } struct LineRect { - float left = 0; - float top = 0; - float right = 0; - float bottom = 0; - [[nodiscard]] float width() const { return right - left; } + float64 left = 0; + float64 top = 0; + float64 right = 0; + float64 bottom = 0; + [[nodiscard]] float64 width() const { return right - left; } }; QPainterPath BuildConnectedBackground( const QTextLayout &layout, int contentWidth, int padding, - float fontSize) { - const auto linePadH = fontSize / 3.f; - const auto linePadV = fontSize / 8.f; - const auto cornerRadius = fontSize / 3.f; - const auto mergeRadius = cornerRadius * 1.5f; - const auto centerX = padding + contentWidth / 2.f; + float64 fontSize) { + const auto linePadH = fontSize * kLinePadHFactor; + const auto linePadV = fontSize * kLinePadVFactor; + const auto cornerRadius = fontSize * kCornerRadiusFactor; + const auto mergeRadius = cornerRadius * kMergeRadiusFactor; + const auto centerX = padding + contentWidth / 2.; auto rects = std::vector(); for (auto i = 0; i < layout.lineCount(); ++i) { const auto line = layout.lineAt(i); - const auto hw = float(line.naturalTextWidth()) / 2.f + linePadH; + const auto hw = float64(line.naturalTextWidth()) / 2. + linePadH; rects.push_back({ .left = centerX - hw, - .top = padding + float(line.y()) - linePadV, + .top = padding + float64(line.y()) - linePadV, .right = centerX + hw, - .bottom = padding + float(line.y() + line.height()) + linePadV, + .bottom = padding + float64(line.y() + line.height()) + linePadV, }); } @@ -157,7 +166,8 @@ QPainterPath BuildConnectedBackground( } if (traceback) { for (auto j = i; j >= 1; --j) { - if (std::abs(rects[j - 1].left - rects[j].left) < mergeRadius) { + if (std::abs(rects[j - 1].left - rects[j].left) + < mergeRadius) { const auto v = std::min( rects[j - 1].left, rects[j].left); @@ -174,14 +184,14 @@ QPainterPath BuildConnectedBackground( } } - struct V { float x, y; }; + struct V { float64 x, y; }; auto verts = std::vector(); verts.push_back({ rects[0].left, rects[0].top }); verts.push_back({ rects[0].right, rects[0].top }); for (auto i = 1; i < int(rects.size()); ++i) { - if (std::abs(rects[i].right - rects[i - 1].right) > 0.5f) { + if (std::abs(rects[i].right - rects[i - 1].right) > 0.5) { verts.push_back({ rects[i - 1].right, rects[i].top }); verts.push_back({ rects[i].right, rects[i].top }); } @@ -192,7 +202,7 @@ QPainterPath BuildConnectedBackground( verts.push_back({ rects[last].left, rects[last].bottom }); for (auto i = last - 1; i >= 0; --i) { - if (std::abs(rects[i].left - rects[i + 1].left) > 0.5f) { + if (std::abs(rects[i].left - rects[i + 1].left) > 0.5) { verts.push_back({ rects[i + 1].left, rects[i + 1].top }); verts.push_back({ rects[i].left, rects[i + 1].top }); } @@ -213,7 +223,7 @@ QPainterPath BuildConnectedBackground( const auto dy2 = next.y - curr.y; const auto len2 = std::sqrt(dx2 * dx2 + dy2 * dy2); - if (len1 < 0.1f || len2 < 0.1f) { + if (len1 < 0.1 || len2 < 0.1) { if (i == 0) { path.moveTo(curr.x, curr.y); } else { @@ -224,8 +234,8 @@ QPainterPath BuildConnectedBackground( const auto r = std::min({ cornerRadius, - len1 / 2.f, - len2 / 2.f, + len1 / 2., + len2 / 2., }); const auto bx = curr.x - dx1 / len1 * r; const auto by = curr.y - dy1 / len1 * r; @@ -245,126 +255,10 @@ QPainterPath BuildConnectedBackground( } // namespace -EmojiDocument::EmojiDocument(QObject *parent) -: QTextDocument(parent) { -} - -QVariant EmojiDocument::loadResource(int type, const QUrl &name) { - if (type != QTextDocument::ImageResource - || name.scheme() != u"emoji"_q) { - return QTextDocument::loadResource(type, name); - } - const auto i = _cache.find(name); - if (i != _cache.end()) { - return i->second; - } - auto result = QVariant(); - if (const auto emoji = Ui::Emoji::FromUrl(name.toDisplayString())) { - const auto factor = style::DevicePixelRatio(); - const auto logical = QFontMetrics(defaultFont()).height(); - const auto source = Ui::Emoji::GetSizeLarge(); - auto image = QImage( - QSize(logical, logical) * factor, - QImage::Format_ARGB32_Premultiplied); - image.setDevicePixelRatio(factor); - image.fill(Qt::transparent); - { - auto p = QPainter(&image); - auto hq = PainterHighQualityEnabler(p); - const auto enlarged = logical * 1.0; - const auto sourceLogical = source / float64(factor); - const auto scale = enlarged / sourceLogical; - const auto offset = (logical - enlarged) / 2.; - p.translate(offset, offset); - p.scale(scale, scale); - Ui::Emoji::Draw(p, emoji, source, 0, 0); - } - result = QVariant(QPixmap::fromImage(std::move(image))); - } - _cache.emplace(name, result); - return result; -} - -void ReplaceEmoji(QTextDocument *doc) { - const auto fontHeight = QFontMetrics(doc->defaultFont()).height(); - auto cursor = QTextCursor(doc); - auto block = doc->begin(); - while (block.isValid()) { - auto text = block.text(); - auto start = text.constData(); - auto end = start + text.size(); - auto ch = start; - while (ch < end) { - auto emojiLength = 0; - const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength); - if (!emoji) { - ++ch; - continue; - } - const auto pos = block.position() + int(ch - start); - cursor.setPosition(pos); - cursor.setPosition( - pos + emojiLength, - QTextCursor::KeepAnchor); - - auto format = QTextImageFormat(); - format.setName(emoji->toUrl()); - format.setWidth(fontHeight); - format.setHeight(fontHeight); - format.setVerticalAlignment( - QTextCharFormat::AlignBaseline); - cursor.insertImage(format); - - block = doc->findBlock(pos); - text = block.text(); - start = text.constData(); - end = start + text.size(); - ch = start + (pos - block.position()) + 1; - continue; - } - block = block.next(); - } -} - -QString RecoverTextFromDocument(QTextDocument *doc) { - auto result = QString(); - auto block = doc->begin(); - while (block.isValid()) { - if (block != doc->begin()) { - result += '\n'; - } - auto it = block.begin(); - while (!it.atEnd()) { - const auto fragment = it.fragment(); - if (!fragment.isValid()) { - ++it; - continue; - } - const auto text = fragment.text(); - const auto format = fragment.charFormat(); - for (const auto &ch : text) { - if (ch == QChar::ObjectReplacementCharacter) { - if (format.isImageFormat()) { - const auto name = format.toImageFormat().name(); - if (const auto emoji = Ui::Emoji::FromUrl(name)) { - result += emoji->text(); - continue; - } - } - } - result += ch; - } - ++it; - } - block = block.next(); - } - return result; -} - ItemText::ItemText( const QString &text, const QColor &color, - float fontSize, + float64 fontSize, TextStyle style, const QSize &imageSize, ItemBase::Data data) @@ -445,14 +339,14 @@ void ItemText::renderContent() { switch (_textStyle) { case TextStyle::Framed: bgColor = _color; - textColor = (brightness >= 0.721f) + textColor = (brightness >= kBrightnessFramedThreshold) ? QColor(0, 0, 0) : QColor(255, 255, 255); break; case TextStyle::SemiTransparent: - bgColor = (brightness >= 0.25f) - ? QColor(0, 0, 0, 0x99) - : QColor(255, 255, 255, 0x99); + bgColor = (brightness >= kBrightnessSemiTransparentThreshold) + ? QColor(0, 0, 0, kSemiTransparentAlpha) + : QColor(255, 255, 255, kSemiTransparentAlpha); break; case TextStyle::Plain: break; @@ -496,14 +390,14 @@ void ItemText::renderContent() { } } - const auto lineShift = _fontSize / 7.f; + const auto lineShift = _fontSize * kLineShiftFactor; const auto lineCount = layout.lineCount(); p.setPen(textColor); for (auto i = 0; i < lineCount; ++i) { const auto line = layout.lineAt(i); const auto xOffset = (m.contentWidth - line.naturalTextWidth()) / 2.; - const auto yShift = (i < lineCount - 1) ? -lineShift : 0.f; + const auto yShift = (i < lineCount - 1) ? -lineShift : 0.; line.draw( &p, QPointF(m.padding + xOffset, m.padding + yShift)); @@ -519,7 +413,7 @@ void ItemText::renderContent() { const auto line = layout.lineAt(i); const auto xOffset = (m.contentWidth - line.naturalTextWidth()) / 2.; - const auto yShift = (i < lineCount - 1) ? -lineShift : 0.f; + const auto yShift = (i < lineCount - 1) ? -lineShift : 0.; const auto lineStart = line.textStart(); const auto lineText = processedText.mid( lineStart, @@ -567,7 +461,7 @@ void ItemText::renderContent() { QSize ItemText::computeContentSize( const QString &text, - float fontSize, + float64 fontSize, const QSize &imageSize, TextStyle style) { if (text.isEmpty()) { @@ -635,7 +529,7 @@ void ItemText::setColor(const QColor &color) { update(); } -float ItemText::fontSize() const { +float64 ItemText::fontSize() const { return _fontSize; } @@ -685,9 +579,13 @@ void ItemText::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { action->setChecked(true); } }; - add(u"Plain"_q, TextStyle::Plain); - add(u"Framed"_q, TextStyle::Framed); - add(u"Semi-Transparent"_q, TextStyle::SemiTransparent); + add(tr::lng_photo_editor_text_style_plain(tr::now), TextStyle::Plain); + add( + tr::lng_photo_editor_text_style_framed(tr::now), + TextStyle::Framed); + add( + tr::lng_photo_editor_text_style_semi_transparent(tr::now), + TextStyle::SemiTransparent); _contextMenu->addSeparator(); diff --git a/Telegram/SourceFiles/editor/scene/scene_item_text.h b/Telegram/SourceFiles/editor/scene/scene_item_text.h index a35bf4cecc..09d9128bd5 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_text.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_text.h @@ -10,8 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" #include "editor/scene/scene_item_base.h" -#include - namespace Ui { class PopupMenu; } // namespace Ui @@ -24,18 +22,6 @@ enum class TextStyle : uchar { Plain, }; -class EmojiDocument final : public QTextDocument { -public: - explicit EmojiDocument(QObject *parent = nullptr); - QVariant loadResource(int type, const QUrl &name) override; - -private: - std::map _cache; -}; - -void ReplaceEmoji(QTextDocument *doc); -[[nodiscard]] QString RecoverTextFromDocument(QTextDocument *doc); - class ItemText : public ItemBase { public: enum { Type = ItemBase::Type + 2 }; @@ -43,7 +29,7 @@ public: ItemText( const QString &text, const QColor &color, - float fontSize, + float64 fontSize, TextStyle style, const QSize &imageSize, ItemBase::Data data); @@ -60,7 +46,7 @@ public: [[nodiscard]] const QColor &color() const; void setColor(const QColor &color); - [[nodiscard]] float fontSize() const; + [[nodiscard]] float64 fontSize() const; [[nodiscard]] TextStyle textStyle() const; void setTextStyle(TextStyle style); @@ -69,7 +55,7 @@ public: [[nodiscard]] static QSize computeContentSize( const QString &text, - float fontSize, + float64 fontSize, const QSize &imageSize, TextStyle style); @@ -88,7 +74,7 @@ private: QString _text; QColor _color; - float _fontSize; + float64 _fontSize; TextStyle _textStyle = TextStyle::Plain; QSize _imageSize; QPixmap _pixmap; @@ -99,7 +85,7 @@ private: NumberedItem::Status status = NumberedItem::Status::Normal; QString text; QColor color; - float fontSize = 0; + float64 fontSize = 0.; TextStyle textStyle = TextStyle::Plain; }; SavedText _savedState, _keepedState; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 9baeaea9c9..840ab71395 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -149,6 +149,8 @@ PRIVATE editor/scene/scene_item_line.h editor/scene/scene_item_text.cpp editor/scene/scene_item_text.h + editor/scene/scene_emoji_document.cpp + editor/scene/scene_emoji_document.h ui/boxes/about_cocoon_box.h ui/boxes/about_cocoon_box.cpp