[img-editor] Improved color handling in text items.

This commit is contained in:
23rd
2026-04-12 14:45:01 +03:00
parent 6153480e43
commit 631fe4f362
16 changed files with 378 additions and 259 deletions
+4
View File
@@ -7182,6 +7182,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_photo_editor_menu_flip" = "Flip"; "lng_photo_editor_menu_flip" = "Flip";
"lng_photo_editor_menu_duplicate" = "Duplicate"; "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_original" = "Original";
"lng_photo_editor_crop_square" = "Square"; "lng_photo_editor_crop_square" = "Square";
"lng_photo_editor_crop_free" = "Free"; "lng_photo_editor_crop_free" = "Free";
@@ -570,6 +570,17 @@ void ColorPicker::storeCurrentBrush() {
_toolBrushes[ToolIndex(_brush.tool)] = _brush; _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) { void ColorPicker::updateColorButtonColor(const QColor &color, bool animated) {
const auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid(); const auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid();
const auto from = hasValid ? colorButtonColor() : color; const auto from = hasValid ? colorButtonColor() : color;
@@ -30,6 +30,7 @@ public:
void moveLine(const QPoint &position); void moveLine(const QPoint &position);
void setCanvasRect(const QRect &rect); void setCanvasRect(const QRect &rect);
void setVisible(bool visible); void setVisible(bool visible);
void setColor(const QColor &color);
bool preventHandleKeyPress() const; bool preventHandleKeyPress() const;
rpl::producer<Brush> saveBrushRequests() const; rpl::producer<Brush> saveBrushRequests() const;
+13 -2
View File
@@ -67,10 +67,13 @@ Paint::Paint(
_scene->setBlurSource(std::move(blurSource)); _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( _scene->setTextDefaults(
QColor(255, 255, 255), QColor(255, 255, 255),
shortSide / 15.f, shortSide / kDefaultFontSizeDivisor,
int(TextStyle::Plain)); int(TextStyle::Plain));
} }
@@ -247,6 +250,14 @@ void Paint::createTextItem() {
_scene->createTextAtCenter(); _scene->createTextAtCenter();
} }
void Paint::setTextColor(const QColor &color) {
_scene->setTextColor(color);
}
rpl::producer<QColor> Paint::textColorRequests() const {
return _scene->textColorRequests();
}
void Paint::handleMimeData(const QMimeData *data) { void Paint::handleMimeData(const QMimeData *data) {
const auto add = [&](QImage image) { const auto add = [&](QImage image) {
if (image.isNull()) { if (image.isNull()) {
@@ -42,6 +42,9 @@ public:
void updateUndoState(); void updateUndoState();
void createTextItem(); void createTextItem();
void setTextColor(const QColor &color);
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
void handleMimeData(const QMimeData *data); void handleMimeData(const QMimeData *data);
void paintImage(QPainter &p, const QPixmap &image) const; void paintImage(QPainter &p, const QPixmap &image) const;
+7 -1
View File
@@ -151,7 +151,7 @@ struct BrushState {
const auto tool = ToolFromSerialized(entryTool); const auto tool = ToolFromSerialized(entryTool);
const auto index = ToolIndex(tool); const auto index = ToolIndex(tool);
if (version == kBrushesVersion && size > 0) { if (version == kBrushesVersion && size > 0) {
result.brushes[index].sizeRatio = size / float(kPrecision); result.brushes[index].sizeRatio = size / float64(kPrecision);
} }
if (color.isValid()) { if (color.isValid()) {
result.brushes[index].color = color; result.brushes[index].color = color;
@@ -344,6 +344,7 @@ PhotoEditor::PhotoEditor(
_colorPicker->saveBrushRequests( _colorPicker->saveBrushRequests(
) | rpl::on_next([=](const Brush &brush) { ) | rpl::on_next([=](const Brush &brush) {
_content->applyBrush(brush); _content->applyBrush(brush);
_content->setTextColor(brush.color);
_brushTool = brush.tool; _brushTool = brush.tool;
_brushes[ToolIndex(brush.tool)] = brush; _brushes[ToolIndex(brush.tool)] = brush;
@@ -353,6 +354,11 @@ PhotoEditor::PhotoEditor(
Core::App().saveSettingsDelayed(); Core::App().saveSettingsDelayed();
} }
}, lifetime()); }, lifetime());
_content->textColorRequests(
) | rpl::on_next([=](const QColor &color) {
_colorPicker->setColor(color);
}, lifetime());
} }
void PhotoEditor::keyPressEvent(QKeyEvent *e) { void PhotoEditor::keyPressEvent(QKeyEvent *e) {
@@ -166,6 +166,14 @@ void PhotoEditorContent::createTextItem() {
_paint->createTextItem(); _paint->createTextItem();
} }
void PhotoEditorContent::setTextColor(const QColor &color) {
_paint->setTextColor(color);
}
rpl::producer<QColor> PhotoEditorContent::textColorRequests() const {
return _paint->textColorRequests();
}
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const { bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
return false; return false;
} }
@@ -32,6 +32,9 @@ public:
void applyMode(const PhotoEditorMode &mode); void applyMode(const PhotoEditorMode &mode);
void applyBrush(const Brush &brush); void applyBrush(const Brush &brush);
void createTextItem(); void createTextItem();
void setTextColor(const QColor &color);
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
void applyAspectRatio(float64 ratio); void applyAspectRatio(float64 ratio);
void save(PhotoModifications &modifications); void save(PhotoModifications &modifications);
@@ -206,9 +206,10 @@ class TextToolButton final : public Ui::AbstractButton {
public: public:
TextToolButton(not_null<QWidget*> parent) TextToolButton(not_null<QWidget*> parent)
: AbstractButton(parent) { : AbstractButton(parent) {
constexpr auto kSizeShrink = 6;
resize( resize(
st::photoEditorStickersButton.width, st::photoEditorStickersButton.width - kSizeShrink,
st::photoEditorStickersButton.height); st::photoEditorStickersButton.height - kSizeShrink);
events( events(
) | rpl::on_next([=](not_null<QEvent*> event) { ) | rpl::on_next([=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter if (event->type() == QEvent::Enter
+90 -71
View File
@@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene_item_line.h" #include "editor/scene/scene_item_line.h"
#include "editor/scene/scene_item_sticker.h" #include "editor/scene/scene_item_sticker.h"
#include "editor/scene/scene_item_text.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/image/image_prepare.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "styles/style_editor.h" #include "styles/style_editor.h"
@@ -22,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QGraphicsView> #include <QGraphicsView>
#include <QTextCursor> #include <QTextCursor>
#include <QTextDocument> #include <QTextDocument>
#include <QTimer>
namespace Editor { namespace Editor {
namespace { namespace {
@@ -108,6 +107,13 @@ bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
return event->isAccepted() || (event->button() == Qt::RightButton); 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 { class TextEditProxy final : public QGraphicsTextItem {
public: public:
using QGraphicsTextItem::QGraphicsTextItem; using QGraphicsTextItem::QGraphicsTextItem;
@@ -141,7 +147,7 @@ private:
const auto cb = std::exchange(callback, nullptr); const auto cb = std::exchange(callback, nullptr);
onFinish = nullptr; onFinish = nullptr;
onCancel = nullptr; onCancel = nullptr;
QTimer::singleShot(0, cb); crl::on_main(cb);
} }
}; };
@@ -352,21 +358,30 @@ void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
_canvas->handleMouseMoveEvent(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); _canvas->applyBrush(color, size, tool);
for (auto *item : selectedItems()) {
if (item->type() == ItemText::Type) {
static_cast<ItemText*>(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; _textColor = color;
_textFontSize = fontSize; _textFontSize = fontSize;
_textStyle = style; _textStyle = style;
} }
void Scene::setTextColor(const QColor &color) {
_textColor = color;
if (_textEdit.proxy) {
_textEdit.proxy->setDefaultTextColor(color);
}
}
rpl::producer<QColor> Scene::textColorRequests() const {
return _textColorRequests.events();
}
void Scene::setBlurSource(Fn<QImage(QRect)> source) { void Scene::setBlurSource(Fn<QImage(QRect)> source) {
_blurSource = std::move(source); _blurSource = std::move(source);
} }
@@ -495,24 +510,19 @@ void Scene::restore(SaveState state) {
cancelDrawing(); cancelDrawing();
} }
void Scene::createTextAtCenter() { void Scene::setupTextProxy(
if (_textEdit.proxy) { QGraphicsTextItem *proxy,
return; const QColor &color,
} float64 fontSize) {
clearSelection();
cancelDrawing();
auto *proxy = new TextEditProxy();
proxy->setTextInteractionFlags(Qt::TextEditorInteraction); proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
proxy->setDefaultTextColor(_textColor); proxy->setDefaultTextColor(color);
auto *emojiDoc = new EmojiDocument(proxy); auto *emojiDoc = new EmojiDocument(proxy);
emojiDoc->setDocumentMargin(0); emojiDoc->setDocumentMargin(0);
proxy->setDocument(emojiDoc); proxy->setDocument(emojiDoc);
auto font = QFont(); auto font = QFont();
font.setPixelSize(int(_textFontSize)); font.setPixelSize(int(fontSize));
font.setWeight(QFont::DemiBold); font.setWeight(QFont::DemiBold);
proxy->setFont(font); proxy->setFont(font);
@@ -521,19 +531,33 @@ void Scene::createTextAtCenter() {
option.setAlignment(Qt::AlignCenter); option.setAlignment(Qt::AlignCenter);
emojiDoc->setDefaultTextOption(option); 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( const auto shortSide = std::min(
sceneRect().width(), sceneRect().width(),
sceneRect().height()); sceneRect().height());
const auto padding = int(_textFontSize * 0.4); const auto padding = int(_textFontSize * kPaddingFactor);
const auto maxTextWidth = int(shortSide * 0.8) - 2 * padding; const auto maxTextWidth = int(shortSide * kMaxWidthFactor) - 2 * padding;
const auto minTextWidth = int(shortSide * 0.16) - 2 * padding; const auto minTextWidth = int(shortSide * kMinWidthFactor) - 2 * padding;
const auto sceneCenter = sceneRect().center(); const auto sceneCenter = sceneRect().center();
const auto adjustWidth = [=] { const auto adjustWidth = [=] {
emojiDoc->setTextWidth(maxTextWidth); emojiDoc->setTextWidth(maxTextWidth);
const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp( const auto width = std::clamp(
ideal + 2, ideal + kIdealWidthExtra,
minTextWidth, minTextWidth,
maxTextWidth); maxTextWidth);
proxy->setTextWidth(width); proxy->setTextWidth(width);
@@ -553,14 +577,16 @@ void Scene::createTextAtCenter() {
views().first()->setFocus(); views().first()->setFocus();
} }
proxy->onFinish = crl::guard(this, [=] { const auto raw = static_cast<TextEditProxy*>(proxy);
raw->onFinish = crl::guard(this, [=] {
finishTextEditing(true); finishTextEditing(true);
}); });
proxy->onCancel = crl::guard(this, [=] { raw->onCancel = crl::guard(this, [=] {
finishTextEditing(false); finishTextEditing(false);
}); });
_textEdit = { .item = nullptr, .proxy = proxy }; _textEdit.item.reset();
_textColorRequests.fire_copy(_textColor);
} }
void Scene::startTextEditing(ItemText *item) { void Scene::startTextEditing(ItemText *item) {
@@ -573,40 +599,26 @@ void Scene::startTextEditing(ItemText *item) {
cancelDrawing(); cancelDrawing();
auto *proxy = new TextEditProxy(); _textEdit.proxy.reset(new TextEditProxy());
proxy->setTextInteractionFlags(Qt::TextEditorInteraction); const auto proxy = _textEdit.proxy.get();
proxy->setDefaultTextColor(item->color()); setupTextProxy(proxy, item->color(), item->fontSize());
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);
}
proxy->setPlainText(item->text()); proxy->setPlainText(item->text());
ReplaceEmoji(emojiDoc); ReplaceEmoji(proxy->document());
const auto emojiDoc = proxy->document();
const auto shortSide = std::min( const auto shortSide = std::min(
sceneRect().width(), sceneRect().width(),
sceneRect().height()); sceneRect().height());
const auto padding = int(item->fontSize() * 0.4); const auto padding = int(item->fontSize() * kPaddingFactor);
const auto maxTextWidth = int(shortSide * 0.8) - 2 * padding; const auto maxTextWidth = int(shortSide * kMaxWidthFactor) - 2 * padding;
const auto minTextWidth = int(shortSide * 0.16) - 2 * padding; const auto minTextWidth = int(shortSide * kMinWidthFactor) - 2 * padding;
const auto anchor = item->scenePos(); const auto anchor = item->scenePos();
const auto adjustWidth = [=] { const auto adjustWidth = [=] {
emojiDoc->setTextWidth(maxTextWidth); emojiDoc->setTextWidth(maxTextWidth);
const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp( const auto width = std::clamp(
ideal + 2, ideal + kIdealWidthExtra,
minTextWidth, minTextWidth,
maxTextWidth); maxTextWidth);
proxy->setTextWidth(width); proxy->setTextWidth(width);
@@ -623,7 +635,7 @@ void Scene::startTextEditing(ItemText *item) {
const auto scale = item->editScale(); const auto scale = item->editScale();
proxy->setRotation(item->rotation()); proxy->setRotation(item->rotation());
if (std::abs(scale - 1.) > 0.01) { if (std::abs(scale - 1.) > kScaleThreshold) {
proxy->setScale(scale); proxy->setScale(scale);
} }
@@ -637,14 +649,19 @@ void Scene::startTextEditing(ItemText *item) {
item->setVisible(false); item->setVisible(false);
proxy->onFinish = crl::guard(this, [=] { const auto raw = static_cast<TextEditProxy*>(proxy);
raw->onFinish = crl::guard(this, [=] {
finishTextEditing(true); finishTextEditing(true);
}); });
proxy->onCancel = crl::guard(this, [=] { raw->onCancel = crl::guard(this, [=] {
finishTextEditing(false); finishTextEditing(false);
}); });
_textEdit = { .item = item, .proxy = proxy }; const auto it = _itemsByPointer.find(item);
_textEdit.item = (it != end(_itemsByPointer))
? it->second
: std::weak_ptr<NumberedItem>();
_textColorRequests.fire_copy(item->color());
} }
void Scene::finishTextEditing(bool save) { void Scene::finishTextEditing(bool save) {
@@ -658,15 +675,19 @@ void Scene::finishTextEditing(bool save) {
const auto proxyRect = _textEdit.proxy->boundingRect(); const auto proxyRect = _textEdit.proxy->boundingRect();
const auto proxyCenter = _textEdit.proxy->pos() const auto proxyCenter = _textEdit.proxy->pos()
+ QPointF(proxyRect.width() / 2., proxyRect.height() / 2.); + QPointF(proxyRect.width() / 2., proxyRect.height() / 2.);
auto *existingItem = _textEdit.item; const auto lockedItem = _textEdit.item.lock();
auto *existingItem = lockedItem
? static_cast<ItemText*>(lockedItem.get())
: (ItemText*)(nullptr);
const auto proxy = static_cast<TextEditProxy*>(_textEdit.proxy); const auto raw = static_cast<TextEditProxy*>(_textEdit.proxy.get());
proxy->onFinish = nullptr; raw->onFinish = nullptr;
proxy->onCancel = nullptr; raw->onCancel = nullptr;
QGraphicsScene::removeItem(proxy); QGraphicsScene::removeItem(_textEdit.proxy.get());
delete proxy;
_textEdit.proxy = nullptr; _textEdit.proxy = nullptr;
_textEdit.item = nullptr; _textEdit.item.reset();
const auto defaultStyle = static_cast<TextStyle>(_textStyle);
if (!text.isEmpty()) { if (!text.isEmpty()) {
if (existingItem) { if (existingItem) {
@@ -678,7 +699,7 @@ void Scene::finishTextEditing(bool save) {
text, text,
_textFontSize, _textFontSize,
imageSize, imageSize,
TextStyle::Plain); defaultStyle);
const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.; const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.;
const auto handleInflate = int( const auto handleInflate = int(
std::ceil(st::photoEditorItemHandleSize / zoom)); std::ceil(st::photoEditorItemHandleSize / zoom));
@@ -697,7 +718,7 @@ void Scene::finishTextEditing(bool save) {
text, text,
_textColor, _textColor,
_textFontSize, _textFontSize,
TextStyle::Plain, defaultStyle,
imageSize, imageSize,
std::move(data)); std::move(data));
addItem(item); addItem(item);
@@ -713,17 +734,15 @@ void Scene::finishTextEditing(bool save) {
Scene::~Scene() { Scene::~Scene() {
if (_textEdit.proxy) { if (_textEdit.proxy) {
const auto proxy = static_cast<TextEditProxy*>(_textEdit.proxy); const auto raw = static_cast<TextEditProxy*>(
proxy->onFinish = nullptr; _textEdit.proxy.get());
proxy->onCancel = nullptr; raw->onFinish = nullptr;
QGraphicsScene::removeItem(proxy); raw->onCancel = nullptr;
delete proxy; QGraphicsScene::removeItem(_textEdit.proxy.get());
_textEdit.proxy = nullptr; _textEdit.proxy = nullptr;
} }
// Prevent destroying by scene of all items.
QGraphicsScene::removeItem(_canvas.get()); QGraphicsScene::removeItem(_canvas.get());
for (const auto &item : items()) { for (const auto &item : items()) {
// Scene loses ownership of an item.
QGraphicsScene::removeItem(item.get()); QGraphicsScene::removeItem(item.get());
} }
} }
+14 -6
View File
@@ -31,9 +31,9 @@ public:
Scene(const QRectF &rect); Scene(const QRectF &rect);
~Scene(); ~Scene();
void applyBrush(const QColor &color, float size, Brush::Tool tool); void applyBrush(const QColor &color, float64 size, Brush::Tool tool);
void setBlurSource(Fn<QImage(QRect)> source); void setBlurSource(Fn<QImage(QRect)> source);
void setTextDefaults(const QColor &color, float fontSize, int style); void setTextDefaults(const QColor &color, float64 fontSize, int style);
[[nodiscard]] std::vector<ItemPtr> items( [[nodiscard]] std::vector<ItemPtr> items(
Qt::SortOrder order = Qt::DescendingOrder) const; Qt::SortOrder order = Qt::DescendingOrder) const;
@@ -51,6 +51,9 @@ public:
void startTextEditing(ItemText *item); void startTextEditing(ItemText *item);
void createTextAtCenter(); void createTextAtCenter();
void setTextColor(const QColor &color);
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
[[nodiscard]] bool hasUndo() const; [[nodiscard]] bool hasUndo() const;
[[nodiscard]] bool hasRedo() const; [[nodiscard]] bool hasRedo() const;
@@ -69,6 +72,10 @@ protected:
private: private:
void removeIf(Fn<bool(const ItemPtr &)> proj); void removeIf(Fn<bool(const ItemPtr &)> proj);
void finishTextEditing(bool save); void finishTextEditing(bool save);
void setupTextProxy(
QGraphicsTextItem *proxy,
const QColor &color,
float64 fontSize);
const std::shared_ptr<ItemCanvas> _canvas; const std::shared_ptr<ItemCanvas> _canvas;
const std::shared_ptr<float64> _lastZ; const std::shared_ptr<float64> _lastZ;
@@ -82,15 +89,16 @@ private:
int _itemNumber = 0; int _itemNumber = 0;
QColor _textColor; QColor _textColor;
float _textFontSize = 0.f; float64 _textFontSize = 0.;
int _textStyle = 3; int _textStyle = 0;
struct { struct {
ItemText *item = nullptr; std::weak_ptr<NumberedItem> item;
QGraphicsTextItem *proxy = nullptr; base::unique_qptr<QGraphicsTextItem> proxy;
} _textEdit; } _textEdit;
rpl::event_stream<> _addsItem, _removesItem; rpl::event_stream<> _addsItem, _removesItem;
rpl::event_stream<QColor> _textColorRequests;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };
@@ -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 <QTextBlock>
#include <QTextCursor>
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
@@ -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 <QTextDocument>
namespace Editor {
class EmojiDocument final : public QTextDocument {
public:
explicit EmojiDocument(QObject *parent = nullptr);
QVariant loadResource(int type, const QUrl &name) override;
private:
std::map<QUrl, QVariant> _cache;
};
void ReplaceEmoji(QTextDocument *doc);
[[nodiscard]] QString RecoverTextFromDocument(QTextDocument *doc);
} // namespace Editor
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene_item_text.h" #include "editor/scene/scene_item_text.h"
#include "editor/scene/scene.h" #include "editor/scene/scene.h"
#include "editor/scene/scene_emoji_document.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/emoji_config.h" #include "ui/emoji_config.h"
#include "ui/painter.h" #include "ui/painter.h"
@@ -26,9 +27,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Editor { namespace Editor {
namespace { namespace {
constexpr auto kPaddingFactor = 0.4f; constexpr auto kPaddingFactor = 0.4;
constexpr auto kMaxWidthFactor = 0.8f; constexpr auto kMaxWidthFactor = 0.8;
constexpr auto kMinContentWidth = 20; 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 { struct LayoutMetrics {
int contentWidth = 0; int contentWidth = 0;
@@ -37,22 +46,22 @@ struct LayoutMetrics {
int textMaxWidth = 0; int textMaxWidth = 0;
}; };
QFont TextFont(float fontSize) { QFont TextFont(float64 fontSize) {
auto font = QFont(); auto font = QFont();
font.setPixelSize(std::max(int(fontSize), 1)); font.setPixelSize(std::max(int(fontSize), 1));
font.setWeight(QFont::DemiBold); font.setWeight(QFont::DemiBold);
return font; return font;
} }
float ComputeBrightness(const QColor &color) { float64 ComputeBrightness(const QColor &color) {
return (color.red() * 0.2126f return (color.red() * 0.2126
+ color.green() * 0.7152f + color.green() * 0.7152
+ color.blue() * 0.0722f) / 255.f; + color.blue() * 0.0722) / 255.;
} }
LayoutMetrics ComputeMetrics( LayoutMetrics ComputeMetrics(
const QString &text, const QString &text,
float fontSize, float64 fontSize,
const QSize &imageSize, const QSize &imageSize,
TextStyle style) { TextStyle style) {
const auto hasBackground = (style == TextStyle::Framed) const auto hasBackground = (style == TextStyle::Framed)
@@ -96,33 +105,33 @@ LayoutMetrics ComputeMetrics(
} }
struct LineRect { struct LineRect {
float left = 0; float64 left = 0;
float top = 0; float64 top = 0;
float right = 0; float64 right = 0;
float bottom = 0; float64 bottom = 0;
[[nodiscard]] float width() const { return right - left; } [[nodiscard]] float64 width() const { return right - left; }
}; };
QPainterPath BuildConnectedBackground( QPainterPath BuildConnectedBackground(
const QTextLayout &layout, const QTextLayout &layout,
int contentWidth, int contentWidth,
int padding, int padding,
float fontSize) { float64 fontSize) {
const auto linePadH = fontSize / 3.f; const auto linePadH = fontSize * kLinePadHFactor;
const auto linePadV = fontSize / 8.f; const auto linePadV = fontSize * kLinePadVFactor;
const auto cornerRadius = fontSize / 3.f; const auto cornerRadius = fontSize * kCornerRadiusFactor;
const auto mergeRadius = cornerRadius * 1.5f; const auto mergeRadius = cornerRadius * kMergeRadiusFactor;
const auto centerX = padding + contentWidth / 2.f; const auto centerX = padding + contentWidth / 2.;
auto rects = std::vector<LineRect>(); auto rects = std::vector<LineRect>();
for (auto i = 0; i < layout.lineCount(); ++i) { for (auto i = 0; i < layout.lineCount(); ++i) {
const auto line = layout.lineAt(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({ rects.push_back({
.left = centerX - hw, .left = centerX - hw,
.top = padding + float(line.y()) - linePadV, .top = padding + float64(line.y()) - linePadV,
.right = centerX + hw, .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) { if (traceback) {
for (auto j = i; j >= 1; --j) { 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( const auto v = std::min(
rects[j - 1].left, rects[j - 1].left,
rects[j].left); rects[j].left);
@@ -174,14 +184,14 @@ QPainterPath BuildConnectedBackground(
} }
} }
struct V { float x, y; }; struct V { float64 x, y; };
auto verts = std::vector<V>(); auto verts = std::vector<V>();
verts.push_back({ rects[0].left, rects[0].top }); verts.push_back({ rects[0].left, rects[0].top });
verts.push_back({ rects[0].right, rects[0].top }); verts.push_back({ rects[0].right, rects[0].top });
for (auto i = 1; i < int(rects.size()); ++i) { 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 - 1].right, rects[i].top });
verts.push_back({ rects[i].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 }); verts.push_back({ rects[last].left, rects[last].bottom });
for (auto i = last - 1; i >= 0; --i) { 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 + 1].left, rects[i + 1].top });
verts.push_back({ rects[i].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 dy2 = next.y - curr.y;
const auto len2 = std::sqrt(dx2 * dx2 + dy2 * dy2); 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) { if (i == 0) {
path.moveTo(curr.x, curr.y); path.moveTo(curr.x, curr.y);
} else { } else {
@@ -224,8 +234,8 @@ QPainterPath BuildConnectedBackground(
const auto r = std::min({ const auto r = std::min({
cornerRadius, cornerRadius,
len1 / 2.f, len1 / 2.,
len2 / 2.f, len2 / 2.,
}); });
const auto bx = curr.x - dx1 / len1 * r; const auto bx = curr.x - dx1 / len1 * r;
const auto by = curr.y - dy1 / len1 * r; const auto by = curr.y - dy1 / len1 * r;
@@ -245,126 +255,10 @@ QPainterPath BuildConnectedBackground(
} // namespace } // 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( ItemText::ItemText(
const QString &text, const QString &text,
const QColor &color, const QColor &color,
float fontSize, float64 fontSize,
TextStyle style, TextStyle style,
const QSize &imageSize, const QSize &imageSize,
ItemBase::Data data) ItemBase::Data data)
@@ -445,14 +339,14 @@ void ItemText::renderContent() {
switch (_textStyle) { switch (_textStyle) {
case TextStyle::Framed: case TextStyle::Framed:
bgColor = _color; bgColor = _color;
textColor = (brightness >= 0.721f) textColor = (brightness >= kBrightnessFramedThreshold)
? QColor(0, 0, 0) ? QColor(0, 0, 0)
: QColor(255, 255, 255); : QColor(255, 255, 255);
break; break;
case TextStyle::SemiTransparent: case TextStyle::SemiTransparent:
bgColor = (brightness >= 0.25f) bgColor = (brightness >= kBrightnessSemiTransparentThreshold)
? QColor(0, 0, 0, 0x99) ? QColor(0, 0, 0, kSemiTransparentAlpha)
: QColor(255, 255, 255, 0x99); : QColor(255, 255, 255, kSemiTransparentAlpha);
break; break;
case TextStyle::Plain: case TextStyle::Plain:
break; break;
@@ -496,14 +390,14 @@ void ItemText::renderContent() {
} }
} }
const auto lineShift = _fontSize / 7.f; const auto lineShift = _fontSize * kLineShiftFactor;
const auto lineCount = layout.lineCount(); const auto lineCount = layout.lineCount();
p.setPen(textColor); p.setPen(textColor);
for (auto i = 0; i < lineCount; ++i) { for (auto i = 0; i < lineCount; ++i) {
const auto line = layout.lineAt(i); const auto line = layout.lineAt(i);
const auto xOffset = const auto xOffset =
(m.contentWidth - line.naturalTextWidth()) / 2.; (m.contentWidth - line.naturalTextWidth()) / 2.;
const auto yShift = (i < lineCount - 1) ? -lineShift : 0.f; const auto yShift = (i < lineCount - 1) ? -lineShift : 0.;
line.draw( line.draw(
&p, &p,
QPointF(m.padding + xOffset, m.padding + yShift)); QPointF(m.padding + xOffset, m.padding + yShift));
@@ -519,7 +413,7 @@ void ItemText::renderContent() {
const auto line = layout.lineAt(i); const auto line = layout.lineAt(i);
const auto xOffset = const auto xOffset =
(m.contentWidth - line.naturalTextWidth()) / 2.; (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 lineStart = line.textStart();
const auto lineText = processedText.mid( const auto lineText = processedText.mid(
lineStart, lineStart,
@@ -567,7 +461,7 @@ void ItemText::renderContent() {
QSize ItemText::computeContentSize( QSize ItemText::computeContentSize(
const QString &text, const QString &text,
float fontSize, float64 fontSize,
const QSize &imageSize, const QSize &imageSize,
TextStyle style) { TextStyle style) {
if (text.isEmpty()) { if (text.isEmpty()) {
@@ -635,7 +529,7 @@ void ItemText::setColor(const QColor &color) {
update(); update();
} }
float ItemText::fontSize() const { float64 ItemText::fontSize() const {
return _fontSize; return _fontSize;
} }
@@ -685,9 +579,13 @@ void ItemText::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
action->setChecked(true); action->setChecked(true);
} }
}; };
add(u"Plain"_q, TextStyle::Plain); add(tr::lng_photo_editor_text_style_plain(tr::now), TextStyle::Plain);
add(u"Framed"_q, TextStyle::Framed); add(
add(u"Semi-Transparent"_q, TextStyle::SemiTransparent); 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(); _contextMenu->addSeparator();
@@ -10,8 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unique_qptr.h" #include "base/unique_qptr.h"
#include "editor/scene/scene_item_base.h" #include "editor/scene/scene_item_base.h"
#include <QTextDocument>
namespace Ui { namespace Ui {
class PopupMenu; class PopupMenu;
} // namespace Ui } // namespace Ui
@@ -24,18 +22,6 @@ enum class TextStyle : uchar {
Plain, Plain,
}; };
class EmojiDocument final : public QTextDocument {
public:
explicit EmojiDocument(QObject *parent = nullptr);
QVariant loadResource(int type, const QUrl &name) override;
private:
std::map<QUrl, QVariant> _cache;
};
void ReplaceEmoji(QTextDocument *doc);
[[nodiscard]] QString RecoverTextFromDocument(QTextDocument *doc);
class ItemText : public ItemBase { class ItemText : public ItemBase {
public: public:
enum { Type = ItemBase::Type + 2 }; enum { Type = ItemBase::Type + 2 };
@@ -43,7 +29,7 @@ public:
ItemText( ItemText(
const QString &text, const QString &text,
const QColor &color, const QColor &color,
float fontSize, float64 fontSize,
TextStyle style, TextStyle style,
const QSize &imageSize, const QSize &imageSize,
ItemBase::Data data); ItemBase::Data data);
@@ -60,7 +46,7 @@ public:
[[nodiscard]] const QColor &color() const; [[nodiscard]] const QColor &color() const;
void setColor(const QColor &color); void setColor(const QColor &color);
[[nodiscard]] float fontSize() const; [[nodiscard]] float64 fontSize() const;
[[nodiscard]] TextStyle textStyle() const; [[nodiscard]] TextStyle textStyle() const;
void setTextStyle(TextStyle style); void setTextStyle(TextStyle style);
@@ -69,7 +55,7 @@ public:
[[nodiscard]] static QSize computeContentSize( [[nodiscard]] static QSize computeContentSize(
const QString &text, const QString &text,
float fontSize, float64 fontSize,
const QSize &imageSize, const QSize &imageSize,
TextStyle style); TextStyle style);
@@ -88,7 +74,7 @@ private:
QString _text; QString _text;
QColor _color; QColor _color;
float _fontSize; float64 _fontSize;
TextStyle _textStyle = TextStyle::Plain; TextStyle _textStyle = TextStyle::Plain;
QSize _imageSize; QSize _imageSize;
QPixmap _pixmap; QPixmap _pixmap;
@@ -99,7 +85,7 @@ private:
NumberedItem::Status status = NumberedItem::Status::Normal; NumberedItem::Status status = NumberedItem::Status::Normal;
QString text; QString text;
QColor color; QColor color;
float fontSize = 0; float64 fontSize = 0.;
TextStyle textStyle = TextStyle::Plain; TextStyle textStyle = TextStyle::Plain;
}; };
SavedText _savedState, _keepedState; SavedText _savedState, _keepedState;
+2
View File
@@ -149,6 +149,8 @@ PRIVATE
editor/scene/scene_item_line.h editor/scene/scene_item_line.h
editor/scene/scene_item_text.cpp editor/scene/scene_item_text.cpp
editor/scene/scene_item_text.h 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.h
ui/boxes/about_cocoon_box.cpp ui/boxes/about_cocoon_box.cpp