init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
157
Telegram/lib_ui/ui/text/custom_emoji_helper.cpp
Normal file
157
Telegram/lib_ui/ui/text/custom_emoji_helper.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
|
||||
#include "ui/text/custom_emoji_instance.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString Prefix(int counter) {
|
||||
return u"helper%1:"_q.arg(counter);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString PaddingPostfix(QMargins padding) {
|
||||
if (padding.isNull()) {
|
||||
return QString();
|
||||
}
|
||||
return u":%1,%2,%3,%4"_q
|
||||
.arg(padding.left())
|
||||
.arg(padding.top())
|
||||
.arg(padding.right())
|
||||
.arg(padding.bottom());
|
||||
}
|
||||
|
||||
[[nodiscard]] QMargins PaddingFromPostfix(QStringView postfix) {
|
||||
const auto parts = postfix.split(u',');
|
||||
if (parts.size() != 4) {
|
||||
return {};
|
||||
}
|
||||
return QMargins(
|
||||
parts[0].toInt(),
|
||||
parts[1].toInt(),
|
||||
parts[2].toInt(),
|
||||
parts[3].toInt());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CustomEmojiHelper::CustomEmojiHelper() = default;
|
||||
|
||||
CustomEmojiHelper::CustomEmojiHelper(MarkedContext parent)
|
||||
: _parent(std::move(parent)) {
|
||||
}
|
||||
|
||||
QString CustomEmojiHelper::imageData(ImageEmoji emoji) {
|
||||
Expects(!emoji.image.isNull());
|
||||
|
||||
const auto data = ensureData();
|
||||
const auto result = data->prefix
|
||||
+ u"image%1"_q.arg(data->images.size())
|
||||
+ (emoji.margin.isNull() ? QString() : PaddingPostfix(emoji.margin))
|
||||
+ (emoji.textColor
|
||||
? (emoji.margin.isNull() ? u"::1"_q : u":1"_q)
|
||||
: QString());
|
||||
data->images.push_back(std::move(emoji.image));
|
||||
return result;
|
||||
}
|
||||
|
||||
TextWithEntities CustomEmojiHelper::image(ImageEmoji emoji){
|
||||
return SingleCustomEmoji(imageData(std::move(emoji)));
|
||||
}
|
||||
|
||||
QString CustomEmojiHelper::paletteDependentData(
|
||||
PaletteDependentEmoji emoji) {
|
||||
Expects(emoji.factory != nullptr);
|
||||
|
||||
const auto data = ensureData();
|
||||
const auto result = data->prefix
|
||||
+ u"factory%1"_q.arg(data->paletteDependent.size())
|
||||
+ (emoji.margin.isNull() ? QString() : PaddingPostfix(emoji.margin));
|
||||
data->paletteDependent.push_back(std::move(emoji.factory));
|
||||
return result;
|
||||
}
|
||||
|
||||
TextWithEntities CustomEmojiHelper::paletteDependent(
|
||||
PaletteDependentEmoji emoji) {
|
||||
return SingleCustomEmoji(paletteDependentData(std::move(emoji)));
|
||||
}
|
||||
|
||||
MarkedContext CustomEmojiHelper::context(Fn<void()> repaint) {
|
||||
auto result = _parent;
|
||||
if (repaint) {
|
||||
result.repaint = std::move(repaint);
|
||||
}
|
||||
if (!_data) {
|
||||
return result;
|
||||
}
|
||||
auto factory = [map = _data](
|
||||
QStringView data,
|
||||
const MarkedContext &context
|
||||
) -> std::unique_ptr<CustomEmoji> {
|
||||
if (!data.startsWith(map->prefix)) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto id = data.mid(map->prefix.size());
|
||||
const auto parts = id.split(':');
|
||||
if (parts.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto type = parts[0];
|
||||
const auto postfix = (parts.size() > 1) ? parts[1] : QStringView();
|
||||
const auto padding = PaddingFromPostfix(postfix);
|
||||
if (type.startsWith(u"image"_q)) {
|
||||
const auto index = type.mid(u"image"_q.size()).toInt();
|
||||
if (index >= 0 && index < map->images.size()) {
|
||||
return std::make_unique<Ui::CustomEmoji::Internal>(
|
||||
data.toString(),
|
||||
base::duplicate(map->images[index]),
|
||||
padding,
|
||||
(parts.size() > 2) && parts[2] == u"1"_q);
|
||||
}
|
||||
} else if (type.startsWith(u"factory"_q)) {
|
||||
const auto index = type.mid(u"factory"_q.size()).toInt();
|
||||
if (index >= 0 && index < map->paletteDependent.size()) {
|
||||
return std::make_unique<PaletteDependentCustomEmoji>(
|
||||
map->paletteDependent[index],
|
||||
data.toString(),
|
||||
padding);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
if (auto old = _parent.customEmojiFactory) {
|
||||
result.customEmojiFactory = [
|
||||
factory = std::move(factory),
|
||||
old = std::move(old)
|
||||
](
|
||||
QStringView data,
|
||||
const MarkedContext &context
|
||||
) -> std::unique_ptr<CustomEmoji> {
|
||||
if (auto result = factory(data, context)) {
|
||||
return result;
|
||||
}
|
||||
return old(data, context);
|
||||
};
|
||||
} else {
|
||||
result.customEmojiFactory = factory;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto CustomEmojiHelper::ensureData() -> not_null<Data*> {
|
||||
if (!_data) {
|
||||
static auto counter = std::atomic<int>();
|
||||
_data = std::make_shared<Data>();
|
||||
_data->prefix = Prefix(counter++);
|
||||
}
|
||||
return _data.get();
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
52
Telegram/lib_ui/ui/text/custom_emoji_helper.h
Normal file
52
Telegram/lib_ui/ui/text/custom_emoji_helper.h
Normal file
@@ -0,0 +1,52 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
struct ImageEmoji {
|
||||
QImage image;
|
||||
QMargins margin;
|
||||
bool textColor = true;
|
||||
};
|
||||
|
||||
struct PaletteDependentEmoji {
|
||||
Fn<QImage()> factory;
|
||||
QMargins margin;
|
||||
};
|
||||
|
||||
class CustomEmojiHelper final {
|
||||
public:
|
||||
CustomEmojiHelper();
|
||||
CustomEmojiHelper(MarkedContext parent);
|
||||
|
||||
[[nodiscard]] QString imageData(ImageEmoji emoji);
|
||||
[[nodiscard]] TextWithEntities image(ImageEmoji emoji);
|
||||
|
||||
[[nodiscard]] QString paletteDependentData(PaletteDependentEmoji emoji);
|
||||
[[nodiscard]] TextWithEntities paletteDependent(
|
||||
PaletteDependentEmoji emoji);
|
||||
|
||||
[[nodiscard]] MarkedContext context(Fn<void()> repaint = nullptr);
|
||||
|
||||
private:
|
||||
struct Data {
|
||||
QString prefix;
|
||||
std::vector<QImage> images;
|
||||
std::vector<Fn<QImage()>> paletteDependent;
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<Data*> ensureData();
|
||||
|
||||
MarkedContext _parent;
|
||||
std::shared_ptr<Data> _data;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
989
Telegram/lib_ui/ui/text/custom_emoji_instance.cpp
Normal file
989
Telegram/lib_ui/ui/text/custom_emoji_instance.cpp
Normal file
@@ -0,0 +1,989 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/custom_emoji_instance.h"
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/frame_generator.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <crl/crl_async.h>
|
||||
#include <lz4.h>
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace Ui::CustomEmoji {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxFrames = 180;
|
||||
constexpr auto kCacheVersion = 1;
|
||||
constexpr auto kPreloadFrames = 3;
|
||||
|
||||
struct CacheHeader {
|
||||
int version = 0;
|
||||
int size = 0;
|
||||
int frames = 0;
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
void PaintScaledImage(
|
||||
QPainter &p,
|
||||
const QRect &target,
|
||||
const Cache::Frame &frame,
|
||||
const Context &context) {
|
||||
static QImage PaintCache;
|
||||
const auto cache = context.internal.colorized ? &PaintCache : nullptr;
|
||||
auto q = std::optional<QPainter>();
|
||||
if (cache) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
if (cache->width() < target.width() * ratio
|
||||
|| cache->height() < target.height() * ratio) {
|
||||
*cache = QImage(
|
||||
std::max(cache->width(), target.width() * ratio),
|
||||
std::max(cache->height(), target.height() * ratio),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
cache->setDevicePixelRatio(ratio);
|
||||
}
|
||||
q.emplace(cache);
|
||||
q->setCompositionMode(QPainter::CompositionMode_Source);
|
||||
if (context.scaled) {
|
||||
q->fillRect(QRect(QPoint(), target.size()), Qt::transparent);
|
||||
}
|
||||
q->translate(-target.topLeft());
|
||||
}
|
||||
const auto to = q ? &*q : &p;
|
||||
if (context.scaled) {
|
||||
const auto sx = anim::interpolate(
|
||||
target.width() / 2,
|
||||
0,
|
||||
context.scale);
|
||||
const auto sy = (target.height() == target.width())
|
||||
? sx
|
||||
: anim::interpolate(target.height() / 2, 0, context.scale);
|
||||
const auto scaled = target.marginsRemoved({ sx, sy, sx, sy });
|
||||
if (frame.source.isNull()) {
|
||||
to->drawImage(scaled, *frame.image);
|
||||
} else {
|
||||
to->drawImage(scaled, *frame.image, frame.source);
|
||||
}
|
||||
} else if (frame.source.isNull()) {
|
||||
to->drawImage(target, *frame.image);
|
||||
} else {
|
||||
to->drawImage(target, *frame.image, frame.source);
|
||||
}
|
||||
if (q) {
|
||||
q.reset();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto source = QRect(QPoint(), target.size() * ratio);
|
||||
const auto &color = context.textColor;
|
||||
style::colorizeImage(*cache, color, cache, source, {}, true);
|
||||
p.drawImage(target, *cache, source);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QColor PreviewColorFromTextColor(QColor color) {
|
||||
color.setAlpha((color.alpha() + 1) / 8);
|
||||
return color;
|
||||
}
|
||||
|
||||
Preview::Preview(QPainterPath path, float64 scale)
|
||||
: _data(ScaledPath{ std::move(path), scale }) {
|
||||
}
|
||||
|
||||
Preview::Preview(QImage image, bool exact)
|
||||
: _data(Image{ .data = std::move(image), .exact = exact }) {
|
||||
}
|
||||
|
||||
void Preview::paint(QPainter &p, const Context &context) {
|
||||
if (const auto path = std::get_if<ScaledPath>(&_data)) {
|
||||
paintPath(p, context, *path);
|
||||
} else if (const auto image = std::get_if<Image>(&_data)) {
|
||||
const auto &data = image->data;
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto rect = QRect(context.position, data.size() / factor);
|
||||
PaintScaledImage(p, rect, { .image = &data }, context);
|
||||
}
|
||||
}
|
||||
|
||||
bool Preview::isImage() const {
|
||||
return v::is<Image>(_data);
|
||||
}
|
||||
|
||||
bool Preview::isExactImage() const {
|
||||
if (const auto image = std::get_if<Image>(&_data)) {
|
||||
return image->exact;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage Preview::image() const {
|
||||
if (const auto image = std::get_if<Image>(&_data)) {
|
||||
return image->data;
|
||||
}
|
||||
return QImage();
|
||||
}
|
||||
|
||||
void Preview::paintPath(
|
||||
QPainter &p,
|
||||
const Context &context,
|
||||
const ScaledPath &path) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(PreviewColorFromTextColor(context.textColor));
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto scale = path.scale;
|
||||
const auto required = (scale != 1.) || context.scaled;
|
||||
if (required) {
|
||||
p.save();
|
||||
}
|
||||
p.translate(context.position);
|
||||
if (required) {
|
||||
p.scale(scale, scale);
|
||||
const auto center = QPoint(
|
||||
context.size.width() / 2,
|
||||
context.size.height() / 2);
|
||||
if (context.scaled) {
|
||||
p.translate(center);
|
||||
p.scale(context.scale, context.scale);
|
||||
p.translate(-center);
|
||||
}
|
||||
}
|
||||
p.drawPath(path.path);
|
||||
if (required) {
|
||||
p.restore();
|
||||
} else {
|
||||
p.translate(-context.position);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::Cache(int size) : _size(size) {
|
||||
}
|
||||
|
||||
std::optional<Cache> Cache::FromSerialized(
|
||||
const QByteArray &serialized,
|
||||
int requestedSize) {
|
||||
if (serialized.size() <= sizeof(CacheHeader)) {
|
||||
return {};
|
||||
}
|
||||
auto header = CacheHeader();
|
||||
memcpy(&header, serialized.data(), sizeof(header));
|
||||
const auto size = header.size;
|
||||
if (size != requestedSize
|
||||
|| header.frames <= 0
|
||||
|| header.frames >= kMaxFrames
|
||||
|| header.length <= 0
|
||||
|| header.length > (size * size * header.frames * sizeof(int32))
|
||||
|| (serialized.size() != sizeof(CacheHeader)
|
||||
+ header.length
|
||||
+ (header.frames * sizeof(Cache(0)._durations[0])))) {
|
||||
return {};
|
||||
}
|
||||
const auto rows = (header.frames + kPerRow - 1) / kPerRow;
|
||||
const auto columns = std::min(header.frames, kPerRow);
|
||||
auto durations = std::vector<uint16>(header.frames, 0);
|
||||
auto full = QImage(
|
||||
columns * size,
|
||||
rows * size,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
Assert(full.bytesPerLine() == full.width() * sizeof(int32));
|
||||
|
||||
const auto decompressed = LZ4_decompress_safe(
|
||||
serialized.data() + sizeof(CacheHeader),
|
||||
reinterpret_cast<char*>(full.bits()),
|
||||
header.length,
|
||||
full.bytesPerLine() * full.height());
|
||||
if (decompressed <= 0) {
|
||||
return {};
|
||||
}
|
||||
memcpy(
|
||||
durations.data(),
|
||||
serialized.data() + sizeof(CacheHeader) + header.length,
|
||||
header.frames * sizeof(durations[0]));
|
||||
|
||||
auto result = Cache(size);
|
||||
result._finished = true;
|
||||
result._full = std::move(full);
|
||||
result._frames = header.frames;
|
||||
result._durations = std::move(durations);
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray Cache::serialize() {
|
||||
Expects(_finished);
|
||||
Expects(_durations.size() == _frames);
|
||||
Expects(_full.bytesPerLine() == sizeof(int32) * _full.width());
|
||||
|
||||
auto header = CacheHeader{
|
||||
.version = kCacheVersion,
|
||||
.size = _size,
|
||||
.frames = _frames,
|
||||
};
|
||||
const auto input = _full.width() * _full.height() * sizeof(int32);
|
||||
const auto max = sizeof(CacheHeader)
|
||||
+ LZ4_compressBound(input)
|
||||
+ (_frames * sizeof(_durations[0]));
|
||||
auto result = QByteArray(max, Qt::Uninitialized);
|
||||
header.length = LZ4_compress_default(
|
||||
reinterpret_cast<const char*>(_full.constBits()),
|
||||
result.data() + sizeof(CacheHeader),
|
||||
input,
|
||||
result.size() - sizeof(CacheHeader));
|
||||
Assert(header.length > 0);
|
||||
memcpy(result.data(), &header, sizeof(CacheHeader));
|
||||
memcpy(
|
||||
result.data() + sizeof(CacheHeader) + header.length,
|
||||
_durations.data(),
|
||||
_frames * sizeof(_durations[0]));
|
||||
result.resize(sizeof(CacheHeader)
|
||||
+ header.length
|
||||
+ _frames * sizeof(_durations[0]));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int Cache::frames() const {
|
||||
return _frames;
|
||||
}
|
||||
|
||||
bool Cache::readyInDefaultState() const {
|
||||
return (_frames > 0) && !_frame;
|
||||
}
|
||||
|
||||
Cache::Frame Cache::frame(int index) const {
|
||||
Expects(index < _frames);
|
||||
|
||||
const auto row = index / kPerRow;
|
||||
const auto inrow = index % kPerRow;
|
||||
if (_finished) {
|
||||
return { &_full, { inrow * _size, row * _size, _size, _size } };
|
||||
}
|
||||
return { &_images[row], { 0, inrow * _size, _size, _size } };
|
||||
}
|
||||
|
||||
int Cache::size() const {
|
||||
return _size;
|
||||
}
|
||||
|
||||
Preview Cache::makePreview() const {
|
||||
Expects(_frames > 0);
|
||||
|
||||
const auto first = frame(0);
|
||||
return { first.image->copy(first.source), true };
|
||||
}
|
||||
|
||||
void Cache::reserve(int frames) {
|
||||
Expects(!_finished);
|
||||
|
||||
const auto rows = (frames + kPerRow - 1) / kPerRow;
|
||||
if (const auto add = rows - int(_images.size()); add > 0) {
|
||||
_images.resize(rows);
|
||||
for (auto e = end(_images), i = e - add; i != e; ++i) {
|
||||
(*i) = QImage(
|
||||
_size,
|
||||
_size * kPerRow,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
}
|
||||
_durations.reserve(frames);
|
||||
}
|
||||
|
||||
int Cache::frameRowByteSize() const {
|
||||
return _size * 4;
|
||||
}
|
||||
|
||||
int Cache::frameByteSize() const {
|
||||
return _size * frameRowByteSize();
|
||||
}
|
||||
|
||||
void Cache::add(crl::time duration, const QImage &frame) {
|
||||
Expects(!_finished);
|
||||
Expects(frame.size() == QSize(_size, _size));
|
||||
Expects(frame.format() == QImage::Format_ARGB32_Premultiplied);
|
||||
|
||||
const auto row = (_frames / kPerRow);
|
||||
const auto inrow = (_frames % kPerRow);
|
||||
const auto rows = row + 1;
|
||||
while (_images.size() < rows) {
|
||||
_images.emplace_back();
|
||||
_images.back() = QImage(
|
||||
_size,
|
||||
_size * kPerRow,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
const auto srcPerLine = frame.bytesPerLine();
|
||||
const auto dstPerLine = _images[row].bytesPerLine();
|
||||
const auto perLine = std::min(srcPerLine, dstPerLine);
|
||||
auto dst = _images[row].bits() + inrow * _size * dstPerLine;
|
||||
auto src = frame.constBits();
|
||||
for (auto y = 0; y != _size; ++y) {
|
||||
memcpy(dst, src, perLine);
|
||||
dst += dstPerLine;
|
||||
src += srcPerLine;
|
||||
}
|
||||
++_frames;
|
||||
_durations.push_back(std::clamp(
|
||||
duration,
|
||||
crl::time(0),
|
||||
crl::time(std::numeric_limits<uint16>::max())));
|
||||
}
|
||||
|
||||
void Cache::finish() {
|
||||
_finished = true;
|
||||
if (_frame == _frames) {
|
||||
_frame = 0;
|
||||
}
|
||||
const auto rows = (_frames + kPerRow - 1) / kPerRow;
|
||||
const auto columns = std::min(_frames, kPerRow);
|
||||
const auto zero = (rows * columns) - _frames;
|
||||
_full = QImage(
|
||||
columns * _size,
|
||||
rows * _size,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
auto dstData = _full.bits();
|
||||
const auto perLine = _size * 4;
|
||||
const auto dstPerLine = _full.bytesPerLine();
|
||||
for (auto y = 0; y != rows; ++y) {
|
||||
auto &row = _images[y];
|
||||
auto src = row.bits();
|
||||
const auto srcPerLine = row.bytesPerLine();
|
||||
const auto till = columns - ((y + 1 == rows) ? zero : 0);
|
||||
for (auto x = 0; x != till; ++x) {
|
||||
auto dst = dstData + y * dstPerLine * _size + x * perLine;
|
||||
for (auto line = 0; line != _size; ++line) {
|
||||
memcpy(dst, src, perLine);
|
||||
src += srcPerLine;
|
||||
dst += dstPerLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const auto perLine = zero * _size) {
|
||||
auto dst = dstData
|
||||
+ (rows - 1) * dstPerLine * _size
|
||||
+ (columns - zero) * _size * 4;
|
||||
for (auto left = 0; left != _size; ++left) {
|
||||
memset(dst, 0, perLine);
|
||||
dst += dstPerLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PaintFrameResult Cache::paintCurrentFrame(
|
||||
QPainter &p,
|
||||
const Context &context) {
|
||||
if (!_frames) {
|
||||
return {};
|
||||
}
|
||||
const auto first = context.internal.forceFirstFrame;
|
||||
auto last = context.internal.forceLastFrame;
|
||||
if (!first && !last) {
|
||||
const auto now = context.paused ? 0 : context.now;
|
||||
const auto finishes = now ? currentFrameFinishes() : 0;
|
||||
if (finishes && now >= finishes) {
|
||||
++_frame;
|
||||
if (_finished && _frame == _frames) {
|
||||
_frame = 0;
|
||||
if (context.internal.overrideFirstWithLastFrame) {
|
||||
last = true;
|
||||
}
|
||||
}
|
||||
_shown = now;
|
||||
} else if (!_shown) {
|
||||
_shown = now;
|
||||
}
|
||||
}
|
||||
const auto index = first
|
||||
? 0
|
||||
: last
|
||||
? (_frames - 1)
|
||||
: std::min(_frame, _frames - 1);
|
||||
const auto info = frame(index);
|
||||
const auto size = _size / style::DevicePixelRatio();
|
||||
const auto rect = QRect(context.position, QSize(size, size));
|
||||
PaintScaledImage(p, rect, info, context);
|
||||
const auto next = first ? 0 : currentFrameFinishes();
|
||||
return {
|
||||
.painted = true,
|
||||
.next = next,
|
||||
.duration = next ? (next - _shown) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
int Cache::currentFrame() const {
|
||||
return _frame;
|
||||
}
|
||||
|
||||
crl::time Cache::currentFrameFinishes() const {
|
||||
if (!_shown || _frame >= _durations.size()) {
|
||||
return 0;
|
||||
} else if (const auto duration = _durations[_frame]) {
|
||||
return _shown + duration;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Cached::Cached(
|
||||
const QString &entityData,
|
||||
Fn<std::unique_ptr<Loader>()> unloader,
|
||||
Cache cache)
|
||||
: _unloader(std::move(unloader))
|
||||
, _cache(std::move(cache))
|
||||
, _entityData(entityData) {
|
||||
}
|
||||
|
||||
QString Cached::entityData() const {
|
||||
return _entityData;
|
||||
}
|
||||
|
||||
PaintFrameResult Cached::paint(QPainter &p, const Context &context) {
|
||||
return _cache.paintCurrentFrame(p, context);
|
||||
}
|
||||
|
||||
bool Cached::inDefaultState() const {
|
||||
return _cache.readyInDefaultState();
|
||||
}
|
||||
|
||||
Preview Cached::makePreview() const {
|
||||
return _cache.makePreview();
|
||||
}
|
||||
|
||||
Loading Cached::unload() {
|
||||
return Loading(_unloader(), makePreview());
|
||||
}
|
||||
|
||||
Renderer::Renderer(RendererDescriptor &&descriptor)
|
||||
: _cache(descriptor.size)
|
||||
, _put(std::move(descriptor.put))
|
||||
, _loader(std::move(descriptor.loader)) {
|
||||
Expects(_loader != nullptr);
|
||||
|
||||
const auto size = _cache.size();
|
||||
const auto guard = base::make_weak(this);
|
||||
crl::async([=, factory = std::move(descriptor.generator)]() mutable {
|
||||
auto generator = factory();
|
||||
auto rendered = generator->renderNext(
|
||||
QImage(),
|
||||
QSize(size, size),
|
||||
Qt::KeepAspectRatio);
|
||||
if (rendered.image.isNull()) {
|
||||
return;
|
||||
}
|
||||
crl::on_main(guard, [
|
||||
=,
|
||||
frame = std::move(rendered),
|
||||
generator = std::move(generator)
|
||||
]() mutable {
|
||||
frameReady(
|
||||
std::move(generator),
|
||||
frame.duration,
|
||||
std::move(frame.image));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Renderer::~Renderer() = default;
|
||||
|
||||
void Renderer::frameReady(
|
||||
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||
crl::time duration,
|
||||
QImage frame) {
|
||||
if (frame.isNull()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (const auto count = generator->count()) {
|
||||
if (!_cache.frames()) {
|
||||
_cache.reserve(std::max(count, kMaxFrames));
|
||||
}
|
||||
}
|
||||
const auto current = _cache.currentFrame();
|
||||
const auto total = _cache.frames();
|
||||
const auto explicitRepaint = (current == total);
|
||||
_cache.add(duration, frame);
|
||||
if (explicitRepaint && _repaint) {
|
||||
_repaint();
|
||||
}
|
||||
if (!duration || total + 1 >= kMaxFrames) {
|
||||
finish();
|
||||
} else if (current + kPreloadFrames > total) {
|
||||
renderNext(std::move(generator), std::move(frame));
|
||||
} else {
|
||||
_generator = std::move(generator);
|
||||
_storage = std::move(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::renderNext(
|
||||
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||
QImage storage) {
|
||||
const auto size = _cache.size();
|
||||
const auto guard = base::make_weak(this);
|
||||
crl::async([
|
||||
=,
|
||||
storage = std::move(storage),
|
||||
generator = std::move(generator)
|
||||
]() mutable {
|
||||
auto rendered = generator->renderNext(
|
||||
std::move(storage),
|
||||
QSize(size, size),
|
||||
Qt::KeepAspectRatio);
|
||||
crl::on_main(guard, [
|
||||
=,
|
||||
frame = std::move(rendered),
|
||||
generator = std::move(generator)
|
||||
]() mutable {
|
||||
frameReady(
|
||||
std::move(generator),
|
||||
frame.duration,
|
||||
std::move(frame.image));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Renderer::finish() {
|
||||
_finished = true;
|
||||
_cache.finish();
|
||||
if (_put) {
|
||||
_put(_cache.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
PaintFrameResult Renderer::paint(QPainter &p, const Context &context) {
|
||||
const auto result = _cache.paintCurrentFrame(p, context);
|
||||
if (_generator
|
||||
&& (!result.painted
|
||||
|| _cache.currentFrame() + kPreloadFrames >= _cache.frames())) {
|
||||
renderNext(std::move(_generator), std::move(_storage));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<Cached> Renderer::ready(const QString &entityData) {
|
||||
return _finished
|
||||
? Cached{ entityData, std::move(_loader), std::move(_cache) }
|
||||
: std::optional<Cached>();
|
||||
}
|
||||
|
||||
std::unique_ptr<Loader> Renderer::cancel() {
|
||||
return _loader();
|
||||
}
|
||||
|
||||
bool Renderer::canMakePreview() const {
|
||||
return _cache.frames() > 0;
|
||||
}
|
||||
|
||||
Preview Renderer::makePreview() const {
|
||||
return _cache.makePreview();
|
||||
}
|
||||
|
||||
bool Renderer::readyInDefaultState() const {
|
||||
return _cache.readyInDefaultState();
|
||||
}
|
||||
|
||||
void Renderer::setRepaintCallback(Fn<void()> repaint) {
|
||||
_repaint = std::move(repaint);
|
||||
}
|
||||
|
||||
Cache Renderer::takeCache() {
|
||||
return std::move(_cache);
|
||||
}
|
||||
|
||||
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
|
||||
: _loader(std::move(loader))
|
||||
, _preview(std::move(preview)) {
|
||||
}
|
||||
|
||||
QString Loading::entityData() const {
|
||||
return _loader->entityData();
|
||||
}
|
||||
|
||||
void Loading::load(Fn<void(Loader::LoadResult)> done) {
|
||||
_loader->load(crl::guard(this, [this, done = std::move(done)](
|
||||
Loader::LoadResult result) mutable {
|
||||
if (const auto caching = std::get_if<Caching>(&result)) {
|
||||
caching->preview = _preview
|
||||
? std::move(_preview)
|
||||
: _loader->preview();
|
||||
}
|
||||
done(std::move(result));
|
||||
}));
|
||||
}
|
||||
|
||||
bool Loading::loading() const {
|
||||
return _loader->loading();
|
||||
}
|
||||
|
||||
void Loading::paint(QPainter &p, const Context &context) {
|
||||
if (!_preview) {
|
||||
if (auto preview = _loader->preview()) {
|
||||
_preview = std::move(preview);
|
||||
}
|
||||
}
|
||||
_preview.paint(p, context);
|
||||
}
|
||||
|
||||
bool Loading::hasImagePreview() const {
|
||||
return _preview.isImage();
|
||||
}
|
||||
|
||||
Preview Loading::imagePreview() const {
|
||||
return _preview.isImage() ? _preview : Preview();
|
||||
}
|
||||
|
||||
void Loading::updatePreview(Preview preview) {
|
||||
if (!_preview.isImage() && preview.isImage()) {
|
||||
_preview = std::move(preview);
|
||||
} else if (!_preview) {
|
||||
if (auto loaderPreview = _loader->preview()) {
|
||||
_preview = std::move(loaderPreview);
|
||||
} else if (preview) {
|
||||
_preview = std::move(preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Loading::cancel() {
|
||||
_loader->cancel();
|
||||
invalidate_weak_ptrs(this);
|
||||
}
|
||||
|
||||
Instance::Instance(
|
||||
Loading loading,
|
||||
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater)
|
||||
: _state(std::move(loading))
|
||||
, _repaintLater(std::move(repaintLater)) {
|
||||
}
|
||||
|
||||
QString Instance::entityData() const {
|
||||
return v::match(_state, [](const Loading &state) {
|
||||
return state.entityData();
|
||||
}, [](const Caching &state) {
|
||||
return state.entityData;
|
||||
}, [](const Cached &state) {
|
||||
return state.entityData();
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::paint(QPainter &p, const Context &context) {
|
||||
context.internal.colorized = _colored;
|
||||
|
||||
v::match(_state, [&](Loading &state) {
|
||||
state.paint(p, context);
|
||||
load(state);
|
||||
}, [&](Caching &state) {
|
||||
auto result = state.renderer->paint(p, context);
|
||||
if (!result.painted) {
|
||||
state.preview.paint(p, context);
|
||||
} else {
|
||||
if (!state.preview.isExactImage()) {
|
||||
state.preview = state.renderer->makePreview();
|
||||
}
|
||||
if (result.next > context.now) {
|
||||
_repaintLater(this, { result.next, result.duration });
|
||||
}
|
||||
}
|
||||
if (auto cached = state.renderer->ready(state.entityData)) {
|
||||
_state = std::move(*cached);
|
||||
}
|
||||
}, [&](Cached &state) {
|
||||
const auto result = state.paint(p, context);
|
||||
if (result.next > context.now) {
|
||||
_repaintLater(this, { result.next, result.duration });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool Instance::ready() {
|
||||
return v::match(_state, [&](Loading &state) {
|
||||
if (state.hasImagePreview()) {
|
||||
return true;
|
||||
}
|
||||
if (!_usage.empty()) {
|
||||
load(state);
|
||||
}
|
||||
return false;
|
||||
}, [](Caching &state) {
|
||||
return state.renderer->canMakePreview();
|
||||
}, [](Cached &state) {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool Instance::readyInDefaultState() {
|
||||
return v::match(_state, [&](Loading &state) {
|
||||
if (state.hasImagePreview()) {
|
||||
return true;
|
||||
}
|
||||
load(state);
|
||||
return false;
|
||||
}, [](Caching &state) {
|
||||
return state.renderer->readyInDefaultState();
|
||||
}, [](Cached &state) {
|
||||
return state.inDefaultState();
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::load(Loading &state) {
|
||||
state.load([=](Loader::LoadResult result) {
|
||||
if (auto caching = std::get_if<Caching>(&result)) {
|
||||
caching->renderer->setRepaintCallback([=] { repaint(); });
|
||||
_state = std::move(*caching);
|
||||
} else if (auto cached = std::get_if<Cached>(&result)) {
|
||||
_state = std::move(*cached);
|
||||
repaint();
|
||||
} else {
|
||||
Unexpected("Value in Loader::LoadResult.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool Instance::hasImagePreview() const {
|
||||
return v::match(_state, [](const Loading &state) {
|
||||
return state.hasImagePreview();
|
||||
}, [](const Caching &state) {
|
||||
return state.preview.isImage();
|
||||
}, [](const Cached &state) {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Preview Instance::imagePreview() const {
|
||||
return v::match(_state, [](const Loading &state) {
|
||||
return state.imagePreview();
|
||||
}, [](const Caching &state) {
|
||||
return state.preview.isImage() ? state.preview : Preview();
|
||||
}, [](const Cached &state) {
|
||||
return state.makePreview();
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::updatePreview(Preview preview) {
|
||||
v::match(_state, [&](Loading &state) {
|
||||
state.updatePreview(std::move(preview));
|
||||
}, [&](Caching &state) {
|
||||
if ((!state.preview.isImage() && preview.isImage())
|
||||
|| (!state.preview && preview)) {
|
||||
state.preview = std::move(preview);
|
||||
}
|
||||
}, [](const Cached &) {});
|
||||
}
|
||||
|
||||
void Instance::setColored() {
|
||||
if (!_colored) {
|
||||
_colored = true;
|
||||
if (ready()) {
|
||||
_repaintLater(this, { .when = crl::now() + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::repaint() {
|
||||
for (const auto &object : _usage) {
|
||||
object->repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::incrementUsage(not_null<Object*> object) {
|
||||
_usage.emplace(object);
|
||||
}
|
||||
|
||||
void Instance::decrementUsage(not_null<Object*> object) {
|
||||
_usage.remove(object);
|
||||
if (!_usage.empty()) {
|
||||
return;
|
||||
}
|
||||
v::match(_state, [](Loading &state) {
|
||||
state.cancel();
|
||||
}, [&](Caching &state) {
|
||||
_state = Loading{
|
||||
state.renderer->cancel(),
|
||||
std::move(state.preview),
|
||||
};
|
||||
}, [&](Cached &state) {
|
||||
_state = state.unload();
|
||||
});
|
||||
_repaintLater(this, RepaintRequest());
|
||||
}
|
||||
|
||||
Object::Object(not_null<Instance*> instance, Fn<void()> repaint)
|
||||
: _instance(instance)
|
||||
, _repaint(std::move(repaint)) {
|
||||
}
|
||||
|
||||
Object::~Object() {
|
||||
unload();
|
||||
}
|
||||
|
||||
int Object::width() {
|
||||
return st::emojiSize + 2 * st::emojiPadding;
|
||||
}
|
||||
|
||||
QString Object::entityData() {
|
||||
return _instance->entityData();
|
||||
}
|
||||
|
||||
void Object::paint(QPainter &p, const Context &context) {
|
||||
if (!_using) {
|
||||
_using = true;
|
||||
_instance->incrementUsage(this);
|
||||
}
|
||||
_instance->paint(p, context);
|
||||
}
|
||||
|
||||
void Object::unload() {
|
||||
if (_using) {
|
||||
_using = false;
|
||||
_instance->decrementUsage(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool Object::ready() {
|
||||
if (!_using) {
|
||||
_using = true;
|
||||
_instance->incrementUsage(this);
|
||||
}
|
||||
return _instance->ready();
|
||||
}
|
||||
|
||||
bool Object::readyInDefaultState() {
|
||||
if (!_using) {
|
||||
_using = true;
|
||||
_instance->incrementUsage(this);
|
||||
}
|
||||
return _instance->readyInDefaultState();
|
||||
}
|
||||
|
||||
void Object::repaint() {
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
Internal::Internal(
|
||||
QString entityData,
|
||||
QImage image,
|
||||
QMargins padding,
|
||||
bool colored)
|
||||
: _entityData(std::move(entityData))
|
||||
, _image(std::move(image))
|
||||
, _padding(padding)
|
||||
, _colored(colored) {
|
||||
}
|
||||
|
||||
int Internal::width() {
|
||||
return _padding.left()
|
||||
+ (_image.width() / _image.devicePixelRatio())
|
||||
+ _padding.right();
|
||||
}
|
||||
|
||||
QString Internal::entityData() {
|
||||
return _entityData;
|
||||
}
|
||||
|
||||
void Internal::paint(QPainter &p, const Context &context) {
|
||||
context.internal.colorized = _colored;
|
||||
|
||||
const auto size = _image.size() / style::DevicePixelRatio();
|
||||
const auto rect = QRect(
|
||||
context.position + QPoint(_padding.left(), _padding.top()),
|
||||
size);
|
||||
PaintScaledImage(p, rect, { &_image }, context);
|
||||
}
|
||||
|
||||
void Internal::unload() {
|
||||
}
|
||||
|
||||
bool Internal::ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Internal::readyInDefaultState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
DynamicImageEmoji::DynamicImageEmoji(
|
||||
QString entityData,
|
||||
std::shared_ptr<DynamicImage> image,
|
||||
Fn<void()> repaint,
|
||||
QMargins padding,
|
||||
int size)
|
||||
: _entityData(entityData)
|
||||
, _image(std::move(image))
|
||||
, _repaint(std::move(repaint))
|
||||
, _padding(padding)
|
||||
, _size(size) {
|
||||
}
|
||||
|
||||
int DynamicImageEmoji::width() {
|
||||
return _padding.left() + _size + _padding.right();
|
||||
}
|
||||
|
||||
QString DynamicImageEmoji::entityData() {
|
||||
return _entityData;
|
||||
}
|
||||
|
||||
void DynamicImageEmoji::paint(QPainter &p, const Context &context) {
|
||||
if (!_subscribed) {
|
||||
_subscribed = true;
|
||||
_image->subscribeToUpdates(_repaint);
|
||||
}
|
||||
|
||||
auto image = _image->image(_size);
|
||||
const auto size = image.size() / image.devicePixelRatio();
|
||||
const auto rect = QRect(
|
||||
context.position + QPoint(_padding.left(), _padding.top()),
|
||||
size);
|
||||
context.internal.colorized = false;
|
||||
PaintScaledImage(p, rect, { &image }, context);
|
||||
}
|
||||
|
||||
void DynamicImageEmoji::unload() {
|
||||
if (_subscribed) {
|
||||
_subscribed = false;
|
||||
_image->subscribeToUpdates(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool DynamicImageEmoji::ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DynamicImageEmoji::readyInDefaultState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PaintIconEmoji(
|
||||
QPainter &p,
|
||||
const Context &context,
|
||||
not_null<const style::IconEmoji*> emoji,
|
||||
IconEmojiFrameCache &cache) {
|
||||
context.internal.colorized = !emoji->useIconColor;
|
||||
|
||||
const auto size = emoji->icon.size();
|
||||
const auto rect = QRect(context.position
|
||||
+ QPoint(emoji->padding.left(), emoji->padding.top()), size);
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto full = size * ratio;
|
||||
const auto invalid = (cache.frame.size() != full);
|
||||
if (invalid
|
||||
|| (emoji->useIconColor
|
||||
&& cache.paletteVersion != style::PaletteVersion())) {
|
||||
cache.paletteVersion = style::PaletteVersion();
|
||||
if (invalid) {
|
||||
cache.frame = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
cache.frame.setDevicePixelRatio(ratio);
|
||||
cache.frame.fill(Qt::transparent);
|
||||
auto q = QPainter(&cache.frame);
|
||||
emoji->icon.paint(q, 0, 0, size.width());
|
||||
}
|
||||
PaintScaledImage(p, rect, { &cache.frame }, context);
|
||||
}
|
||||
|
||||
} // namespace Ui::CustomEmoji
|
||||
340
Telegram/lib_ui/ui/text/custom_emoji_instance.h
Normal file
340
Telegram/lib_ui/ui/text/custom_emoji_instance.h
Normal file
@@ -0,0 +1,340 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/bytes.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <QtGui/QPainterPath>
|
||||
|
||||
class QColor;
|
||||
class QPainter;
|
||||
|
||||
namespace style {
|
||||
struct IconEmoji;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class FrameGenerator;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::CustomEmoji {
|
||||
|
||||
using Context = Ui::Text::CustomEmoji::Context;
|
||||
|
||||
[[nodiscard]] QColor PreviewColorFromTextColor(QColor color);
|
||||
|
||||
class Preview final {
|
||||
public:
|
||||
Preview() = default;
|
||||
Preview(QImage image, bool exact);
|
||||
Preview(QPainterPath path, float64 scale);
|
||||
|
||||
void paint(QPainter &p, const Context &context);
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] bool isExactImage() const;
|
||||
[[nodiscard]] QImage image() const;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return !v::is_null(_data);
|
||||
}
|
||||
|
||||
private:
|
||||
struct ScaledPath {
|
||||
QPainterPath path;
|
||||
float64 scale = 1.;
|
||||
};
|
||||
struct Image {
|
||||
QImage data;
|
||||
bool exact = false;
|
||||
};
|
||||
|
||||
void paintPath(
|
||||
QPainter &p,
|
||||
const Context &context,
|
||||
const ScaledPath &path);
|
||||
|
||||
std::variant<v::null_t, ScaledPath, Image> _data;
|
||||
|
||||
};
|
||||
|
||||
struct PaintFrameResult {
|
||||
bool painted = false;
|
||||
crl::time next = 0;
|
||||
crl::time duration = 0;
|
||||
};
|
||||
|
||||
class Cache final {
|
||||
public:
|
||||
Cache(int size);
|
||||
|
||||
struct Frame {
|
||||
not_null<const QImage*> image;
|
||||
QRect source;
|
||||
};
|
||||
|
||||
[[nodiscard]] static std::optional<Cache> FromSerialized(
|
||||
const QByteArray &serialized,
|
||||
int requestedSize);
|
||||
[[nodiscard]] QByteArray serialize();
|
||||
|
||||
[[nodiscard]] int size() const;
|
||||
[[nodiscard]] int frames() const;
|
||||
[[nodiscard]] bool readyInDefaultState() const;
|
||||
[[nodiscard]] Frame frame(int index) const;
|
||||
void reserve(int frames);
|
||||
void add(crl::time duration, const QImage &frame);
|
||||
void finish();
|
||||
|
||||
[[nodiscard]] Preview makePreview() const;
|
||||
|
||||
PaintFrameResult paintCurrentFrame(QPainter &p, const Context &context);
|
||||
[[nodiscard]] int currentFrame() const;
|
||||
|
||||
private:
|
||||
static constexpr auto kPerRow = 16;
|
||||
|
||||
[[nodiscard]] int frameRowByteSize() const;
|
||||
[[nodiscard]] int frameByteSize() const;
|
||||
[[nodiscard]] crl::time currentFrameFinishes() const;
|
||||
|
||||
std::vector<QImage> _images;
|
||||
std::vector<uint16> _durations;
|
||||
QImage _full;
|
||||
crl::time _shown = 0;
|
||||
int _frame = 0;
|
||||
int _size = 0;
|
||||
int _frames = 0;
|
||||
bool _finished = false;
|
||||
|
||||
};
|
||||
|
||||
class Loader;
|
||||
class Loading;
|
||||
|
||||
class Cached final {
|
||||
public:
|
||||
Cached(
|
||||
const QString &entityData,
|
||||
Fn<std::unique_ptr<Loader>()> unloader,
|
||||
Cache cache);
|
||||
|
||||
[[nodiscard]] QString entityData() const;
|
||||
[[nodiscard]] Preview makePreview() const;
|
||||
PaintFrameResult paint(QPainter &p, const Context &context);
|
||||
[[nodiscard]] bool inDefaultState() const;
|
||||
[[nodiscard]] Loading unload();
|
||||
|
||||
private:
|
||||
Fn<std::unique_ptr<Loader>()> _unloader;
|
||||
Cache _cache;
|
||||
QString _entityData;
|
||||
|
||||
};
|
||||
|
||||
struct RendererDescriptor {
|
||||
Fn<std::unique_ptr<Ui::FrameGenerator>()> generator;
|
||||
Fn<void(QByteArray)> put;
|
||||
Fn<std::unique_ptr<Loader>()> loader;
|
||||
int size = 0;
|
||||
};
|
||||
|
||||
class Renderer final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Renderer(RendererDescriptor &&descriptor);
|
||||
virtual ~Renderer();
|
||||
|
||||
PaintFrameResult paint(QPainter &p, const Context &context);
|
||||
[[nodiscard]] std::optional<Cached> ready(const QString &entityData);
|
||||
[[nodiscard]] std::unique_ptr<Loader> cancel();
|
||||
|
||||
[[nodiscard]] bool canMakePreview() const;
|
||||
[[nodiscard]] Preview makePreview() const;
|
||||
[[nodiscard]] bool readyInDefaultState() const;
|
||||
|
||||
void setRepaintCallback(Fn<void()> repaint);
|
||||
[[nodiscard]] Cache takeCache();
|
||||
|
||||
private:
|
||||
void frameReady(
|
||||
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||
crl::time duration,
|
||||
QImage frame);
|
||||
void renderNext(
|
||||
std::unique_ptr<Ui::FrameGenerator> generator,
|
||||
QImage storage);
|
||||
void finish();
|
||||
|
||||
Cache _cache;
|
||||
std::unique_ptr<Ui::FrameGenerator> _generator;
|
||||
QImage _storage;
|
||||
Fn<void(QByteArray)> _put;
|
||||
Fn<void()> _repaint;
|
||||
Fn<std::unique_ptr<Loader>()> _loader;
|
||||
bool _finished = false;
|
||||
|
||||
};
|
||||
|
||||
struct Caching {
|
||||
std::unique_ptr<Renderer> renderer;
|
||||
QString entityData;
|
||||
Preview preview;
|
||||
};
|
||||
|
||||
class Loader {
|
||||
public:
|
||||
using LoadResult = std::variant<Caching, Cached>;
|
||||
[[nodiscard]] virtual QString entityData() = 0;
|
||||
virtual void load(Fn<void(LoadResult)> loaded) = 0;
|
||||
[[nodiscard]] virtual bool loading() = 0;
|
||||
virtual void cancel() = 0;
|
||||
[[nodiscard]] virtual Preview preview() = 0;
|
||||
virtual ~Loader() = default;
|
||||
};
|
||||
|
||||
class Loading final : public base::has_weak_ptr {
|
||||
public:
|
||||
Loading(std::unique_ptr<Loader> loader, Preview preview);
|
||||
|
||||
[[nodiscard]] QString entityData() const;
|
||||
|
||||
void load(Fn<void(Loader::LoadResult)> done);
|
||||
[[nodiscard]] bool loading() const;
|
||||
void paint(QPainter &p, const Context &context);
|
||||
[[nodiscard]] bool hasImagePreview() const;
|
||||
[[nodiscard]] Preview imagePreview() const;
|
||||
void updatePreview(Preview preview);
|
||||
void cancel();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Loader> _loader;
|
||||
Preview _preview;
|
||||
|
||||
};
|
||||
|
||||
struct RepaintRequest {
|
||||
crl::time when = 0;
|
||||
crl::time duration = 0;
|
||||
};
|
||||
|
||||
class Object;
|
||||
class Instance final : public base::has_weak_ptr {
|
||||
public:
|
||||
Instance(
|
||||
Loading loading,
|
||||
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater);
|
||||
Instance(const Instance&) = delete;
|
||||
Instance &operator=(const Instance&) = delete;
|
||||
|
||||
[[nodiscard]] QString entityData() const;
|
||||
void paint(QPainter &p, const Context &context);
|
||||
[[nodiscard]] bool ready();
|
||||
[[nodiscard]] bool readyInDefaultState();
|
||||
[[nodiscard]] bool hasImagePreview() const;
|
||||
[[nodiscard]] Preview imagePreview() const;
|
||||
void updatePreview(Preview preview);
|
||||
void setColored();
|
||||
|
||||
void incrementUsage(not_null<Object*> object);
|
||||
void decrementUsage(not_null<Object*> object);
|
||||
|
||||
void repaint();
|
||||
|
||||
private:
|
||||
void load(Loading &state);
|
||||
|
||||
std::variant<Loading, Caching, Cached> _state;
|
||||
base::flat_set<not_null<Object*>> _usage;
|
||||
Fn<void(not_null<Instance*> that, RepaintRequest)> _repaintLater;
|
||||
bool _colored = false;
|
||||
|
||||
};
|
||||
|
||||
class Object final : public Ui::Text::CustomEmoji {
|
||||
public:
|
||||
Object(not_null<Instance*> instance, Fn<void()> repaint);
|
||||
~Object();
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
void repaint();
|
||||
|
||||
private:
|
||||
const not_null<Instance*> _instance;
|
||||
Fn<void()> _repaint;
|
||||
bool _using = false;
|
||||
|
||||
};
|
||||
|
||||
class Internal final : public Text::CustomEmoji {
|
||||
public:
|
||||
Internal(
|
||||
QString entityData,
|
||||
QImage image,
|
||||
QMargins padding = {},
|
||||
bool colored = false);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const QString _entityData;
|
||||
const QImage _image;
|
||||
const QMargins _padding;
|
||||
const bool _colored = false;
|
||||
|
||||
};
|
||||
|
||||
class DynamicImageEmoji final : public Ui::Text::CustomEmoji {
|
||||
public:
|
||||
DynamicImageEmoji(
|
||||
QString entityData,
|
||||
std::shared_ptr<DynamicImage> image,
|
||||
Fn<void()> repaint,
|
||||
QMargins padding,
|
||||
int size);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const QString _entityData;
|
||||
const std::shared_ptr<DynamicImage> _image;
|
||||
const Fn<void()> _repaint;
|
||||
const QMargins _padding;
|
||||
const int _size = 0;
|
||||
bool _subscribed = false;
|
||||
|
||||
};
|
||||
|
||||
struct IconEmojiFrameCache {
|
||||
QImage frame;
|
||||
int paletteVersion = 0;
|
||||
};
|
||||
void PaintIconEmoji(
|
||||
QPainter &p,
|
||||
const Context &context,
|
||||
not_null<const style::IconEmoji*> emoji,
|
||||
IconEmojiFrameCache &cache);
|
||||
|
||||
} // namespace Ui::CustomEmoji
|
||||
53
Telegram/lib_ui/ui/text/custom_emoji_text_badge.cpp
Normal file
53
Telegram/lib_ui/ui/text/custom_emoji_text_badge.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/custom_emoji_text_badge.h"
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
[[nodiscard]] PaletteDependentEmoji CustomEmojiTextBadge(
|
||||
const QString &text,
|
||||
const style::RoundButton &st,
|
||||
const style::margins &margin) {
|
||||
return { .factory = [=, &st] {
|
||||
auto string = Ui::Text::String(st.style, text.toUpper());
|
||||
const auto size = QSize(string.maxWidth(), string.minHeight());
|
||||
const auto full = QSize(
|
||||
(st.width < 0) ? (size.width() - st.width) : st.width,
|
||||
st.height);
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
|
||||
auto result = QImage(
|
||||
full * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(Qt::transparent);
|
||||
|
||||
auto p = QPainter(&result);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st.textBg);
|
||||
|
||||
const auto r = st.radius;
|
||||
p.drawRoundedRect(0, 0, full.width(), full.height(), r, r);
|
||||
|
||||
const auto x = (full.width() - size.width()) / 2;
|
||||
p.setPen(st.textFg);
|
||||
string.draw(p, {
|
||||
.position = { x, st.textTop },
|
||||
.availableWidth = size.width(),
|
||||
});
|
||||
|
||||
p.end();
|
||||
return result;
|
||||
}, .margin = margin };
|
||||
}
|
||||
|
||||
}
|
||||
28
Telegram/lib_ui/ui/text/custom_emoji_text_badge.h
Normal file
28
Telegram/lib_ui/ui/text/custom_emoji_text_badge.h
Normal file
@@ -0,0 +1,28 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/style/style_core_types.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
|
||||
namespace style {
|
||||
struct RoundButton;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::RoundButton &customEmojiTextBadge;
|
||||
extern const style::margins &customEmojiTextBadgeMargin;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
[[nodiscard]] PaletteDependentEmoji CustomEmojiTextBadge(
|
||||
const QString &text,
|
||||
const style::RoundButton &st = st::customEmojiTextBadge,
|
||||
const style::margins &margin = st::customEmojiTextBadgeMargin);
|
||||
|
||||
} // namespace Ui::Text
|
||||
1985
Telegram/lib_ui/ui/text/text.cpp
Normal file
1985
Telegram/lib_ui/ui/text/text.cpp
Normal file
File diff suppressed because it is too large
Load Diff
541
Telegram/lib_ui/ui/text/text.h
Normal file
541
Telegram/lib_ui/ui/text/text.h
Normal file
@@ -0,0 +1,541 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "base/flags.h"
|
||||
#include "ui/style/style_core_types.h"
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
#include <any>
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace anim {
|
||||
enum class type : uchar;
|
||||
} // namespace anim
|
||||
|
||||
namespace style {
|
||||
struct TextStyle;
|
||||
struct TextPalette;
|
||||
struct QuoteStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class SpoilerMessCached;
|
||||
|
||||
extern const QString kQEllipsis;
|
||||
|
||||
inline constexpr auto kQFixedMax = (INT_MAX / 256);
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
struct TextParseOptions {
|
||||
int32 flags;
|
||||
int32 maxw;
|
||||
int32 maxh;
|
||||
Qt::LayoutDirection dir;
|
||||
};
|
||||
extern const TextParseOptions kDefaultTextOptions;
|
||||
extern const TextParseOptions kMarkupTextOptions;
|
||||
extern const TextParseOptions kPlainTextOptions;
|
||||
|
||||
enum class TextSelectType {
|
||||
Letters = 0x01,
|
||||
Words = 0x02,
|
||||
Paragraphs = 0x03,
|
||||
};
|
||||
|
||||
struct TextSelection {
|
||||
constexpr TextSelection() = default;
|
||||
constexpr TextSelection(uint16 from, uint16 to) : from(from), to(to) {
|
||||
}
|
||||
constexpr bool empty() const {
|
||||
return from == to;
|
||||
}
|
||||
uint16 from = 0;
|
||||
uint16 to = 0;
|
||||
};
|
||||
|
||||
inline bool operator==(TextSelection a, TextSelection b) {
|
||||
return a.from == b.from && a.to == b.to;
|
||||
}
|
||||
|
||||
inline bool operator!=(TextSelection a, TextSelection b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
static constexpr TextSelection AllTextSelection = { 0, 0xFFFF };
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
class CustomEmoji;
|
||||
class AbstractBlock;
|
||||
class Block;
|
||||
class Word;
|
||||
struct IsolatedEmoji;
|
||||
struct OnlyCustomEmoji;
|
||||
struct SpoilerData;
|
||||
struct QuoteDetails;
|
||||
struct QuotesData;
|
||||
struct ExtendedData;
|
||||
struct MarkedContext;
|
||||
|
||||
using CustomEmojiFactory = Fn<std::unique_ptr<CustomEmoji>(
|
||||
QStringView,
|
||||
const MarkedContext &)>;
|
||||
|
||||
struct MarkedContext {
|
||||
Fn<void()> repaint;
|
||||
CustomEmojiFactory customEmojiFactory;
|
||||
std::any other;
|
||||
};
|
||||
|
||||
struct Modification {
|
||||
int position = 0;
|
||||
uint16 skipped = 0;
|
||||
bool added = false;
|
||||
};
|
||||
|
||||
struct StateRequest {
|
||||
enum class Flag {
|
||||
BreakEverywhere = (1 << 0),
|
||||
LookupSymbol = (1 << 1),
|
||||
LookupLink = (1 << 2),
|
||||
LookupCustomTooltip = (1 << 3),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; };
|
||||
|
||||
StateRequest() {
|
||||
}
|
||||
|
||||
style::align align = style::al_left;
|
||||
Flags flags = Flag::LookupLink;
|
||||
};
|
||||
|
||||
struct StateResult {
|
||||
ClickHandlerPtr link;
|
||||
bool uponSymbol = false;
|
||||
bool afterSymbol = false;
|
||||
uint16 symbol = 0;
|
||||
};
|
||||
|
||||
struct StateRequestElided : StateRequest {
|
||||
StateRequestElided() {
|
||||
}
|
||||
StateRequestElided(const StateRequest &other) : StateRequest(other) {
|
||||
}
|
||||
int lines = 1;
|
||||
int removeFromEnd = 0;
|
||||
};
|
||||
|
||||
class SpoilerMessCache {
|
||||
public:
|
||||
explicit SpoilerMessCache(int capacity);
|
||||
~SpoilerMessCache();
|
||||
|
||||
[[nodiscard]] not_null<SpoilerMessCached*> lookup(QColor color);
|
||||
void reset();
|
||||
|
||||
private:
|
||||
struct Entry;
|
||||
|
||||
std::vector<Entry> _cache;
|
||||
const int _capacity = 0;
|
||||
|
||||
};
|
||||
|
||||
struct SpecialColor {
|
||||
const QPen *pen = nullptr;
|
||||
const QPen *penSelected = nullptr;
|
||||
};
|
||||
|
||||
struct LineGeometry {
|
||||
int left = 0;
|
||||
int width = 0;
|
||||
bool elided = false;
|
||||
};
|
||||
struct GeometryDescriptor {
|
||||
Fn<LineGeometry(int line)> layout;
|
||||
bool breakEverywhere = false;
|
||||
bool *outElided = nullptr;
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<SpoilerMessCache*> DefaultSpoilerCache();
|
||||
|
||||
[[nodiscard]] GeometryDescriptor SimpleGeometry(
|
||||
int availableWidth,
|
||||
int elisionLines,
|
||||
int elisionRemoveFromEnd,
|
||||
bool elisionBreakEverywhere);
|
||||
|
||||
constexpr auto kMaxQuoteOutlines = 3;
|
||||
|
||||
struct QuotePaintCache {
|
||||
QImage corners;
|
||||
QImage outline;
|
||||
QImage expand;
|
||||
QImage collapse;
|
||||
mutable QImage bottomCorner;
|
||||
mutable QImage bottomRounding;
|
||||
mutable QImage collapsedLine;
|
||||
|
||||
std::array<QColor, kMaxQuoteOutlines> outlinesCached;
|
||||
QColor headerCached;
|
||||
QColor bgCached;
|
||||
QColor iconCached;
|
||||
|
||||
std::array<QColor, kMaxQuoteOutlines> outlines;
|
||||
QColor header;
|
||||
QColor bg;
|
||||
QColor icon;
|
||||
};
|
||||
|
||||
void ValidateQuotePaintCache(
|
||||
QuotePaintCache &cache,
|
||||
const style::QuoteStyle &st);
|
||||
|
||||
struct SkipBlockPaintParts {
|
||||
uint32 skippedTop : 29 = 0;
|
||||
uint32 skipBottom : 1 = 0;
|
||||
uint32 expandIcon : 1 = 0;
|
||||
uint32 collapseIcon : 1 = 0;
|
||||
};
|
||||
void FillQuotePaint(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const QuotePaintCache &cache,
|
||||
const style::QuoteStyle &st,
|
||||
SkipBlockPaintParts parts = {});
|
||||
|
||||
struct HighlightInfoRequest {
|
||||
TextSelection range;
|
||||
QRect interpolateTo;
|
||||
float64 interpolateProgress = 0.;
|
||||
QPainterPath *outPath = nullptr;
|
||||
};
|
||||
|
||||
struct PaintContext {
|
||||
QPoint position;
|
||||
int outerWidth = 0; // For automatic RTL Ui inversion.
|
||||
int availableWidth = 0;
|
||||
GeometryDescriptor geometry; // By default is SimpleGeometry.
|
||||
style::align align = style::al_left;
|
||||
QRect clip;
|
||||
|
||||
const style::TextPalette *palette = nullptr;
|
||||
QuotePaintCache *pre = nullptr;
|
||||
QuotePaintCache *blockquote = nullptr;
|
||||
std::span<SpecialColor> colors;
|
||||
SpoilerMessCache *spoiler = nullptr;
|
||||
crl::time now = 0;
|
||||
bool paused = false;
|
||||
bool pausedEmoji = false;
|
||||
bool pausedSpoiler = false;
|
||||
|
||||
bool fullWidthSelection = true;
|
||||
TextSelection selection;
|
||||
|
||||
HighlightInfoRequest *highlight = nullptr;
|
||||
|
||||
int elisionHeight = 0;
|
||||
int elisionLines = 0;
|
||||
int elisionRemoveFromEnd = 0;
|
||||
bool elisionBreakEverywhere = false;
|
||||
// Elision middle works only with elisionLines = 1 and is very limited.
|
||||
bool elisionMiddle = false;
|
||||
bool useFullWidth = false; // !(width = min(availableWidth, maxWidth()))
|
||||
};
|
||||
|
||||
class String {
|
||||
public:
|
||||
String(int minResizeWidth = kQFixedMax);
|
||||
String(
|
||||
const style::TextStyle &st,
|
||||
const QString &text,
|
||||
const TextParseOptions &options = kDefaultTextOptions,
|
||||
int minResizeWidth = kQFixedMax);
|
||||
String(
|
||||
const style::TextStyle &st,
|
||||
const TextWithEntities &textWithEntities,
|
||||
const TextParseOptions &options = kMarkupTextOptions,
|
||||
int minResizeWidth = kQFixedMax,
|
||||
const MarkedContext &context = {});
|
||||
String(String &&other);
|
||||
String &operator=(String &&other);
|
||||
~String();
|
||||
|
||||
[[nodiscard]] int countWidth(
|
||||
int width,
|
||||
bool breakEverywhere = false) const;
|
||||
[[nodiscard]] int countHeight(
|
||||
int width,
|
||||
bool breakEverywhere = false) const;
|
||||
|
||||
struct LineWidthsOptions {
|
||||
bool breakEverywhere = false;
|
||||
int reserve = 0;
|
||||
};
|
||||
[[nodiscard]] std::vector<int> countLineWidths(int width) const;
|
||||
[[nodiscard]] std::vector<int> countLineWidths(
|
||||
int width,
|
||||
LineWidthsOptions options) const;
|
||||
|
||||
struct DimensionsResult {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<int> lineWidths;
|
||||
};
|
||||
struct DimensionsRequest {
|
||||
bool breakEverywhere = false;
|
||||
bool lineWidths = false;
|
||||
int reserve = 0;
|
||||
};
|
||||
[[nodiscard]] DimensionsResult countDimensions(
|
||||
GeometryDescriptor geometry) const;
|
||||
[[nodiscard]] DimensionsResult countDimensions(
|
||||
GeometryDescriptor geometry,
|
||||
DimensionsRequest request) const;
|
||||
|
||||
void setText(
|
||||
const style::TextStyle &st,
|
||||
const QString &text,
|
||||
const TextParseOptions &options = kDefaultTextOptions);
|
||||
void setMarkedText(
|
||||
const style::TextStyle &st,
|
||||
const TextWithEntities &textWithEntities,
|
||||
const TextParseOptions &options = kMarkupTextOptions,
|
||||
const MarkedContext &context = {});
|
||||
|
||||
[[nodiscard]] bool hasLinks() const;
|
||||
void setLink(uint16 index, const ClickHandlerPtr &lnk);
|
||||
|
||||
[[nodiscard]] bool hasSpoilers() const;
|
||||
void setSpoilerRevealed(bool revealed, anim::type animated);
|
||||
void setSpoilerLinkFilter(Fn<bool(const ClickContext&)> filter);
|
||||
|
||||
[[nodiscard]] bool hasCollapsedBlockquots() const;
|
||||
[[nodiscard]] bool blockquoteCollapsed(int index) const;
|
||||
[[nodiscard]] bool blockquoteExpanded(int index) const;
|
||||
void setBlockquoteExpanded(int index, bool expanded);
|
||||
void setBlockquoteExpandCallback(
|
||||
Fn<void(int index, bool expanded)> callback);
|
||||
|
||||
[[nodiscard]] bool hasSkipBlock() const;
|
||||
bool updateSkipBlock(int width, int height);
|
||||
bool removeSkipBlock();
|
||||
|
||||
[[nodiscard]] int maxWidth() const {
|
||||
return _maxWidth;
|
||||
}
|
||||
[[nodiscard]] int minHeight() const {
|
||||
return _minHeight;
|
||||
}
|
||||
[[nodiscard]] int countMaxMonospaceWidth() const;
|
||||
|
||||
void draw(QPainter &p, const PaintContext &context) const;
|
||||
[[nodiscard]] StateResult getState(
|
||||
QPoint point,
|
||||
GeometryDescriptor geometry,
|
||||
StateRequest request = StateRequest()) const;
|
||||
|
||||
void draw(Painter &p, int32 left, int32 top, int32 width, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) const;
|
||||
void drawElided(Painter &p, int32 left, int32 top, int32 width, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const;
|
||||
void drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }) const;
|
||||
void drawLeftElided(Painter &p, int32 left, int32 top, int32 width, int32 outerw, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const;
|
||||
void drawRight(Painter &p, int32 right, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }) const;
|
||||
void drawRightElided(Painter &p, int32 right, int32 top, int32 width, int32 outerw, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const;
|
||||
|
||||
[[nodiscard]] StateResult getState(QPoint point, int width, StateRequest request = StateRequest()) const;
|
||||
[[nodiscard]] StateResult getStateLeft(QPoint point, int width, int outerw, StateRequest request = StateRequest()) const;
|
||||
[[nodiscard]] StateResult getStateElided(QPoint point, int width, StateRequestElided request = StateRequestElided()) const;
|
||||
[[nodiscard]] StateResult getStateElidedLeft(QPoint point, int width, int outerw, StateRequestElided request = StateRequestElided()) const;
|
||||
|
||||
[[nodiscard]] TextSelection adjustSelection(TextSelection selection, TextSelectType selectType) const;
|
||||
[[nodiscard]] bool isFullSelection(TextSelection selection) const {
|
||||
return (selection.from == 0) && (selection.to >= _text.size());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isEmpty() const;
|
||||
[[nodiscard]] bool isNull() const {
|
||||
return !_st;
|
||||
}
|
||||
[[nodiscard]] int length() const {
|
||||
return _text.size();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString toString(
|
||||
TextSelection selection = AllTextSelection) const;
|
||||
[[nodiscard]] TextWithEntities toTextWithEntities(
|
||||
TextSelection selection = AllTextSelection) const;
|
||||
[[nodiscard]] TextForMimeData toTextForMimeData(
|
||||
TextSelection selection = AllTextSelection) const;
|
||||
|
||||
[[nodiscard]] bool hasPersistentAnimation() const;
|
||||
void unloadPersistentAnimation();
|
||||
|
||||
[[nodiscard]] bool isIsolatedEmoji() const;
|
||||
[[nodiscard]] IsolatedEmoji toIsolatedEmoji() const;
|
||||
|
||||
[[nodiscard]] bool isOnlyCustomEmoji() const;
|
||||
[[nodiscard]] OnlyCustomEmoji toOnlyCustomEmoji() const;
|
||||
|
||||
[[nodiscard]] bool hasNotEmojiAndSpaces() const;
|
||||
[[nodiscard]] const std::vector<Modification> &modifications() const;
|
||||
|
||||
[[nodiscard]] const style::TextStyle *style() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
[[nodiscard]] int lineHeight() const;
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
class ExtendedWrap : public std::unique_ptr<ExtendedData> {
|
||||
public:
|
||||
ExtendedWrap() noexcept;
|
||||
ExtendedWrap(ExtendedWrap &&other) noexcept;
|
||||
ExtendedWrap &operator=(ExtendedWrap &&other) noexcept;
|
||||
~ExtendedWrap();
|
||||
|
||||
ExtendedWrap(
|
||||
std::unique_ptr<ExtendedData> &&other) noexcept;
|
||||
ExtendedWrap &operator=(
|
||||
std::unique_ptr<ExtendedData> &&other) noexcept;
|
||||
|
||||
private:
|
||||
void adjustFrom(const ExtendedWrap *other);
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<ExtendedData*> ensureExtended();
|
||||
[[nodiscard]] not_null<QuotesData*> ensureQuotes();
|
||||
|
||||
[[nodiscard]] uint16 blockPosition(
|
||||
std::vector<Block>::const_iterator i,
|
||||
int fullLengthOverride = -1) const;
|
||||
[[nodiscard]] uint16 blockEnd(
|
||||
std::vector<Block>::const_iterator i,
|
||||
int fullLengthOverride = -1) const;
|
||||
[[nodiscard]] uint16 blockLength(
|
||||
std::vector<Block>::const_iterator i,
|
||||
int fullLengthOverride = -1) const;
|
||||
|
||||
[[nodiscard]] QuoteDetails *quoteByIndex(int index) const;
|
||||
[[nodiscard]] const style::QuoteStyle "eStyle(
|
||||
not_null<QuoteDetails*> quote) const;
|
||||
[[nodiscard]] QMargins quotePadding(QuoteDetails *quote) const;
|
||||
[[nodiscard]] int quoteMinWidth(QuoteDetails *quote) const;
|
||||
[[nodiscard]] const QString "eHeaderText(QuoteDetails *quote) const;
|
||||
|
||||
// Returns -1 in case there is no limit.
|
||||
[[nodiscard]] int quoteLinesLimit(QuoteDetails *quote) const;
|
||||
|
||||
// Block must be either nullptr or a pointer to a NewlineBlock.
|
||||
[[nodiscard]] int quoteIndex(const AbstractBlock *block) const;
|
||||
|
||||
// Template method for originalText(), originalTextWithEntities().
|
||||
template <
|
||||
typename AppendPartCallback,
|
||||
typename ClickHandlerStartCallback,
|
||||
typename ClickHandlerFinishCallback,
|
||||
typename FlagsChangeCallback>
|
||||
void enumerateText(
|
||||
TextSelection selection,
|
||||
AppendPartCallback appendPartCallback,
|
||||
ClickHandlerStartCallback clickHandlerStartCallback,
|
||||
ClickHandlerFinishCallback clickHandlerFinishCallback,
|
||||
FlagsChangeCallback flagsChangeCallback) const;
|
||||
|
||||
// Template method for countWidth(), countHeight(), countLineWidths().
|
||||
// callback(lineWidth, lineBottom) will be called for all lines with:
|
||||
// QFixed lineWidth, int lineBottom
|
||||
template <typename Callback>
|
||||
void enumerateLines(
|
||||
int w,
|
||||
bool breakEverywhere,
|
||||
Callback &&callback) const;
|
||||
template <typename Callback>
|
||||
void enumerateLines(
|
||||
GeometryDescriptor geometry,
|
||||
Callback &&callback) const;
|
||||
|
||||
void insertModifications(int position, int delta);
|
||||
void removeModificationsAfter(int size);
|
||||
void recountNaturalSize(
|
||||
bool initial,
|
||||
Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto);
|
||||
|
||||
[[nodiscard]] TextForMimeData toText(
|
||||
TextSelection selection,
|
||||
bool composeExpanded,
|
||||
bool composeEntities) const;
|
||||
|
||||
const style::TextStyle *_st = nullptr;
|
||||
QString _text;
|
||||
std::vector<Block> _blocks;
|
||||
std::vector<Word> _words;
|
||||
ExtendedWrap _extended;
|
||||
|
||||
int _minResizeWidth = 0;
|
||||
int _maxWidth = 0;
|
||||
int _minHeight = 0;
|
||||
uint16 _startQuoteIndex = 0;
|
||||
bool _startParagraphLTR : 1 = false;
|
||||
bool _startParagraphRTL : 1 = false;
|
||||
bool _hasCustomEmoji : 1 = false;
|
||||
bool _isIsolatedEmoji : 1 = false;
|
||||
bool _isOnlyCustomEmoji : 1 = false;
|
||||
bool _hasNotEmojiAndSpaces : 1 = false;
|
||||
bool _skipBlockAddedNewline : 1 = false;
|
||||
bool _endsWithQuoteOrOtherDirection : 1 = false;
|
||||
|
||||
friend class BlockParser;
|
||||
friend class WordParser;
|
||||
friend class Renderer;
|
||||
friend class BidiAlgorithm;
|
||||
friend class StackEngine;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool IsBad(QChar ch);
|
||||
[[nodiscard]] bool IsWordSeparator(QChar ch);
|
||||
[[nodiscard]] bool IsAlmostLinkEnd(QChar ch);
|
||||
[[nodiscard]] bool IsLinkEnd(QChar ch);
|
||||
[[nodiscard]] bool IsNewline(QChar ch);
|
||||
[[nodiscard]] bool IsSpace(QChar ch);
|
||||
[[nodiscard]] bool IsDiacritic(QChar ch);
|
||||
[[nodiscard]] bool IsReplacedBySpace(QChar ch);
|
||||
[[nodiscard]] bool IsTrimmed(QChar ch);
|
||||
|
||||
[[nodiscard]] QSize CountOptimalTextSize(
|
||||
const String &text,
|
||||
int minWidth,
|
||||
int maxWidth);
|
||||
|
||||
} // namespace Ui::Text
|
||||
|
||||
inline TextSelection snapSelection(int from, int to) {
|
||||
return { static_cast<uint16>(std::clamp(from, 0, 0xFFFF)), static_cast<uint16>(std::clamp(to, 0, 0xFFFF)) };
|
||||
}
|
||||
inline TextSelection shiftSelection(TextSelection selection, uint16 byLength) {
|
||||
return snapSelection(int(selection.from) + byLength, int(selection.to) + byLength);
|
||||
}
|
||||
inline TextSelection unshiftSelection(TextSelection selection, uint16 byLength) {
|
||||
return snapSelection(int(selection.from) - int(byLength), int(selection.to) - int(byLength));
|
||||
}
|
||||
inline TextSelection shiftSelection(TextSelection selection, const Ui::Text::String &byText) {
|
||||
return shiftSelection(selection, byText.length());
|
||||
}
|
||||
inline TextSelection unshiftSelection(TextSelection selection, const Ui::Text::String &byText) {
|
||||
return unshiftSelection(selection, byText.length());
|
||||
}
|
||||
1013
Telegram/lib_ui/ui/text/text_bidi_algorithm.h
Normal file
1013
Telegram/lib_ui/ui/text/text_bidi_algorithm.h
Normal file
File diff suppressed because it is too large
Load Diff
276
Telegram/lib_ui/ui/text/text_block.cpp
Normal file
276
Telegram/lib_ui/ui/text/text_block.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_block.h"
|
||||
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#include <private/qfontengine_p.h>
|
||||
|
||||
namespace Ui {
|
||||
namespace Text {
|
||||
|
||||
style::font WithFlags(
|
||||
const style::font &font,
|
||||
TextBlockFlags flags,
|
||||
style::FontFlags fontFlags) {
|
||||
using namespace style::internal;
|
||||
|
||||
using Flag = style::FontFlag;
|
||||
if (!flags && !fontFlags) {
|
||||
return font;
|
||||
} else if (IsMono(flags) || (fontFlags & Flag::Monospace)) {
|
||||
return font->monospace();
|
||||
}
|
||||
auto result = font;
|
||||
if ((flags & TextBlockFlag::Bold) || (fontFlags & Flag::Bold)) {
|
||||
result = result->bold();
|
||||
} else if ((flags & TextBlockFlag::Semibold)
|
||||
|| (fontFlags & Flag::Semibold)) {
|
||||
result = result->semibold();
|
||||
}
|
||||
if ((flags & TextBlockFlag::Italic) || (fontFlags & Flag::Italic)) {
|
||||
result = result->italic();
|
||||
}
|
||||
if ((flags & TextBlockFlag::Underline)
|
||||
|| (fontFlags & Flag::Underline)) {
|
||||
result = result->underline();
|
||||
}
|
||||
if ((flags & TextBlockFlag::StrikeOut)
|
||||
|| (fontFlags & Flag::StrikeOut)) {
|
||||
result = result->strikeout();
|
||||
}
|
||||
if (flags & TextBlockFlag::Tilde) { // Tilde fix in OpenSans.
|
||||
result = result->semibold();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Qt::LayoutDirection UnpackParagraphDirection(bool ltr, bool rtl) {
|
||||
return ltr
|
||||
? Qt::LeftToRight
|
||||
: rtl
|
||||
? Qt::RightToLeft
|
||||
: Qt::LayoutDirectionAuto;
|
||||
}
|
||||
|
||||
AbstractBlock::AbstractBlock(TextBlockType type, BlockDescriptor descriptor)
|
||||
: _position(descriptor.position)
|
||||
, _type(static_cast<uint16>(type))
|
||||
, _flags(descriptor.flags)
|
||||
, _linkIndex(descriptor.linkIndex)
|
||||
, _colorIndex(descriptor.colorIndex) {
|
||||
}
|
||||
|
||||
uint16 AbstractBlock::position() const {
|
||||
return _position;
|
||||
}
|
||||
|
||||
TextBlockType AbstractBlock::type() const {
|
||||
return static_cast<TextBlockType>(_type);
|
||||
}
|
||||
|
||||
TextBlockFlags AbstractBlock::flags() const {
|
||||
return TextBlockFlags::from_raw(_flags);
|
||||
}
|
||||
|
||||
int AbstractBlock::objectWidth() const {
|
||||
switch (type()) {
|
||||
case TextBlockType::Emoji: return st::emojiSize + 2 * st::emojiPadding;
|
||||
case TextBlockType::CustomEmoji:
|
||||
return static_cast<const CustomEmojiBlock*>(this)->custom()->width();
|
||||
case TextBlockType::Skip:
|
||||
return static_cast<const SkipBlock*>(this)->width();
|
||||
}
|
||||
Unexpected("Type in AbstractBlock::objectWidth.");
|
||||
}
|
||||
|
||||
uint16 AbstractBlock::linkIndex() const {
|
||||
return _linkIndex;
|
||||
}
|
||||
|
||||
uint16 AbstractBlock::colorIndex() const {
|
||||
return _colorIndex;
|
||||
}
|
||||
|
||||
void AbstractBlock::setLinkIndex(uint16 index) {
|
||||
_linkIndex = index;
|
||||
}
|
||||
|
||||
TextBlock::TextBlock(BlockDescriptor descriptor)
|
||||
: AbstractBlock(TextBlockType::Text, descriptor) {
|
||||
}
|
||||
|
||||
EmojiBlock::EmojiBlock(BlockDescriptor descriptor, EmojiPtr emoji)
|
||||
: AbstractBlock(TextBlockType::Emoji, descriptor)
|
||||
, _emoji(emoji) {
|
||||
}
|
||||
|
||||
CustomEmojiBlock::CustomEmojiBlock(
|
||||
BlockDescriptor descriptor,
|
||||
std::unique_ptr<CustomEmoji> custom)
|
||||
: AbstractBlock(TextBlockType::CustomEmoji, descriptor)
|
||||
, _custom(std::move(custom)) {
|
||||
}
|
||||
|
||||
NewlineBlock::NewlineBlock(BlockDescriptor descriptor, uint16 quoteIndex)
|
||||
: AbstractBlock(TextBlockType::Newline, descriptor)
|
||||
, _quoteIndex(quoteIndex) {
|
||||
}
|
||||
|
||||
SkipBlock::SkipBlock(BlockDescriptor descriptor, int width, int height)
|
||||
: AbstractBlock(TextBlockType::Skip, descriptor)
|
||||
, _width(width)
|
||||
, _height(height) {
|
||||
}
|
||||
|
||||
int SkipBlock::width() const {
|
||||
return _width;
|
||||
}
|
||||
|
||||
int SkipBlock::height() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
Block::Block() {
|
||||
Unexpected("Should not be called.");
|
||||
}
|
||||
|
||||
Block::Block(Block &&other) {
|
||||
switch (other->type()) {
|
||||
case TextBlockType::Newline:
|
||||
emplace<NewlineBlock>(std::move(other.unsafe<NewlineBlock>()));
|
||||
break;
|
||||
case TextBlockType::Text:
|
||||
emplace<TextBlock>(std::move(other.unsafe<TextBlock>()));
|
||||
break;
|
||||
case TextBlockType::Emoji:
|
||||
emplace<EmojiBlock>(std::move(other.unsafe<EmojiBlock>()));
|
||||
break;
|
||||
case TextBlockType::CustomEmoji:
|
||||
emplace<CustomEmojiBlock>(
|
||||
std::move(other.unsafe<CustomEmojiBlock>()));
|
||||
break;
|
||||
case TextBlockType::Skip:
|
||||
emplace<SkipBlock>(std::move(other.unsafe<SkipBlock>()));
|
||||
break;
|
||||
default:
|
||||
Unexpected("Bad text block type in Block(Block&&).");
|
||||
}
|
||||
}
|
||||
|
||||
Block &Block::operator=(Block &&other) {
|
||||
if (&other == this) {
|
||||
return *this;
|
||||
}
|
||||
destroy();
|
||||
switch (other->type()) {
|
||||
case TextBlockType::Newline:
|
||||
emplace<NewlineBlock>(std::move(other.unsafe<NewlineBlock>()));
|
||||
break;
|
||||
case TextBlockType::Text:
|
||||
emplace<TextBlock>(std::move(other.unsafe<TextBlock>()));
|
||||
break;
|
||||
case TextBlockType::Emoji:
|
||||
emplace<EmojiBlock>(std::move(other.unsafe<EmojiBlock>()));
|
||||
break;
|
||||
case TextBlockType::CustomEmoji:
|
||||
emplace<CustomEmojiBlock>(
|
||||
std::move(other.unsafe<CustomEmojiBlock>()));
|
||||
break;
|
||||
case TextBlockType::Skip:
|
||||
emplace<SkipBlock>(std::move(other.unsafe<SkipBlock>()));
|
||||
break;
|
||||
default:
|
||||
Unexpected("Bad text block type in operator=(Block&&).");
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Block::~Block() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
Block Block::Newline(BlockDescriptor descriptor, uint16 quoteIndex) {
|
||||
return New<NewlineBlock>(descriptor, quoteIndex);
|
||||
}
|
||||
|
||||
Block Block::Text(BlockDescriptor descriptor) {
|
||||
return New<TextBlock>(descriptor);
|
||||
}
|
||||
|
||||
Block Block::Emoji(BlockDescriptor descriptor, EmojiPtr emoji) {
|
||||
return New<EmojiBlock>(descriptor, emoji);
|
||||
}
|
||||
|
||||
Block Block::CustomEmoji(
|
||||
BlockDescriptor descriptor,
|
||||
std::unique_ptr<Text::CustomEmoji> custom) {
|
||||
return New<CustomEmojiBlock>(descriptor, std::move(custom));
|
||||
}
|
||||
|
||||
Block Block::Skip(BlockDescriptor descriptor, int width, int height) {
|
||||
return New<SkipBlock>(descriptor, width, height);
|
||||
}
|
||||
|
||||
AbstractBlock *Block::get() {
|
||||
return &unsafe<AbstractBlock>();
|
||||
}
|
||||
|
||||
const AbstractBlock *Block::get() const {
|
||||
return &unsafe<AbstractBlock>();
|
||||
}
|
||||
|
||||
AbstractBlock *Block::operator->() {
|
||||
return get();
|
||||
}
|
||||
|
||||
const AbstractBlock *Block::operator->() const {
|
||||
return get();
|
||||
}
|
||||
|
||||
AbstractBlock &Block::operator*() {
|
||||
return *get();
|
||||
}
|
||||
|
||||
const AbstractBlock &Block::operator*() const {
|
||||
return *get();
|
||||
}
|
||||
|
||||
void Block::destroy() {
|
||||
switch (get()->type()) {
|
||||
case TextBlockType::Newline:
|
||||
unsafe<NewlineBlock>().~NewlineBlock();
|
||||
break;
|
||||
case TextBlockType::Text:
|
||||
unsafe<TextBlock>().~TextBlock();
|
||||
break;
|
||||
case TextBlockType::Emoji:
|
||||
unsafe<EmojiBlock>().~EmojiBlock();
|
||||
break;
|
||||
case TextBlockType::CustomEmoji:
|
||||
unsafe<CustomEmojiBlock>().~CustomEmojiBlock();
|
||||
break;
|
||||
case TextBlockType::Skip:
|
||||
unsafe<SkipBlock>().~SkipBlock();
|
||||
break;
|
||||
default:
|
||||
Unexpected("Bad text block type in Block(Block&&).");
|
||||
}
|
||||
}
|
||||
|
||||
int CountBlockHeight(
|
||||
const AbstractBlock *block,
|
||||
const style::TextStyle *st) {
|
||||
return (block->type() == TextBlockType::Skip)
|
||||
? static_cast<const SkipBlock*>(block)->height()
|
||||
: st->lineHeight
|
||||
? st->lineHeight
|
||||
: st->font->height;
|
||||
}
|
||||
|
||||
} // namespace Text
|
||||
} // namespace Ui
|
||||
246
Telegram/lib_ui/ui/text/text_block.h
Normal file
246
Telegram/lib_ui/ui/text/text_block.h
Normal file
@@ -0,0 +1,246 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/flags.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/style/style_core.h"
|
||||
#include "ui/emoji_config.h"
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
#include <private/qfixed_p.h>
|
||||
|
||||
namespace style {
|
||||
struct TextStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
enum class TextBlockType : uint16 {
|
||||
Newline = 0x01,
|
||||
Text = 0x02,
|
||||
Emoji = 0x03,
|
||||
CustomEmoji = 0x04,
|
||||
Skip = 0x05,
|
||||
};
|
||||
|
||||
enum class TextBlockFlag : uint16 {
|
||||
Bold = 0x001,
|
||||
Italic = 0x002,
|
||||
Underline = 0x004,
|
||||
StrikeOut = 0x008,
|
||||
Tilde = 0x010, // Tilde fix in OpenSans.
|
||||
Semibold = 0x020,
|
||||
Code = 0x040,
|
||||
Pre = 0x080,
|
||||
Spoiler = 0x100,
|
||||
Blockquote = 0x200,
|
||||
};
|
||||
inline constexpr bool is_flag_type(TextBlockFlag) { return true; }
|
||||
using TextBlockFlags = base::flags<TextBlockFlag>;
|
||||
|
||||
[[nodiscard]] style::font WithFlags(
|
||||
const style::font &font,
|
||||
TextBlockFlags flags,
|
||||
style::FontFlags fontFlags = 0);
|
||||
|
||||
[[nodiscard]] Qt::LayoutDirection UnpackParagraphDirection(
|
||||
bool ltr,
|
||||
bool rtl);
|
||||
|
||||
struct BlockDescriptor {
|
||||
uint16 position = 0;
|
||||
TextBlockFlags flags;
|
||||
uint16 linkIndex = 0;
|
||||
uint16 colorIndex = 0;
|
||||
};
|
||||
|
||||
class AbstractBlock {
|
||||
public:
|
||||
[[nodiscard]] uint16 position() const;
|
||||
[[nodiscard]] TextBlockType type() const;
|
||||
[[nodiscard]] TextBlockFlags flags() const;
|
||||
[[nodiscard]] int objectWidth() const;
|
||||
[[nodiscard]] uint16 colorIndex() const;
|
||||
[[nodiscard]] uint16 linkIndex() const;
|
||||
void setLinkIndex(uint16 index);
|
||||
|
||||
protected:
|
||||
AbstractBlock(TextBlockType type, BlockDescriptor descriptor);
|
||||
|
||||
uint16 _position = 0;
|
||||
uint16 _type : 4 = 0;
|
||||
uint16 _flags : 12 = 0;
|
||||
uint16 _linkIndex = 0;
|
||||
uint16 _colorIndex = 0;
|
||||
|
||||
};
|
||||
|
||||
class NewlineBlock final : public AbstractBlock {
|
||||
public:
|
||||
NewlineBlock(BlockDescriptor descriptor, uint16 quoteIndex);
|
||||
|
||||
void setQuoteIndex(uint16 index) {
|
||||
_quoteIndex = index;
|
||||
}
|
||||
[[nodiscard]] uint16 quoteIndex() const {
|
||||
return _quoteIndex;
|
||||
}
|
||||
void setParagraphDirection(Qt::LayoutDirection direction) {
|
||||
_paragraphLTR = (direction == Qt::LeftToRight);
|
||||
_paragraphRTL = (direction == Qt::RightToLeft);
|
||||
}
|
||||
[[nodiscard]] Qt::LayoutDirection paragraphDirection() const {
|
||||
return UnpackParagraphDirection(_paragraphLTR, _paragraphRTL);
|
||||
}
|
||||
|
||||
private:
|
||||
uint16 _quoteIndex = 0;
|
||||
bool _paragraphLTR : 1 = false;
|
||||
bool _paragraphRTL : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
class TextBlock final : public AbstractBlock {
|
||||
public:
|
||||
explicit TextBlock(BlockDescriptor descriptor);
|
||||
|
||||
};
|
||||
|
||||
class EmojiBlock final : public AbstractBlock {
|
||||
public:
|
||||
EmojiBlock(BlockDescriptor descriptor, EmojiPtr emoji);
|
||||
|
||||
[[nodiscard]] EmojiPtr emoji() const {
|
||||
return _emoji;
|
||||
}
|
||||
|
||||
private:
|
||||
EmojiPtr _emoji = nullptr;
|
||||
|
||||
};
|
||||
|
||||
class CustomEmojiBlock final : public AbstractBlock {
|
||||
public:
|
||||
CustomEmojiBlock(
|
||||
BlockDescriptor descriptor,
|
||||
std::unique_ptr<CustomEmoji> custom);
|
||||
|
||||
[[nodiscard]] not_null<CustomEmoji*> custom() const {
|
||||
return _custom.get();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<CustomEmoji> _custom;
|
||||
|
||||
};
|
||||
|
||||
class SkipBlock final : public AbstractBlock {
|
||||
public:
|
||||
SkipBlock(BlockDescriptor descriptor, int width, int height);
|
||||
|
||||
[[nodiscard]] int width() const;
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
private:
|
||||
int _width = 0;
|
||||
int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
class Block final {
|
||||
public:
|
||||
Block();
|
||||
Block(Block &&other);
|
||||
Block &operator=(Block &&other);
|
||||
~Block();
|
||||
|
||||
[[nodiscard]] static Block Newline(
|
||||
BlockDescriptor descriptor,
|
||||
uint16 quoteIndex);
|
||||
[[nodiscard]] static Block Text(BlockDescriptor descriptor);
|
||||
[[nodiscard]] static Block Emoji(
|
||||
BlockDescriptor descriptor,
|
||||
EmojiPtr emoji);
|
||||
[[nodiscard]] static Block CustomEmoji(
|
||||
BlockDescriptor descriptor,
|
||||
std::unique_ptr<CustomEmoji> custom);
|
||||
|
||||
[[nodiscard]] static Block Skip(
|
||||
BlockDescriptor descriptor,
|
||||
int width,
|
||||
int height);
|
||||
|
||||
template <typename FinalBlock>
|
||||
[[nodiscard]] FinalBlock &unsafe() {
|
||||
return *reinterpret_cast<FinalBlock*>(&_data);
|
||||
}
|
||||
|
||||
template <typename FinalBlock>
|
||||
[[nodiscard]] const FinalBlock &unsafe() const {
|
||||
return *reinterpret_cast<const FinalBlock*>(&_data);
|
||||
}
|
||||
|
||||
[[nodiscard]] AbstractBlock *get();
|
||||
[[nodiscard]] const AbstractBlock *get() const;
|
||||
|
||||
[[nodiscard]] AbstractBlock *operator->();
|
||||
[[nodiscard]] const AbstractBlock *operator->() const;
|
||||
|
||||
[[nodiscard]] AbstractBlock &operator*();
|
||||
[[nodiscard]] const AbstractBlock &operator*() const;
|
||||
|
||||
private:
|
||||
struct Tag {
|
||||
};
|
||||
|
||||
explicit Block(const Tag &) {
|
||||
}
|
||||
|
||||
template <typename FinalType, typename ...Args>
|
||||
[[nodiscard]] static Block New(Args &&...args) {
|
||||
auto result = Block(Tag{});
|
||||
result.emplace<FinalType>(std::forward<Args>(args)...);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename FinalType, typename ...Args>
|
||||
void emplace(Args &&...args) {
|
||||
new (&_data) FinalType(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
void destroy();
|
||||
|
||||
static_assert(sizeof(NewlineBlock) <= sizeof(SkipBlock));
|
||||
static_assert(alignof(NewlineBlock) <= alignof(void*));
|
||||
static_assert(sizeof(EmojiBlock) <= sizeof(SkipBlock));
|
||||
static_assert(alignof(EmojiBlock) <= alignof(void*));
|
||||
static_assert(sizeof(TextBlock) <= sizeof(SkipBlock));
|
||||
static_assert(alignof(TextBlock) <= alignof(void*));
|
||||
static_assert(sizeof(CustomEmojiBlock) <= sizeof(SkipBlock));
|
||||
static_assert(alignof(CustomEmojiBlock) <= alignof(void*));
|
||||
|
||||
std::aligned_storage_t<sizeof(SkipBlock), alignof(void*)> _data;
|
||||
|
||||
};
|
||||
|
||||
using Blocks = std::vector<Block>;
|
||||
|
||||
[[nodiscard]] inline uint16 CountPosition(Blocks::const_iterator i) {
|
||||
return (*i)->position();
|
||||
}
|
||||
|
||||
[[nodiscard]] int CountBlockHeight(
|
||||
const AbstractBlock *block,
|
||||
const style::TextStyle *st);
|
||||
|
||||
[[nodiscard]] inline bool IsMono(TextBlockFlags flags) {
|
||||
return (flags & TextBlockFlag::Pre) || (flags & TextBlockFlag::Code);
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
850
Telegram/lib_ui/ui/text/text_block_parser.cpp
Normal file
850
Telegram/lib_ui/ui/text/text_block_parser.cpp
Normal file
@@ -0,0 +1,850 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_block_parser.h"
|
||||
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "ui/integration.h"
|
||||
#include "ui/text/text_extended_data.h"
|
||||
#include "ui/text/text_isolated_emoji.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#include <QtCore/QUrl>
|
||||
#include <private/qfixed_p.h>
|
||||
|
||||
namespace Ui::Text {
|
||||
namespace {
|
||||
|
||||
constexpr auto kStringLinkIndexShift = uint16(0x8000);
|
||||
constexpr auto kMaxDiacAfterSymbol = 2;
|
||||
|
||||
[[nodiscard]] TextWithEntities PrepareRichFromRich(
|
||||
const TextWithEntities &text,
|
||||
const TextParseOptions &options) {
|
||||
auto result = text;
|
||||
const auto &preparsed = text.entities;
|
||||
const bool parseLinks = (options.flags & TextParseLinks);
|
||||
const bool parseColorized = (options.flags & TextParseColorized);
|
||||
if (!preparsed.isEmpty() && (parseLinks || parseColorized)) {
|
||||
bool parseMentions = (options.flags & TextParseMentions);
|
||||
bool parseHashtags = (options.flags & TextParseHashtags);
|
||||
bool parseBotCommands = (options.flags & TextParseBotCommands);
|
||||
bool parseMarkdown = (options.flags & TextParseMarkdown);
|
||||
if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
|
||||
int32 i = 0, l = preparsed.size();
|
||||
result.entities.clear();
|
||||
result.entities.reserve(l);
|
||||
for (; i < l; ++i) {
|
||||
auto type = preparsed.at(i).type();
|
||||
if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
|
||||
(type == EntityType::Hashtag && !parseHashtags) ||
|
||||
(type == EntityType::Cashtag && !parseHashtags) ||
|
||||
(!parseLinks
|
||||
&& (type == EntityType::Url
|
||||
|| type == EntityType::CustomUrl)) ||
|
||||
(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
|
||||
(!parseMarkdown && (type == EntityType::Bold
|
||||
|| type == EntityType::Semibold
|
||||
|| type == EntityType::Italic
|
||||
|| type == EntityType::Underline
|
||||
|| type == EntityType::StrikeOut
|
||||
|| type == EntityType::Colorized
|
||||
|| type == EntityType::Spoiler
|
||||
|| type == EntityType::Code
|
||||
|| type == EntityType::Pre
|
||||
|| type == EntityType::Blockquote))) {
|
||||
continue;
|
||||
}
|
||||
result.entities.push_back(preparsed.at(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tilde fix in OpenSans.
|
||||
[[nodiscard]] bool ComputeCheckTilde(const style::TextStyle &st) {
|
||||
const auto &font = st.font;
|
||||
return (font->size() * style::DevicePixelRatio() == 13)
|
||||
&& (font->flags() == 0)
|
||||
&& (font->f.family() == qstr("Open Sans"));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsDiacriticAllowedAfter(QChar ch) {
|
||||
const auto code = ch.unicode();
|
||||
const auto category = ch.category();
|
||||
return (code > 32)
|
||||
&& (category != QChar::Other_Control)
|
||||
&& (category != QChar::Other_Format)
|
||||
&& (category != QChar::Other_PrivateUse)
|
||||
&& (category != QChar::Other_NotAssigned);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlockParser::StartedEntity::StartedEntity(TextBlockFlags flags)
|
||||
: _value(flags.value())
|
||||
, _type(Type::Flags) {
|
||||
Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
|
||||
}
|
||||
|
||||
BlockParser::StartedEntity::StartedEntity(uint16 index, Type type)
|
||||
: _value(index)
|
||||
, _type(type) {
|
||||
Expects((_type == Type::Link)
|
||||
? (_value >= kStringLinkIndexShift)
|
||||
: (_value < kStringLinkIndexShift));
|
||||
}
|
||||
|
||||
BlockParser::StartedEntity::Type BlockParser::StartedEntity::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
std::optional<TextBlockFlags> BlockParser::StartedEntity::flags() const {
|
||||
if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) {
|
||||
return TextBlockFlags::from_raw(uint16(_value));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint16> BlockParser::StartedEntity::linkIndex() const {
|
||||
if ((_value < int(kStringLinkIndexShift) && (_type == Type::IndexedLink))
|
||||
|| (_value >= int(kStringLinkIndexShift) && (_type == Type::Link))) {
|
||||
return uint16(_value);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint16> BlockParser::StartedEntity::colorIndex() const {
|
||||
if (_type == Type::Colorized) {
|
||||
return uint16(_value);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
BlockParser::BlockParser(
|
||||
not_null<String*> string,
|
||||
const TextWithEntities &textWithEntities,
|
||||
const TextParseOptions &options,
|
||||
const MarkedContext &context)
|
||||
: BlockParser(
|
||||
string,
|
||||
PrepareRichFromRich(textWithEntities, options),
|
||||
options,
|
||||
context,
|
||||
ReadyToken()) {
|
||||
}
|
||||
|
||||
BlockParser::BlockParser(
|
||||
not_null<String*> string,
|
||||
TextWithEntities &&source,
|
||||
const TextParseOptions &options,
|
||||
const MarkedContext &context,
|
||||
ReadyToken)
|
||||
: _t(string)
|
||||
, _tText(string->_text)
|
||||
, _tBlocks(string->_blocks)
|
||||
, _source(std::move(source))
|
||||
, _context(context)
|
||||
, _start(_source.text.constData())
|
||||
, _end(_start + _source.text.size())
|
||||
, _ptr(_start)
|
||||
, _entitiesEnd(_source.entities.end())
|
||||
, _waitingEntity(_source.entities.begin())
|
||||
, _multiline(options.flags & TextParseMultiline)
|
||||
, _checkTilde(ComputeCheckTilde(*_t->_st)) {
|
||||
parse(options);
|
||||
}
|
||||
|
||||
void BlockParser::createBlock(int skipBack) {
|
||||
if (_linkIndex < kStringLinkIndexShift && _linkIndex > _maxLinkIndex) {
|
||||
_maxLinkIndex = _linkIndex;
|
||||
}
|
||||
if (_linkIndex > kStringLinkIndexShift) {
|
||||
_maxShiftedLinkIndex = std::max(
|
||||
uint16(_linkIndex - kStringLinkIndexShift),
|
||||
_maxShiftedLinkIndex);
|
||||
}
|
||||
|
||||
const auto length = int(_tText.size()) + skipBack - _blockStart;
|
||||
if (length <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto newline = !_emoji
|
||||
&& (length == 1)
|
||||
&& (_tText.at(_blockStart) == QChar::LineFeed);
|
||||
if (_newlineAwaited) {
|
||||
_newlineAwaited = false;
|
||||
if (!newline) {
|
||||
_t->insertModifications(_blockStart, 1);
|
||||
_tText.insert(_blockStart, QChar::LineFeed);
|
||||
createBlock(skipBack - length);
|
||||
}
|
||||
}
|
||||
const auto linkIndex = _monoIndex ? _monoIndex : _linkIndex;
|
||||
auto custom = _customEmojiData.isEmpty()
|
||||
? nullptr
|
||||
: MakeCustomEmoji(_customEmojiData, _context);
|
||||
const auto push = [&](auto &&factory, auto &&...args) {
|
||||
_tBlocks.push_back(factory({
|
||||
.position = uint16(_blockStart),
|
||||
.flags = _flags,
|
||||
.linkIndex = linkIndex,
|
||||
.colorIndex = _colorIndex,
|
||||
}, std::forward<decltype(args)>(args)...));
|
||||
};
|
||||
if (custom) {
|
||||
push(&Block::CustomEmoji, std::move(custom));
|
||||
} else if (_emoji) {
|
||||
push(&Block::Emoji, _emoji);
|
||||
} else if (newline) {
|
||||
push(&Block::Newline, _quoteIndex);
|
||||
} else {
|
||||
push(&Block::Text/*, _t->_minResizeWidth*/);
|
||||
}
|
||||
// Diacritic can't attach from the next block to this one.
|
||||
_allowDiacritic = false;
|
||||
_blockStart += length;
|
||||
_customEmojiData = QByteArray();
|
||||
_emoji = nullptr;
|
||||
}
|
||||
|
||||
void BlockParser::createNewlineBlock(bool fromOriginalText) {
|
||||
if (!fromOriginalText) {
|
||||
_t->insertModifications(_tText.size(), 1);
|
||||
}
|
||||
_tText.push_back(QChar::LineFeed);
|
||||
_allowDiacritic = false;
|
||||
createBlock();
|
||||
}
|
||||
|
||||
void BlockParser::ensureAtNewline(QuoteDetails quote) {
|
||||
createBlock();
|
||||
const auto lastType = _tBlocks.empty()
|
||||
? TextBlockType::Newline
|
||||
: _tBlocks.back()->type();
|
||||
if (lastType != TextBlockType::Newline) {
|
||||
auto saved = base::take(_customEmojiData);
|
||||
createNewlineBlock(false);
|
||||
_customEmojiData = base::take(saved);
|
||||
}
|
||||
if (_quoteIndex) {
|
||||
closeQuote();
|
||||
}
|
||||
_quoteStartPosition = _tText.size();
|
||||
auto "es = _t->ensureQuotes()->list;
|
||||
quotes.push_back(std::move(quote));
|
||||
const auto index = _quoteIndex = int(quotes.size());
|
||||
if (_tBlocks.empty()) {
|
||||
_t->_startQuoteIndex = index;
|
||||
} else {
|
||||
auto &last = _tBlocks.back();
|
||||
Assert(last->type() == TextBlockType::Newline);
|
||||
last.unsafe<NewlineBlock>().setQuoteIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
void BlockParser::closeQuote() {
|
||||
if (!_quoteIndex) {
|
||||
return;
|
||||
}
|
||||
auto "es = _t->ensureQuotes()->list;
|
||||
auto "e = quotes[_quoteIndex - 1];
|
||||
const auto from = _quoteStartPosition;
|
||||
const auto till = _tText.size();
|
||||
if (quote.pre && till > from) {
|
||||
quote.copy = std::make_shared<PreClickHandler>(
|
||||
_t,
|
||||
from,
|
||||
till - from);
|
||||
} else if (quote.blockquote && quote.collapsed) {
|
||||
quote.toggle = std::make_shared<BlockquoteClickHandler>(
|
||||
_t,
|
||||
_quoteIndex);
|
||||
}
|
||||
_quoteIndex = 0;
|
||||
}
|
||||
|
||||
void BlockParser::finishEntities() {
|
||||
while (!_startedEntities.empty()
|
||||
&& (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
|
||||
auto list = std::move(_startedEntities.begin()->second);
|
||||
_startedEntities.erase(_startedEntities.begin());
|
||||
|
||||
while (!list.empty()) {
|
||||
if (list.back().type() == StartedEntity::Type::CustomEmoji) {
|
||||
createBlock();
|
||||
} else if (const auto flags = list.back().flags()) {
|
||||
if (_flags & (*flags)) {
|
||||
createBlock();
|
||||
_flags &= ~(*flags);
|
||||
const auto lastType = _tBlocks.empty()
|
||||
? TextBlockType::Newline
|
||||
: _tBlocks.back()->type();
|
||||
if ((*flags)
|
||||
& (TextBlockFlag::Pre | TextBlockFlag::Blockquote)) {
|
||||
closeQuote();
|
||||
if (lastType != TextBlockType::Newline) {
|
||||
_newlineAwaited = true;
|
||||
} else if (_tBlocks.empty()) {
|
||||
_t->_startQuoteIndex = 0;
|
||||
} else {
|
||||
auto &last = _tBlocks.back();
|
||||
last.unsafe<NewlineBlock>().setQuoteIndex(0);
|
||||
}
|
||||
}
|
||||
if (IsMono(*flags)) {
|
||||
_monoIndex = 0;
|
||||
}
|
||||
}
|
||||
} else if (const auto linkIndex = list.back().linkIndex()) {
|
||||
if (_linkIndex == *linkIndex) {
|
||||
createBlock();
|
||||
_linkIndex = 0;
|
||||
}
|
||||
} else if (const auto colorIndex = list.back().colorIndex()) {
|
||||
if (_colorIndex == *colorIndex) {
|
||||
createBlock();
|
||||
_colorIndex = 0;
|
||||
}
|
||||
}
|
||||
list.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if at least one entity was parsed in the current position.
|
||||
bool BlockParser::checkEntities() {
|
||||
finishEntities();
|
||||
skipPassedEntities();
|
||||
if (_waitingEntity == _entitiesEnd
|
||||
|| _ptr < _start + _waitingEntity->offset()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto flags = TextBlockFlags();
|
||||
auto link = EntityLinkData();
|
||||
auto monoIndex = 0;
|
||||
const auto entityType = _waitingEntity->type();
|
||||
const auto entityLength = _waitingEntity->length();
|
||||
const auto entityBegin = _start + _waitingEntity->offset();
|
||||
const auto entityEnd = entityBegin + entityLength;
|
||||
const auto pushSimpleUrl = [&](EntityType type) {
|
||||
link.type = type;
|
||||
link.data = QString(entityBegin, entityLength);
|
||||
if (type == EntityType::Url) {
|
||||
computeLinkText(link.data, &link.text, &link.shown);
|
||||
} else {
|
||||
link.text = link.data;
|
||||
}
|
||||
};
|
||||
const auto pushComplexUrl = [&] {
|
||||
link.type = entityType;
|
||||
link.data = _waitingEntity->data();
|
||||
link.text = QString(entityBegin, entityLength);
|
||||
};
|
||||
|
||||
using Type = StartedEntity::Type;
|
||||
|
||||
if (entityType == EntityType::CustomEmoji) {
|
||||
createBlock();
|
||||
_customEmojiData = _waitingEntity->data();
|
||||
_startedEntities[entityEnd].emplace_back(0, Type::CustomEmoji);
|
||||
} else if (entityType == EntityType::Bold) {
|
||||
flags = TextBlockFlag::Bold;
|
||||
} else if (entityType == EntityType::Semibold) {
|
||||
flags = TextBlockFlag::Semibold;
|
||||
} else if (entityType == EntityType::Italic) {
|
||||
flags = TextBlockFlag::Italic;
|
||||
} else if (entityType == EntityType::Underline) {
|
||||
flags = TextBlockFlag::Underline;
|
||||
} else if (entityType == EntityType::Spoiler) {
|
||||
flags = TextBlockFlag::Spoiler;
|
||||
} else if (entityType == EntityType::StrikeOut) {
|
||||
flags = TextBlockFlag::StrikeOut;
|
||||
} else if ((entityType == EntityType::Code) // #TODO entities
|
||||
|| (entityType == EntityType::Pre)) {
|
||||
if (entityType == EntityType::Code) {
|
||||
flags = TextBlockFlag::Code;
|
||||
} else {
|
||||
flags = TextBlockFlag::Pre;
|
||||
ensureAtNewline({
|
||||
.language = _waitingEntity->data(),
|
||||
.pre = true,
|
||||
});
|
||||
}
|
||||
const auto text = QString(entityBegin, entityLength);
|
||||
|
||||
// It is better to trim the text to identify "Sample\n" as inline.
|
||||
const auto trimmed = text.trimmed();
|
||||
const auto isSingleLine = !trimmed.isEmpty()
|
||||
&& ranges::none_of(trimmed, IsNewline);
|
||||
|
||||
// TODO: remove trimming.
|
||||
if (isSingleLine && (entityType == EntityType::Code)) {
|
||||
_monos.push_back({ .text = text, .type = entityType });
|
||||
monoIndex = _monos.size();
|
||||
}
|
||||
} else if (entityType == EntityType::Blockquote) {
|
||||
flags = TextBlockFlag::Blockquote;
|
||||
ensureAtNewline({
|
||||
.blockquote = true,
|
||||
.collapsed = !_waitingEntity->data().isEmpty(),
|
||||
});
|
||||
} else if (entityType == EntityType::Url
|
||||
|| entityType == EntityType::Email
|
||||
|| entityType == EntityType::Phone
|
||||
|| entityType == EntityType::BankCard
|
||||
|| entityType == EntityType::Mention
|
||||
|| entityType == EntityType::Hashtag
|
||||
|| entityType == EntityType::Cashtag
|
||||
|| entityType == EntityType::BotCommand) {
|
||||
pushSimpleUrl(entityType);
|
||||
} else if (entityType == EntityType::CustomUrl) {
|
||||
const auto url = _waitingEntity->data();
|
||||
const auto text = QString(entityBegin, entityLength);
|
||||
if (url == text) {
|
||||
pushSimpleUrl(EntityType::Url);
|
||||
} else {
|
||||
pushComplexUrl();
|
||||
}
|
||||
} else if (entityType == EntityType::MentionName) {
|
||||
pushComplexUrl();
|
||||
} else if (entityType == EntityType::Colorized) {
|
||||
createBlock();
|
||||
|
||||
const auto data = _waitingEntity->data();
|
||||
_colorIndex = data.isEmpty() ? 1 : (data.front().unicode() + 1);
|
||||
_startedEntities[entityEnd].emplace_back(
|
||||
_colorIndex,
|
||||
Type::Colorized);
|
||||
}
|
||||
|
||||
if (link.type != EntityType::Invalid) {
|
||||
createBlock();
|
||||
|
||||
_links.push_back(link);
|
||||
const auto tempIndex = _links.size();
|
||||
const auto useCustom = processCustomIndex(tempIndex);
|
||||
_linkIndex = tempIndex + (useCustom ? 0 : kStringLinkIndexShift);
|
||||
_startedEntities[entityEnd].emplace_back(
|
||||
_linkIndex,
|
||||
useCustom ? Type::IndexedLink : Type::Link);
|
||||
} else if (flags) {
|
||||
if (!(_flags & flags)) {
|
||||
createBlock();
|
||||
_flags |= flags;
|
||||
_startedEntities[entityEnd].emplace_back(flags);
|
||||
_monoIndex = monoIndex;
|
||||
}
|
||||
}
|
||||
|
||||
++_waitingEntity;
|
||||
skipBadEntities();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BlockParser::processCustomIndex(uint16 index) {
|
||||
auto &url = _links[index - 1].data;
|
||||
if (url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (url.startsWith("internal:index")) {
|
||||
const auto customIndex = uint16(url.back().unicode());
|
||||
// if (customIndex != index) {
|
||||
url = QString();
|
||||
_linksIndexes.push_back(customIndex);
|
||||
return true;
|
||||
// }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BlockParser::skipPassedEntities() {
|
||||
while (_waitingEntity != _entitiesEnd
|
||||
&& _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) {
|
||||
++_waitingEntity;
|
||||
}
|
||||
}
|
||||
|
||||
void BlockParser::skipBadEntities() {
|
||||
if (_links.size() >= 0x7FFF) {
|
||||
while (_waitingEntity != _entitiesEnd
|
||||
&& (isLinkEntity(*_waitingEntity)
|
||||
|| isInvalidEntity(*_waitingEntity))) {
|
||||
++_waitingEntity;
|
||||
}
|
||||
} else {
|
||||
while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) {
|
||||
++_waitingEntity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlockParser::parseCurrentChar() {
|
||||
_ch = ((_ptr < _end) ? *_ptr : QChar(0));
|
||||
_emojiLookback = 0;
|
||||
const auto inCustomEmoji = !_customEmojiData.isEmpty();
|
||||
const auto isNewLine = !inCustomEmoji && _multiline && IsNewline(_ch);
|
||||
const auto replaceWithSpace = IsSpace(_ch) && (_ch != QChar::Nbsp);
|
||||
const auto isDiacritic = IsDiacritic(_ch);
|
||||
const auto isTilde = !inCustomEmoji && _checkTilde && (_ch == '~');
|
||||
const auto skip = [&] {
|
||||
if (IsBad(_ch) || _ch.isLowSurrogate()) {
|
||||
return true;
|
||||
} else if (_ch.unicode() == 0xFE0F && Platform::IsMac()) {
|
||||
// Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :(
|
||||
return true;
|
||||
} else if (isDiacritic) {
|
||||
if (!_allowDiacritic
|
||||
|| _emoji
|
||||
|| ++_diacritics > kMaxDiacAfterSymbol) {
|
||||
return true;
|
||||
}
|
||||
} else if (_ch.isHighSurrogate()) {
|
||||
if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) {
|
||||
return true;
|
||||
}
|
||||
const auto ucs4 = QChar::surrogateToUcs4(_ch, *(_ptr + 1));
|
||||
if (ucs4 >= 0xE0000) {
|
||||
// Unicode tags are skipped.
|
||||
// Only place they work is in some flag emoji,
|
||||
// but in that case they were already parsed as emoji before.
|
||||
//
|
||||
// For unknown reason in some unknown cases strings with such
|
||||
// symbols lead to crashes on some Linux distributions, see
|
||||
// https://github.com/telegramdesktop/tdesktop/issues/7005
|
||||
//
|
||||
// At least one crashing text was starting that way:
|
||||
//
|
||||
// 0xd83d 0xdcda 0xdb40 0xdc69 0xdb40 0xdc64 0xdb40 0xdc6a
|
||||
// 0xdb40 0xdc77 0xdb40 0xdc7f 0x32 ... simple text here ...
|
||||
//
|
||||
// or in codepoints:
|
||||
//
|
||||
// 0x1f4da 0xe0069 0xe0064 0xe006a 0xe0077 0xe007f 0x32 ...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
if (_ch.isHighSurrogate() && !skip) {
|
||||
_tText.push_back(_ch);
|
||||
++_ptr;
|
||||
_ch = *_ptr;
|
||||
_emojiLookback = 1;
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
if (_ptr < _end) {
|
||||
_t->insertModifications(_tText.size(), -1);
|
||||
}
|
||||
_ch = QChar(0);
|
||||
_allowDiacritic = false;
|
||||
} else {
|
||||
if (isTilde) { // Tilde fix in OpenSans.
|
||||
if (!(_flags & TextBlockFlag::Tilde)) {
|
||||
createBlock(-_emojiLookback);
|
||||
_flags |= TextBlockFlag::Tilde;
|
||||
}
|
||||
} else {
|
||||
if (_flags & TextBlockFlag::Tilde) {
|
||||
createBlock(-_emojiLookback);
|
||||
_flags &= ~TextBlockFlag::Tilde;
|
||||
}
|
||||
}
|
||||
if (isNewLine) {
|
||||
createBlock();
|
||||
createNewlineBlock(true);
|
||||
} else if (replaceWithSpace) {
|
||||
_tText.push_back(QChar::Space);
|
||||
_allowDiacritic = false;
|
||||
} else {
|
||||
if (_emoji) {
|
||||
createBlock(-_emojiLookback);
|
||||
}
|
||||
_tText.push_back(_ch);
|
||||
_allowDiacritic = IsDiacriticAllowedAfter(_ch);
|
||||
}
|
||||
if (!isDiacritic) {
|
||||
_diacritics = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlockParser::parseEmojiFromCurrent() {
|
||||
if (!_customEmojiData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int len = 0;
|
||||
auto e = Emoji::Find(_ptr - _emojiLookback, _end, &len);
|
||||
if (!e) return;
|
||||
|
||||
for (int l = len - _emojiLookback - 1; l > 0; --l) {
|
||||
_tText.push_back(*++_ptr);
|
||||
}
|
||||
if (e->hasPostfix()) {
|
||||
Assert(!_tText.isEmpty());
|
||||
const auto last = _tText[_tText.size() - 1];
|
||||
if (last.unicode() != Emoji::kPostfix) {
|
||||
_t->insertModifications(_tText.size(), 1);
|
||||
_tText.push_back(QChar(Emoji::kPostfix));
|
||||
++len;
|
||||
}
|
||||
}
|
||||
|
||||
createBlock(-len);
|
||||
_emoji = e;
|
||||
}
|
||||
|
||||
bool BlockParser::isInvalidEntity(const EntityInText &entity) const {
|
||||
const auto length = entity.length();
|
||||
return (_start + entity.offset() + length > _end) || (length <= 0);
|
||||
}
|
||||
|
||||
bool BlockParser::isLinkEntity(const EntityInText &entity) const {
|
||||
const auto type = entity.type();
|
||||
const auto urls = {
|
||||
EntityType::Url,
|
||||
EntityType::CustomUrl,
|
||||
EntityType::Email,
|
||||
EntityType::Hashtag,
|
||||
EntityType::Cashtag,
|
||||
EntityType::Mention,
|
||||
EntityType::MentionName,
|
||||
EntityType::Phone,
|
||||
EntityType::BankCard,
|
||||
EntityType::BotCommand
|
||||
};
|
||||
return ranges::find(urls, type) != std::end(urls);
|
||||
}
|
||||
|
||||
void BlockParser::parse(const TextParseOptions &options) {
|
||||
skipBadEntities();
|
||||
trimSourceRange();
|
||||
|
||||
_tText.resize(0);
|
||||
if (_t->_extended) {
|
||||
base::take(_t->_extended->modifications);
|
||||
}
|
||||
_tText.reserve(_end - _ptr);
|
||||
|
||||
if (_ptr > _start) {
|
||||
_t->insertModifications(0, -(_ptr - _start));
|
||||
}
|
||||
|
||||
for (; _ptr <= _end; ++_ptr) {
|
||||
while (checkEntities()) {
|
||||
}
|
||||
parseCurrentChar();
|
||||
parseEmojiFromCurrent();
|
||||
|
||||
if (_tText.size() >= 0x8000) {
|
||||
break; // 32k max
|
||||
}
|
||||
}
|
||||
createBlock();
|
||||
finalize(options);
|
||||
}
|
||||
|
||||
void BlockParser::trimSourceRange() {
|
||||
const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
|
||||
_source.entities,
|
||||
_end - _start);
|
||||
|
||||
while (_ptr != _end && IsTrimmed(*_ptr) && _ptr != _start + firstMonospaceOffset) {
|
||||
++_ptr;
|
||||
}
|
||||
while (_ptr != _end && IsTrimmed(*(_end - 1))) {
|
||||
--_end;
|
||||
}
|
||||
}
|
||||
|
||||
// void BlockParser::checkForElidedSkipBlock() {
|
||||
// if (!_sumFinished || !_rich) {
|
||||
// return;
|
||||
// }
|
||||
// // We could've skipped the final skip block command.
|
||||
// for (; _ptr < _end; ++_ptr) {
|
||||
// if (*_ptr == TextCommand && readSkipBlockCommand()) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
void BlockParser::finalize(const TextParseOptions &options) {
|
||||
auto links = (_maxLinkIndex || _maxShiftedLinkIndex)
|
||||
? &_t->ensureExtended()->links
|
||||
: nullptr;
|
||||
if (links) {
|
||||
links->resize(_maxLinkIndex + _maxShiftedLinkIndex);
|
||||
}
|
||||
auto counterCustomIndex = uint16(0);
|
||||
auto currentIndex = uint16(0); // Current the latest index of _t->_links.
|
||||
struct {
|
||||
uint16 mono = 0;
|
||||
uint16 lnk = 0;
|
||||
} lastHandlerIndex;
|
||||
const auto avoidIntersectionsWithCustom = [&] {
|
||||
while (ranges::contains(_linksIndexes, currentIndex)) {
|
||||
currentIndex++;
|
||||
}
|
||||
};
|
||||
auto isolatedEmojiCount = 0;
|
||||
_t->_hasCustomEmoji = false;
|
||||
_t->_isIsolatedEmoji = true;
|
||||
_t->_isOnlyCustomEmoji = true;
|
||||
_t->_hasNotEmojiAndSpaces = false;
|
||||
auto spacesCheckFrom = uint16(-1);
|
||||
const auto length = int(_tText.size());
|
||||
for (auto &block : _tBlocks) {
|
||||
if (block->type() == TextBlockType::CustomEmoji) {
|
||||
_t->_hasCustomEmoji = true;
|
||||
} else if (block->type() != TextBlockType::Newline
|
||||
&& block->type() != TextBlockType::Skip) {
|
||||
_t->_isOnlyCustomEmoji = false;
|
||||
} else if (block->linkIndex()) {
|
||||
_t->_isOnlyCustomEmoji = _t->_isIsolatedEmoji = false;
|
||||
}
|
||||
if (!_t->_hasNotEmojiAndSpaces) {
|
||||
if (block->type() == TextBlockType::Text) {
|
||||
if (spacesCheckFrom == uint16(-1)) {
|
||||
spacesCheckFrom = block->position();
|
||||
}
|
||||
} else if (spacesCheckFrom != uint16(-1)) {
|
||||
const auto checkTill = block->position();
|
||||
for (auto i = spacesCheckFrom; i != checkTill; ++i) {
|
||||
Assert(i < length);
|
||||
if (!_tText[i].isSpace()) {
|
||||
_t->_hasNotEmojiAndSpaces = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
spacesCheckFrom = uint16(-1);
|
||||
}
|
||||
}
|
||||
if (_t->_isIsolatedEmoji) {
|
||||
if (block->type() == TextBlockType::CustomEmoji
|
||||
|| block->type() == TextBlockType::Emoji) {
|
||||
if (++isolatedEmojiCount > kIsolatedEmojiLimit) {
|
||||
_t->_isIsolatedEmoji = false;
|
||||
}
|
||||
} else if (block->type() != TextBlockType::Skip) {
|
||||
_t->_isIsolatedEmoji = false;
|
||||
}
|
||||
}
|
||||
if (block->flags() & TextBlockFlag::Spoiler) {
|
||||
auto &spoiler = _t->ensureExtended()->spoiler;
|
||||
if (!spoiler) {
|
||||
spoiler = std::make_unique<SpoilerData>(_context.repaint);
|
||||
}
|
||||
}
|
||||
const auto shiftedIndex = block->linkIndex();
|
||||
auto useCustomIndex = false;
|
||||
if (shiftedIndex <= kStringLinkIndexShift) {
|
||||
if (IsMono(block->flags()) && shiftedIndex) {
|
||||
const auto monoIndex = shiftedIndex;
|
||||
|
||||
if (lastHandlerIndex.mono == monoIndex) {
|
||||
block->setLinkIndex(currentIndex);
|
||||
continue; // Optimization.
|
||||
} else {
|
||||
currentIndex++;
|
||||
}
|
||||
avoidIntersectionsWithCustom();
|
||||
block->setLinkIndex(currentIndex);
|
||||
const auto handler = Integration::Instance().createLinkHandler(
|
||||
_monos[monoIndex - 1],
|
||||
_context);
|
||||
if (!links) {
|
||||
links = &_t->ensureExtended()->links;
|
||||
}
|
||||
links->resize(currentIndex);
|
||||
if (handler) {
|
||||
_t->setLink(currentIndex, handler);
|
||||
}
|
||||
lastHandlerIndex.mono = monoIndex;
|
||||
continue;
|
||||
} else if (shiftedIndex) {
|
||||
useCustomIndex = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const auto usedIndex = [&] {
|
||||
return useCustomIndex
|
||||
? _linksIndexes[counterCustomIndex - 1]
|
||||
: currentIndex;
|
||||
};
|
||||
const auto realIndex = useCustomIndex
|
||||
? shiftedIndex
|
||||
: (shiftedIndex - kStringLinkIndexShift);
|
||||
if (lastHandlerIndex.lnk == realIndex) {
|
||||
block->setLinkIndex(usedIndex());
|
||||
continue; // Optimization.
|
||||
} else {
|
||||
(useCustomIndex ? counterCustomIndex : currentIndex)++;
|
||||
}
|
||||
if (!useCustomIndex) {
|
||||
avoidIntersectionsWithCustom();
|
||||
}
|
||||
block->setLinkIndex(usedIndex());
|
||||
|
||||
if (links) {
|
||||
links->resize(std::max(usedIndex(), uint16(links->size())));
|
||||
}
|
||||
const auto handler = Integration::Instance().createLinkHandler(
|
||||
_links[realIndex - 1],
|
||||
_context);
|
||||
if (handler) {
|
||||
_t->setLink(usedIndex(), handler);
|
||||
}
|
||||
lastHandlerIndex.lnk = realIndex;
|
||||
}
|
||||
const auto hasSpoiler = (_t->_extended && _t->_extended->spoiler);
|
||||
if (!_t->_hasCustomEmoji || hasSpoiler) {
|
||||
_t->_isOnlyCustomEmoji = false;
|
||||
}
|
||||
if (_tBlocks.empty() || hasSpoiler) {
|
||||
_t->_isIsolatedEmoji = false;
|
||||
}
|
||||
if (!_t->_hasNotEmojiAndSpaces && spacesCheckFrom != uint16(-1)) {
|
||||
Assert(spacesCheckFrom < length);
|
||||
for (auto i = spacesCheckFrom; i != length; ++i) {
|
||||
Assert(i < length);
|
||||
if (!_tText[i].isSpace()) {
|
||||
_t->_hasNotEmojiAndSpaces = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_tText.squeeze();
|
||||
_tBlocks.shrink_to_fit();
|
||||
if (const auto extended = _t->_extended.get()) {
|
||||
extended->links.shrink_to_fit();
|
||||
extended->modifications.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
void BlockParser::computeLinkText(
|
||||
const QString &linkData,
|
||||
QString *outLinkText,
|
||||
EntityLinkShown *outShown) {
|
||||
auto url = QUrl(linkData);
|
||||
auto good = QUrl(url.isValid()
|
||||
? url.toEncoded()
|
||||
: QByteArray());
|
||||
auto readable = good.isValid()
|
||||
? good.toDisplayString()
|
||||
: linkData;
|
||||
*outLinkText = _t->_st->font->elided(readable, st::linkCropLimit);
|
||||
*outShown = (*outLinkText == readable)
|
||||
? EntityLinkShown::Full
|
||||
: EntityLinkShown::Partial;
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
131
Telegram/lib_ui/ui/text/text_block_parser.h
Normal file
131
Telegram/lib_ui/ui/text/text_block_parser.h
Normal file
@@ -0,0 +1,131 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_block.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
struct QuoteDetails;
|
||||
|
||||
class BlockParser {
|
||||
public:
|
||||
BlockParser(
|
||||
not_null<String*> string,
|
||||
const TextWithEntities &textWithEntities,
|
||||
const TextParseOptions &options,
|
||||
const MarkedContext &context);
|
||||
|
||||
private:
|
||||
struct ReadyToken {
|
||||
};
|
||||
|
||||
class StartedEntity {
|
||||
public:
|
||||
enum class Type {
|
||||
Flags,
|
||||
Link,
|
||||
IndexedLink,
|
||||
CustomEmoji,
|
||||
Colorized,
|
||||
};
|
||||
|
||||
explicit StartedEntity(TextBlockFlags flags);
|
||||
explicit StartedEntity(uint16 index, Type type);
|
||||
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] std::optional<TextBlockFlags> flags() const;
|
||||
[[nodiscard]] std::optional<uint16> linkIndex() const;
|
||||
[[nodiscard]] std::optional<uint16> colorIndex() const;
|
||||
|
||||
private:
|
||||
const int _value = 0;
|
||||
const Type _type;
|
||||
|
||||
};
|
||||
|
||||
BlockParser(
|
||||
not_null<String*> string,
|
||||
TextWithEntities &&source,
|
||||
const TextParseOptions &options,
|
||||
const MarkedContext &context,
|
||||
ReadyToken);
|
||||
|
||||
void trimSourceRange();
|
||||
void createBlock(int skipBack = 0);
|
||||
void createNewlineBlock(bool fromOriginalText);
|
||||
void ensureAtNewline(QuoteDetails quote);
|
||||
|
||||
// Returns true if at least one entity was parsed in the current position.
|
||||
bool checkEntities();
|
||||
void parseCurrentChar();
|
||||
void parseEmojiFromCurrent();
|
||||
void finalize(const TextParseOptions &options);
|
||||
|
||||
void closeQuote();
|
||||
void finishEntities();
|
||||
void skipPassedEntities();
|
||||
void skipBadEntities();
|
||||
|
||||
bool isInvalidEntity(const EntityInText &entity) const;
|
||||
bool isLinkEntity(const EntityInText &entity) const;
|
||||
|
||||
bool processCustomIndex(uint16 index);
|
||||
|
||||
void parse(const TextParseOptions &options);
|
||||
void computeLinkText(
|
||||
const QString &linkData,
|
||||
QString *outLinkText,
|
||||
EntityLinkShown *outShown);
|
||||
|
||||
const not_null<String*> _t;
|
||||
QString &_tText;
|
||||
std::vector<Block> &_tBlocks;
|
||||
const TextWithEntities _source;
|
||||
const MarkedContext &_context;
|
||||
const QChar * const _start = nullptr;
|
||||
const QChar *_end = nullptr; // mutable, because we trim by decrementing.
|
||||
const QChar *_ptr = nullptr;
|
||||
const EntitiesInText::const_iterator _entitiesEnd;
|
||||
EntitiesInText::const_iterator _waitingEntity;
|
||||
QString _customEmojiData;
|
||||
const bool _multiline = false;
|
||||
|
||||
const bool _checkTilde = false; // do we need a special text block for tilde symbol
|
||||
|
||||
std::vector<uint16> _linksIndexes;
|
||||
|
||||
std::vector<EntityLinkData> _links;
|
||||
std::vector<EntityLinkData> _monos;
|
||||
base::flat_map<
|
||||
const QChar*,
|
||||
std::vector<StartedEntity>> _startedEntities;
|
||||
|
||||
uint16 _maxLinkIndex = 0;
|
||||
uint16 _maxShiftedLinkIndex = 0;
|
||||
|
||||
// current state
|
||||
TextBlockFlags _flags;
|
||||
uint16 _linkIndex = 0;
|
||||
uint16 _colorIndex = 0;
|
||||
uint16 _monoIndex = 0;
|
||||
uint16 _quoteIndex = 0;
|
||||
int _quoteStartPosition = 0;
|
||||
EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero
|
||||
int _blockStart = 0; // offset in result, from which current parsed block is started
|
||||
int _diacritics = 0; // diacritic chars skipped without good char
|
||||
bool _newlineAwaited = false;
|
||||
|
||||
// current char data
|
||||
QChar _ch; // current char (low surrogate, if current char is surrogate pair)
|
||||
int _emojiLookback = 0; // how far behind the current ptr to look for current emoji
|
||||
bool _allowDiacritic = false; // did we add last char to the current block
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
235
Telegram/lib_ui/ui/text/text_custom_emoji.cpp
Normal file
235
Telegram/lib_ui/ui/text/text_custom_emoji.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/emoji_config.h"
|
||||
|
||||
namespace Ui::Emoji {
|
||||
|
||||
int GetCustomSizeNormal() {
|
||||
const auto full = GetSizeNormal();
|
||||
const auto esize = full / style::DevicePixelRatio();
|
||||
return Ui::Text::AdjustCustomEmojiSize(esize);
|
||||
}
|
||||
|
||||
int GetCustomSizeLarge() {
|
||||
const auto full = GetSizeLarge();
|
||||
const auto esize = full / style::DevicePixelRatio();
|
||||
return Ui::Text::AdjustCustomEmojiSize(esize);
|
||||
}
|
||||
|
||||
int GetCustomSkipNormal() {
|
||||
const auto full = GetSizeNormal();
|
||||
const auto esize = full / style::DevicePixelRatio();
|
||||
return (esize - GetCustomSizeNormal()) / 2;
|
||||
}
|
||||
|
||||
int GetCustomSkipLarge() {
|
||||
const auto full = GetSizeLarge();
|
||||
const auto esize = full / style::DevicePixelRatio();
|
||||
return (esize - GetCustomSizeLarge()) / 2;
|
||||
}
|
||||
|
||||
} // namespace Ui::Emoji
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
int AdjustCustomEmojiSize(int emojiSize) {
|
||||
return base::SafeRound(emojiSize * 1.12);
|
||||
}
|
||||
|
||||
ShiftedEmoji::ShiftedEmoji(
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> wrapped,
|
||||
QPoint shift)
|
||||
: _wrapped(std::move(wrapped))
|
||||
, _shift(shift) {
|
||||
}
|
||||
|
||||
int ShiftedEmoji::width() {
|
||||
return _wrapped->width();
|
||||
}
|
||||
|
||||
QString ShiftedEmoji::entityData() {
|
||||
return _wrapped->entityData();
|
||||
}
|
||||
|
||||
void ShiftedEmoji::paint(QPainter &p, const Context &context) {
|
||||
auto copy = context;
|
||||
copy.position += _shift;
|
||||
_wrapped->paint(p, copy);
|
||||
}
|
||||
|
||||
void ShiftedEmoji::unload() {
|
||||
_wrapped->unload();
|
||||
}
|
||||
|
||||
bool ShiftedEmoji::ready() {
|
||||
return _wrapped->ready();
|
||||
}
|
||||
|
||||
bool ShiftedEmoji::readyInDefaultState() {
|
||||
return _wrapped->readyInDefaultState();
|
||||
}
|
||||
|
||||
FirstFrameEmoji::FirstFrameEmoji(std::unique_ptr<CustomEmoji> wrapped)
|
||||
: _wrapped(std::move(wrapped)) {
|
||||
}
|
||||
|
||||
int FirstFrameEmoji::width() {
|
||||
return _wrapped->width();
|
||||
}
|
||||
|
||||
QString FirstFrameEmoji::entityData() {
|
||||
return _wrapped->entityData();
|
||||
}
|
||||
|
||||
void FirstFrameEmoji::paint(QPainter &p, const Context &context) {
|
||||
const auto was = context.internal.forceFirstFrame;
|
||||
context.internal.forceFirstFrame = true;
|
||||
_wrapped->paint(p, context);
|
||||
context.internal.forceFirstFrame = was;
|
||||
}
|
||||
|
||||
void FirstFrameEmoji::unload() {
|
||||
_wrapped->unload();
|
||||
}
|
||||
|
||||
bool FirstFrameEmoji::ready() {
|
||||
return _wrapped->ready();
|
||||
}
|
||||
|
||||
bool FirstFrameEmoji::readyInDefaultState() {
|
||||
return _wrapped->readyInDefaultState();
|
||||
}
|
||||
|
||||
LimitedLoopsEmoji::LimitedLoopsEmoji(
|
||||
std::unique_ptr<CustomEmoji> wrapped,
|
||||
int limit,
|
||||
bool stopOnLast)
|
||||
: _wrapped(std::move(wrapped))
|
||||
, _limit(limit)
|
||||
, _stopOnLast(stopOnLast) {
|
||||
}
|
||||
|
||||
int LimitedLoopsEmoji::width() {
|
||||
return _wrapped->width();
|
||||
}
|
||||
|
||||
QString LimitedLoopsEmoji::entityData() {
|
||||
return _wrapped->entityData();
|
||||
}
|
||||
|
||||
void LimitedLoopsEmoji::paint(QPainter &p, const Context &context) {
|
||||
if (_played < _limit) {
|
||||
if (_wrapped->readyInDefaultState()) {
|
||||
if (_inLoop) {
|
||||
_inLoop = false;
|
||||
++_played;
|
||||
}
|
||||
} else if (_wrapped->ready()) {
|
||||
_inLoop = true;
|
||||
}
|
||||
}
|
||||
if (_played == _limit) {
|
||||
const auto wasFirst = context.internal.forceFirstFrame;
|
||||
const auto wasLast = context.internal.forceLastFrame;
|
||||
(_stopOnLast
|
||||
? context.internal.forceLastFrame
|
||||
: context.internal.forceFirstFrame) = true;
|
||||
_wrapped->paint(p, context);
|
||||
context.internal.forceFirstFrame = wasFirst;
|
||||
context.internal.forceLastFrame = wasLast;
|
||||
} else if (_played + 1 == _limit && _inLoop && _stopOnLast) {
|
||||
const auto wasLast = context.internal.overrideFirstWithLastFrame;
|
||||
context.internal.overrideFirstWithLastFrame = true;
|
||||
_wrapped->paint(p, context);
|
||||
context.internal.overrideFirstWithLastFrame = wasLast;
|
||||
} else {
|
||||
_wrapped->paint(p, context);
|
||||
}
|
||||
}
|
||||
|
||||
void LimitedLoopsEmoji::unload() {
|
||||
_wrapped->unload();
|
||||
_inLoop = false;
|
||||
_played = 0;
|
||||
}
|
||||
|
||||
bool LimitedLoopsEmoji::ready() {
|
||||
return _wrapped->ready();
|
||||
}
|
||||
|
||||
bool LimitedLoopsEmoji::readyInDefaultState() {
|
||||
return _wrapped->readyInDefaultState();
|
||||
}
|
||||
|
||||
std::unique_ptr<CustomEmoji> MakeCustomEmoji(
|
||||
QStringView data,
|
||||
const MarkedContext &context) {
|
||||
if (auto simple = TryMakeSimpleEmoji(data)) {
|
||||
return simple;
|
||||
} else if (const auto &factory = context.customEmojiFactory) {
|
||||
return factory(data, context);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PaletteDependentCustomEmoji::PaletteDependentCustomEmoji(
|
||||
Fn<QImage()> factory,
|
||||
QString entity,
|
||||
QMargins padding)
|
||||
: _factory(std::move(factory))
|
||||
, _entity(std::move(entity))
|
||||
, _padding(padding) {
|
||||
}
|
||||
|
||||
int PaletteDependentCustomEmoji::width() {
|
||||
if (_frame.isNull()) {
|
||||
validateFrame();
|
||||
}
|
||||
return _padding.left()
|
||||
+ (_frame.width() / style::DevicePixelRatio())
|
||||
+ _padding.right();
|
||||
}
|
||||
|
||||
QString PaletteDependentCustomEmoji::entityData() {
|
||||
return _entity;
|
||||
}
|
||||
|
||||
void PaletteDependentCustomEmoji::paint(
|
||||
QPainter &p,
|
||||
const Context &context) {
|
||||
validateFrame();
|
||||
p.drawImage(
|
||||
context.position + QPoint(_padding.left(), _padding.top()),
|
||||
_frame);
|
||||
}
|
||||
|
||||
void PaletteDependentCustomEmoji::unload() {
|
||||
_frame = QImage();
|
||||
}
|
||||
|
||||
bool PaletteDependentCustomEmoji::ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PaletteDependentCustomEmoji::readyInDefaultState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PaletteDependentCustomEmoji::validateFrame() {
|
||||
const auto version = style::PaletteVersion();
|
||||
if (_frame.isNull() || _paletteVersion != version) {
|
||||
_paletteVersion = version;
|
||||
_frame = _factory();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
152
Telegram/lib_ui/ui/text/text_custom_emoji.h
Normal file
152
Telegram/lib_ui/ui/text/text_custom_emoji.h
Normal file
@@ -0,0 +1,152 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <QtGui/QColor>
|
||||
#include <QtGui/QImage>
|
||||
#include <QtCore/QSize>
|
||||
#include <QtCore/QPoint>
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
#include <any>
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace Ui::Emoji {
|
||||
|
||||
[[nodiscard]] int GetCustomSizeNormal();
|
||||
[[nodiscard]] int GetCustomSkipNormal();
|
||||
[[nodiscard]] int GetCustomSizeLarge();
|
||||
[[nodiscard]] int GetCustomSkipLarge();
|
||||
|
||||
} // namespace Ui::Emoji
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
struct MarkedContext;
|
||||
|
||||
[[nodiscard]] int AdjustCustomEmojiSize(int emojiSize);
|
||||
|
||||
struct CustomEmojiPaintContext {
|
||||
required<QColor> textColor;
|
||||
QSize size; // Required only when scaled = true, for path scaling.
|
||||
crl::time now = 0;
|
||||
float64 scale = 0.;
|
||||
QPoint position;
|
||||
bool paused = false;
|
||||
bool scaled = false;
|
||||
|
||||
mutable struct {
|
||||
bool colorized = false;
|
||||
bool forceFirstFrame = false;
|
||||
bool forceLastFrame = false;
|
||||
bool overrideFirstWithLastFrame = false;
|
||||
} internal;
|
||||
};
|
||||
|
||||
class CustomEmoji {
|
||||
public:
|
||||
virtual ~CustomEmoji() = default;
|
||||
|
||||
[[nodiscard]] virtual int width() = 0;
|
||||
[[nodiscard]] virtual QString entityData() = 0;
|
||||
|
||||
using Context = CustomEmojiPaintContext;
|
||||
virtual void paint(QPainter &p, const Context &context) = 0;
|
||||
virtual void unload() = 0;
|
||||
[[nodiscard]] virtual bool ready() = 0;
|
||||
[[nodiscard]] virtual bool readyInDefaultState() = 0;
|
||||
|
||||
};
|
||||
|
||||
class ShiftedEmoji final : public CustomEmoji {
|
||||
public:
|
||||
ShiftedEmoji(std::unique_ptr<CustomEmoji> wrapped, QPoint shift);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
||||
const QPoint _shift;
|
||||
|
||||
};
|
||||
|
||||
class FirstFrameEmoji final : public CustomEmoji {
|
||||
public:
|
||||
explicit FirstFrameEmoji(std::unique_ptr<CustomEmoji> wrapped);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
||||
|
||||
};
|
||||
|
||||
class LimitedLoopsEmoji final : public CustomEmoji {
|
||||
public:
|
||||
LimitedLoopsEmoji(
|
||||
std::unique_ptr<CustomEmoji> wrapped,
|
||||
int limit,
|
||||
bool stopOnLast = false);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
||||
const int _limit = 0;
|
||||
int _played = 0;
|
||||
bool _inLoop = false;
|
||||
bool _stopOnLast = false;
|
||||
|
||||
};
|
||||
|
||||
class PaletteDependentCustomEmoji final : public CustomEmoji {
|
||||
public:
|
||||
PaletteDependentCustomEmoji(
|
||||
Fn<QImage()> factory,
|
||||
QString entity,
|
||||
QMargins padding = {});
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
void validateFrame();
|
||||
|
||||
Fn<QImage()> _factory;
|
||||
QString _entity;
|
||||
QMargins _padding;
|
||||
QImage _frame;
|
||||
int _paletteVersion = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<CustomEmoji> MakeCustomEmoji(
|
||||
QStringView data,
|
||||
const MarkedContext &context);
|
||||
|
||||
} // namespace Ui::Text
|
||||
2435
Telegram/lib_ui/ui/text/text_entity.cpp
Normal file
2435
Telegram/lib_ui/ui/text/text_entity.cpp
Normal file
File diff suppressed because it is too large
Load Diff
400
Telegram/lib_ui/ui/text/text_entity.h
Normal file
400
Telegram/lib_ui/ui/text/text_entity.h
Normal file
@@ -0,0 +1,400 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/qt/qt_compare.h"
|
||||
#include "base/basic_types.h"
|
||||
#include "base/algorithm.h"
|
||||
|
||||
#include <QtCore/QList>
|
||||
#include <QtCore/QVector>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
struct TextWithEntities;
|
||||
|
||||
namespace style {
|
||||
struct IconEmoji;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
[[nodiscard]] TextWithEntities IconEmoji(
|
||||
not_null<const style::IconEmoji*> emoji,
|
||||
QString text);
|
||||
} // namespace Ui::Text
|
||||
|
||||
enum class EntityType : uchar {
|
||||
Invalid = 0,
|
||||
|
||||
Url,
|
||||
CustomUrl,
|
||||
Email,
|
||||
Hashtag,
|
||||
Cashtag,
|
||||
Mention,
|
||||
MentionName,
|
||||
CustomEmoji,
|
||||
BotCommand,
|
||||
MediaTimestamp,
|
||||
Colorized, // Senders in chat list, attachments in chat list, etc.
|
||||
Phone,
|
||||
BankCard,
|
||||
|
||||
Bold,
|
||||
Semibold,
|
||||
Italic,
|
||||
Underline,
|
||||
StrikeOut,
|
||||
Code, // inline
|
||||
Pre, // block
|
||||
Blockquote,
|
||||
Spoiler,
|
||||
};
|
||||
|
||||
enum class EntityLinkShown : uchar {
|
||||
Full,
|
||||
Partial,
|
||||
};
|
||||
|
||||
struct EntityLinkData {
|
||||
QString text;
|
||||
QString data;
|
||||
EntityType type = EntityType::Invalid;
|
||||
EntityLinkShown shown = EntityLinkShown::Full;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const EntityLinkData &,
|
||||
const EntityLinkData &) = default;
|
||||
friend inline bool operator==(
|
||||
const EntityLinkData &,
|
||||
const EntityLinkData &) = default;
|
||||
};
|
||||
|
||||
class EntityInText;
|
||||
using EntitiesInText = QVector<EntityInText>;
|
||||
|
||||
class EntityInText {
|
||||
public:
|
||||
EntityInText(
|
||||
EntityType type,
|
||||
int offset,
|
||||
int length,
|
||||
const QString &data = QString());
|
||||
|
||||
[[nodiscard]] EntityType type() const {
|
||||
return _type;
|
||||
}
|
||||
[[nodiscard]] int offset() const {
|
||||
return _offset;
|
||||
}
|
||||
[[nodiscard]] int length() const {
|
||||
return _length;
|
||||
}
|
||||
[[nodiscard]] QString data() const {
|
||||
return _data;
|
||||
}
|
||||
|
||||
void extendToLeft(int extent) {
|
||||
_offset -= extent;
|
||||
_length += extent;
|
||||
}
|
||||
void shrinkFromRight(int shrink) {
|
||||
_length -= shrink;
|
||||
}
|
||||
void shiftLeft(int shift) {
|
||||
_offset -= shift;
|
||||
if (_offset < 0) {
|
||||
_length += _offset;
|
||||
_offset = 0;
|
||||
if (_length < 0) {
|
||||
_length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
void shiftRight(int shift) {
|
||||
_offset += shift;
|
||||
}
|
||||
void updateTextEnd(int textEnd) {
|
||||
if (_offset > textEnd) {
|
||||
_offset = textEnd;
|
||||
_length = 0;
|
||||
} else if (_offset + _length > textEnd) {
|
||||
_length = textEnd - _offset;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] static int FirstMonospaceOffset(
|
||||
const EntitiesInText &entities,
|
||||
int textLength);
|
||||
|
||||
explicit operator bool() const {
|
||||
return type() != EntityType::Invalid;
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const EntityInText &,
|
||||
const EntityInText &) = default;
|
||||
friend inline bool operator==(
|
||||
const EntityInText &,
|
||||
const EntityInText &) = default;
|
||||
|
||||
private:
|
||||
EntityType _type = EntityType::Invalid;
|
||||
int _offset = 0;
|
||||
int _length = 0;
|
||||
QString _data;
|
||||
|
||||
};
|
||||
|
||||
struct TextWithEntities {
|
||||
QString text;
|
||||
EntitiesInText entities;
|
||||
|
||||
bool empty() const {
|
||||
return text.isEmpty();
|
||||
}
|
||||
|
||||
void reserve(int size, int entitiesCount = 0) {
|
||||
text.reserve(size);
|
||||
entities.reserve(entitiesCount);
|
||||
}
|
||||
|
||||
TextWithEntities &append(TextWithEntities &&other) {
|
||||
const auto shift = text.size();
|
||||
for (auto &entity : other.entities) {
|
||||
entity.shiftRight(shift);
|
||||
}
|
||||
text.append(other.text);
|
||||
entities.append(other.entities);
|
||||
return *this;
|
||||
}
|
||||
TextWithEntities &append(const TextWithEntities &other) {
|
||||
const auto shift = text.size();
|
||||
text.append(other.text);
|
||||
entities.reserve(entities.size() + other.entities.size());
|
||||
for (auto entity : other.entities) {
|
||||
entity.shiftRight(shift);
|
||||
entities.append(entity);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
TextWithEntities &append(const QString &other) {
|
||||
text.append(other);
|
||||
return *this;
|
||||
}
|
||||
TextWithEntities &append(QLatin1String other) {
|
||||
text.append(other);
|
||||
return *this;
|
||||
}
|
||||
TextWithEntities &append(QChar other) {
|
||||
text.append(other);
|
||||
return *this;
|
||||
}
|
||||
TextWithEntities &append(
|
||||
const style::IconEmoji &icon,
|
||||
const QString &text = QString()) {
|
||||
return append(Ui::Text::IconEmoji(&icon, text));
|
||||
}
|
||||
|
||||
static TextWithEntities Simple(const QString &simple) {
|
||||
auto result = TextWithEntities();
|
||||
result.text = simple;
|
||||
return result;
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const TextWithEntities &,
|
||||
const TextWithEntities &) = default;
|
||||
friend inline bool operator==(
|
||||
const TextWithEntities &,
|
||||
const TextWithEntities &) = default;
|
||||
};
|
||||
|
||||
struct TextForMimeData {
|
||||
QString expanded;
|
||||
TextWithEntities rich;
|
||||
|
||||
bool empty() const {
|
||||
return expanded.isEmpty();
|
||||
}
|
||||
|
||||
void reserve(int size, int entitiesCount = 0) {
|
||||
expanded.reserve(size);
|
||||
rich.reserve(size, entitiesCount);
|
||||
}
|
||||
TextForMimeData &append(TextForMimeData &&other) {
|
||||
expanded.append(other.expanded);
|
||||
rich.append(std::move(other.rich));
|
||||
return *this;
|
||||
}
|
||||
TextForMimeData &append(TextWithEntities &&other) {
|
||||
expanded.append(other.text);
|
||||
rich.append(std::move(other));
|
||||
return *this;
|
||||
}
|
||||
TextForMimeData &append(const QString &other) {
|
||||
expanded.append(other);
|
||||
rich.append(other);
|
||||
return *this;
|
||||
}
|
||||
TextForMimeData &append(QLatin1String other) {
|
||||
expanded.append(other);
|
||||
rich.append(other);
|
||||
return *this;
|
||||
}
|
||||
TextForMimeData &append(QChar other) {
|
||||
expanded.append(other);
|
||||
rich.append(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
static TextForMimeData WithExpandedLinks(const TextWithEntities &text);
|
||||
static TextForMimeData Rich(TextWithEntities &&rich) {
|
||||
auto result = TextForMimeData();
|
||||
result.expanded = rich.text;
|
||||
result.rich = std::move(rich);
|
||||
return result;
|
||||
}
|
||||
static TextForMimeData Simple(const QString &simple) {
|
||||
auto result = TextForMimeData();
|
||||
result.expanded = result.rich.text = simple;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
enum {
|
||||
TextParseMultiline = 0x001,
|
||||
TextParseLinks = 0x002,
|
||||
TextParseMentions = 0x004,
|
||||
TextParseHashtags = 0x008,
|
||||
TextParseBotCommands = 0x010,
|
||||
TextParseMarkdown = 0x020,
|
||||
TextParseColorized = 0x040,
|
||||
};
|
||||
|
||||
struct TextWithTags {
|
||||
struct Tag {
|
||||
int offset = 0;
|
||||
int length = 0;
|
||||
QString id;
|
||||
|
||||
friend inline auto operator<=>(const Tag &, const Tag &) = default;
|
||||
friend inline bool operator==(const Tag &, const Tag &) = default;
|
||||
};
|
||||
using Tags = QVector<Tag>;
|
||||
|
||||
QString text;
|
||||
Tags tags;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return text.isEmpty();
|
||||
}
|
||||
friend inline auto operator<=>(
|
||||
const TextWithTags &,
|
||||
const TextWithTags &) = default;
|
||||
friend inline bool operator==(
|
||||
const TextWithTags &,
|
||||
const TextWithTags &) = default;
|
||||
};
|
||||
|
||||
// Parsing helpers.
|
||||
|
||||
namespace TextUtilities {
|
||||
|
||||
bool IsValidProtocol(const QString &protocol);
|
||||
bool IsValidTopDomain(const QString &domain);
|
||||
|
||||
const QRegularExpression &RegExpMailNameAtEnd();
|
||||
const QRegularExpression &RegExpHashtag(bool allowWithMention);
|
||||
const QRegularExpression &RegExpHashtagExclude();
|
||||
const QRegularExpression &RegExpMention();
|
||||
const QRegularExpression &RegExpBotCommand();
|
||||
const QRegularExpression &RegExpDigitsExclude();
|
||||
QString MarkdownBoldGoodBefore();
|
||||
QString MarkdownBoldBadAfter();
|
||||
QString MarkdownItalicGoodBefore();
|
||||
QString MarkdownItalicBadAfter();
|
||||
QString MarkdownStrikeOutGoodBefore();
|
||||
QString MarkdownStrikeOutBadAfter();
|
||||
QString MarkdownCodeGoodBefore();
|
||||
QString MarkdownCodeBadAfter();
|
||||
QString MarkdownPreGoodBefore();
|
||||
QString MarkdownPreBadAfter();
|
||||
QString MarkdownSpoilerGoodBefore();
|
||||
QString MarkdownSpoilerBadAfter();
|
||||
|
||||
// Text preprocess.
|
||||
QString EscapeForRichParsing(const QString &text);
|
||||
QString SingleLine(const QString &text);
|
||||
TextWithEntities SingleLine(const TextWithEntities &text);
|
||||
QString RemoveAccents(const QString &text);
|
||||
QString RemoveEmoji(const QString &text);
|
||||
QString NameSortKey(const QString &text);
|
||||
QStringList PrepareSearchWords(const QString &query, const QRegularExpression *SplitterOverride = nullptr);
|
||||
bool CutPart(TextWithEntities &sending, TextWithEntities &left, int limit);
|
||||
|
||||
struct MentionNameFields {
|
||||
uint64 selfId = 0;
|
||||
uint64 userId = 0;
|
||||
uint64 accessHash = 0;
|
||||
};
|
||||
[[nodiscard]] MentionNameFields MentionNameDataToFields(QStringView data);
|
||||
[[nodiscard]] QString MentionNameDataFromFields(
|
||||
const MentionNameFields &fields);
|
||||
|
||||
// New entities are added to the ones that are already in result.
|
||||
// Changes text if (flags & TextParseMarkdown).
|
||||
TextWithEntities ParseEntities(const QString &text, int32 flags);
|
||||
void ParseEntities(TextWithEntities &result, int32 flags);
|
||||
|
||||
void PrepareForSending(TextWithEntities &result, int32 flags);
|
||||
void Trim(TextWithEntities &result);
|
||||
|
||||
enum class PrepareTextOption {
|
||||
IgnoreLinks,
|
||||
CheckLinks,
|
||||
};
|
||||
inline QString PrepareForSending(const QString &text, PrepareTextOption option = PrepareTextOption::IgnoreLinks) {
|
||||
auto result = TextWithEntities { text };
|
||||
auto prepareFlags = (option == PrepareTextOption::CheckLinks) ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0;
|
||||
PrepareForSending(result, prepareFlags);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
// Replace bad symbols with space and remove '\r'.
|
||||
void ApplyServerCleaning(TextWithEntities &result);
|
||||
|
||||
[[nodiscard]] int SerializeTagsSize(const TextWithTags::Tags &tags);
|
||||
[[nodiscard]] QByteArray SerializeTags(const TextWithTags::Tags &tags);
|
||||
[[nodiscard]] TextWithTags::Tags DeserializeTags(
|
||||
QByteArray data,
|
||||
int textLength);
|
||||
[[nodiscard]] QString TagsMimeType();
|
||||
[[nodiscard]] QString TagsTextMimeType();
|
||||
|
||||
inline const auto kMentionTagStart = qstr("mention://");
|
||||
|
||||
[[nodiscard]] bool IsMentionLink(QStringView link);
|
||||
[[nodiscard]] QString MentionEntityData(QStringView link);
|
||||
[[nodiscard]] bool IsSeparateTag(QStringView tag);
|
||||
[[nodiscard]] QString JoinTag(const QList<QStringView> &list);
|
||||
[[nodiscard]] QList<QStringView> SplitTags(QStringView tag);
|
||||
[[nodiscard]] QString TagWithRemoved(
|
||||
const QString &tag,
|
||||
const QString &removed);
|
||||
[[nodiscard]] QString TagWithAdded(const QString &tag, const QString &added);
|
||||
[[nodiscard]] TextWithTags::Tags SimplifyTags(TextWithTags::Tags tags);
|
||||
|
||||
EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags);
|
||||
TextWithTags::Tags ConvertEntitiesToTextTags(
|
||||
const EntitiesInText &entities);
|
||||
std::unique_ptr<QMimeData> MimeDataFromText(const TextForMimeData &text);
|
||||
std::unique_ptr<QMimeData> MimeDataFromText(TextWithTags &&text);
|
||||
void SetClipboardText(
|
||||
const TextForMimeData &text,
|
||||
QClipboard::Mode mode = QClipboard::Clipboard);
|
||||
|
||||
} // namespace TextUtilities
|
||||
97
Telegram/lib_ui/ui/text/text_extended_data.cpp
Normal file
97
Telegram/lib_ui/ui/text/text_extended_data.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_extended_data.h"
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/integration.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
SpoilerClickHandler::SpoilerClickHandler(
|
||||
not_null<String*> text,
|
||||
Fn<bool(const ClickContext&)> filter)
|
||||
: _text(text)
|
||||
, _filter(std::move(filter)) {
|
||||
}
|
||||
|
||||
not_null<String*> SpoilerClickHandler::text() const {
|
||||
return _text;
|
||||
}
|
||||
|
||||
void SpoilerClickHandler::setText(not_null<String*> text) {
|
||||
_text = text;
|
||||
}
|
||||
|
||||
void SpoilerClickHandler::onClick(ClickContext context) const {
|
||||
if (_filter && !_filter(context)) {
|
||||
return;
|
||||
}
|
||||
_text->setSpoilerRevealed(true, anim::type::normal);
|
||||
}
|
||||
|
||||
PreClickHandler::PreClickHandler(
|
||||
not_null<String*> text,
|
||||
uint16 offset,
|
||||
uint16 length)
|
||||
: _text(text)
|
||||
, _offset(offset)
|
||||
, _length(length) {
|
||||
}
|
||||
|
||||
not_null<String*> PreClickHandler::text() const {
|
||||
return _text;
|
||||
}
|
||||
|
||||
void PreClickHandler::setText(not_null<String*> text) {
|
||||
_text = text;
|
||||
}
|
||||
|
||||
void PreClickHandler::onClick(ClickContext context) const {
|
||||
if (context.button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const auto till = uint16(_offset + _length);
|
||||
auto text = _text->toTextForMimeData({ _offset, till });
|
||||
if (text.empty()) {
|
||||
return;
|
||||
} else if (!text.rich.text.endsWith('\n')) {
|
||||
text.rich.text.append('\n');
|
||||
}
|
||||
if (!text.expanded.endsWith('\n')) {
|
||||
text.expanded.append('\n');
|
||||
}
|
||||
if (Integration::Instance().copyPreOnClick(context.other)) {
|
||||
TextUtilities::SetClipboardText(std::move(text));
|
||||
}
|
||||
}
|
||||
|
||||
BlockquoteClickHandler::BlockquoteClickHandler(
|
||||
not_null<String*> text,
|
||||
int quoteIndex)
|
||||
: _text(text)
|
||||
, _quoteIndex(quoteIndex) {
|
||||
}
|
||||
|
||||
not_null<String*> BlockquoteClickHandler::text() const {
|
||||
return _text;
|
||||
}
|
||||
|
||||
void BlockquoteClickHandler::setText(not_null<String*> text) {
|
||||
_text = text;
|
||||
}
|
||||
|
||||
void BlockquoteClickHandler::onClick(ClickContext context) const {
|
||||
if (context.button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
_text->setBlockquoteExpanded(
|
||||
_quoteIndex,
|
||||
!_text->blockquoteExpanded(_quoteIndex));
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
|
||||
103
Telegram/lib_ui/ui/text/text_extended_data.h
Normal file
103
Telegram/lib_ui/ui/text/text_extended_data.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/click_handler.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
struct Modification;
|
||||
class String;
|
||||
|
||||
class SpoilerClickHandler final : public ClickHandler {
|
||||
public:
|
||||
SpoilerClickHandler(
|
||||
not_null<String*> text,
|
||||
Fn<bool(const ClickContext&)> filter);
|
||||
|
||||
[[nodiscard]] not_null<String*> text() const;
|
||||
void setText(not_null<String*> text);
|
||||
|
||||
void onClick(ClickContext context) const override;
|
||||
|
||||
private:
|
||||
not_null<String*> _text;
|
||||
const Fn<bool(const ClickContext &)> _filter;
|
||||
|
||||
};
|
||||
|
||||
class PreClickHandler final : public ClickHandler {
|
||||
public:
|
||||
PreClickHandler(not_null<String*> text, uint16 offset, uint16 length);
|
||||
|
||||
[[nodiscard]] not_null<String*> text() const;
|
||||
void setText(not_null<String*> text);
|
||||
|
||||
void onClick(ClickContext context) const override;
|
||||
|
||||
private:
|
||||
not_null<String*> _text;
|
||||
uint16 _offset = 0;
|
||||
uint16 _length = 0;
|
||||
|
||||
};
|
||||
|
||||
class BlockquoteClickHandler final : public ClickHandler {
|
||||
public:
|
||||
BlockquoteClickHandler(not_null<String*> text, int quoteIndex);
|
||||
|
||||
[[nodiscard]] not_null<String*> text() const;
|
||||
void setText(not_null<String*> text);
|
||||
|
||||
void onClick(ClickContext context) const override;
|
||||
|
||||
private:
|
||||
not_null<String*> _text;
|
||||
uint16 _quoteIndex = 0;
|
||||
|
||||
};
|
||||
|
||||
struct SpoilerData {
|
||||
explicit SpoilerData(Fn<void()> repaint)
|
||||
: animation(std::move(repaint)) {
|
||||
}
|
||||
|
||||
SpoilerAnimation animation;
|
||||
std::shared_ptr<SpoilerClickHandler> link;
|
||||
Animations::Simple revealAnimation;
|
||||
bool revealed = false;
|
||||
};
|
||||
|
||||
struct QuoteDetails {
|
||||
QString language;
|
||||
std::shared_ptr<PreClickHandler> copy;
|
||||
std::shared_ptr<BlockquoteClickHandler> toggle;
|
||||
int copyWidth = 0;
|
||||
int maxWidth = 0;
|
||||
int minHeight = 0;
|
||||
int scrollLeft = 0;
|
||||
bool blockquote = false;
|
||||
bool collapsed = false;
|
||||
bool expanded = false;
|
||||
bool pre = false;
|
||||
};
|
||||
|
||||
struct QuotesData {
|
||||
std::vector<QuoteDetails> list;
|
||||
Fn<void(int index, bool expanded)> expandCallback;
|
||||
};
|
||||
|
||||
struct ExtendedData {
|
||||
std::vector<ClickHandlerPtr> links;
|
||||
std::unique_ptr<QuotesData> quotes;
|
||||
std::unique_ptr<SpoilerData> spoiler;
|
||||
std::vector<Modification> modifications;
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
62
Telegram/lib_ui/ui/text/text_isolated_emoji.h
Normal file
62
Telegram/lib_ui/ui/text/text_isolated_emoji.h
Normal file
@@ -0,0 +1,62 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/variant.h"
|
||||
#include "ui/emoji_config.h"
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
inline constexpr auto kIsolatedEmojiLimit = 3;
|
||||
|
||||
struct IsolatedEmoji {
|
||||
using Item = std::variant<v::null_t, EmojiPtr, QString>;
|
||||
using Items = std::array<Item, kIsolatedEmojiLimit>;
|
||||
Items items = {};
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return v::is_null(items[0]);
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
[[nodiscard]] bool operator<(const IsolatedEmoji &other) const {
|
||||
return items < other.items;
|
||||
}
|
||||
[[nodiscard]] bool operator==(const IsolatedEmoji &other) const {
|
||||
return items == other.items;
|
||||
}
|
||||
[[nodiscard]] bool operator>(const IsolatedEmoji &other) const {
|
||||
return other < *this;
|
||||
}
|
||||
[[nodiscard]] bool operator<=(const IsolatedEmoji &other) const {
|
||||
return !(other < *this);
|
||||
}
|
||||
[[nodiscard]] bool operator>=(const IsolatedEmoji &other) const {
|
||||
return !(*this < other);
|
||||
}
|
||||
[[nodiscard]] bool operator!=(const IsolatedEmoji &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
struct OnlyCustomEmoji {
|
||||
struct Item {
|
||||
QString entityData;
|
||||
int spacesBefore = 0;
|
||||
};
|
||||
std::vector<std::vector<Item>> lines;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return lines.empty();
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
1647
Telegram/lib_ui/ui/text/text_renderer.cpp
Normal file
1647
Telegram/lib_ui/ui/text/text_renderer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
220
Telegram/lib_ui/ui/text/text_renderer.h
Normal file
220
Telegram/lib_ui/ui/text/text_renderer.h
Normal file
@@ -0,0 +1,220 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_block.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
|
||||
#include <private/qtextengine_p.h>
|
||||
|
||||
class QTextItemInt;
|
||||
struct QScriptAnalysis;
|
||||
struct QScriptLine;
|
||||
struct QScriptItem;
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
inline constexpr auto kQuoteCollapsedLines = 3;
|
||||
|
||||
class AbstractBlock;
|
||||
|
||||
struct FixedRange {
|
||||
QFixed from;
|
||||
QFixed till;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return (till <= from);
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] FixedRange Intersected(FixedRange a, FixedRange b);
|
||||
[[nodiscard]] bool Intersects(FixedRange a, FixedRange b);
|
||||
[[nodiscard]] FixedRange United(FixedRange a, FixedRange b);
|
||||
[[nodiscard]] bool Distinct(FixedRange a, FixedRange b);
|
||||
|
||||
class Renderer final {
|
||||
public:
|
||||
explicit Renderer(const Ui::Text::String &t);
|
||||
~Renderer();
|
||||
|
||||
void draw(QPainter &p, const PaintContext &context);
|
||||
[[nodiscard]] StateResult getState(
|
||||
QPoint point,
|
||||
GeometryDescriptor geometry,
|
||||
StateRequest request);
|
||||
|
||||
private:
|
||||
static constexpr int kSpoilersRectsSize = 512;
|
||||
|
||||
struct BidiControl;
|
||||
|
||||
void enumerate();
|
||||
|
||||
[[nodiscard]] crl::time now() const;
|
||||
void initNextParagraph(
|
||||
Blocks::const_iterator i,
|
||||
int16 paragraphIndex,
|
||||
Qt::LayoutDirection direction);
|
||||
void initNextLine();
|
||||
void initParagraphBidi();
|
||||
bool drawLine(
|
||||
uint16 lineEnd,
|
||||
Blocks::const_iterator blocksEnd);
|
||||
[[nodiscard]] FixedRange findSelectEmojiRange(
|
||||
const QScriptItem &si,
|
||||
std::vector<Block>::const_iterator blockIt,
|
||||
QFixed x,
|
||||
TextSelection selection) const;
|
||||
[[nodiscard]] FixedRange findSelectTextRange(
|
||||
const QScriptItem &si,
|
||||
int itemStart,
|
||||
int itemEnd,
|
||||
QFixed x,
|
||||
QFixed itemWidth,
|
||||
const QTextItemInt &gf,
|
||||
TextSelection selection) const;
|
||||
void fillSelectRange(FixedRange range);
|
||||
void pushHighlightRange(FixedRange range);
|
||||
void pushSpoilerRange(
|
||||
FixedRange range,
|
||||
FixedRange selected,
|
||||
bool isElidedItem,
|
||||
bool rtl);
|
||||
void fillRectsFromRanges();
|
||||
void fillRectsFromRanges(
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
|
||||
QVarLengthArray<FixedRange> &ranges);
|
||||
void paintSpoilerRects();
|
||||
void paintSpoilerRects(
|
||||
const QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
|
||||
const style::color &color,
|
||||
int index);
|
||||
void composeHighlightPath();
|
||||
[[nodiscard]] const AbstractBlock *markBlockForElisionGetEnd(
|
||||
int blockIndex);
|
||||
void setElideBidi(int elideStart);
|
||||
void prepareElidedLine(
|
||||
QString &lineText,
|
||||
int lineStart,
|
||||
int &lineLength,
|
||||
const AbstractBlock *&endBlock,
|
||||
int recursed = 0);
|
||||
void prepareElisionAt(
|
||||
QString &lineText,
|
||||
int &lineLength,
|
||||
uint16 position);
|
||||
void restoreAfterElided();
|
||||
|
||||
void fillParagraphBg(int paddingBottom);
|
||||
|
||||
void applyBlockProperties(
|
||||
QTextEngine &e,
|
||||
not_null<const AbstractBlock*> block);
|
||||
[[nodiscard]] ClickHandlerPtr lookupLink(
|
||||
const AbstractBlock *block) const;
|
||||
|
||||
const String *_t = nullptr;
|
||||
GeometryDescriptor _geometry;
|
||||
SpoilerData *_spoiler = nullptr;
|
||||
SpoilerMessCache *_spoilerCache = nullptr;
|
||||
QPainter *_p = nullptr;
|
||||
const style::TextPalette *_palette = nullptr;
|
||||
std::span<SpecialColor> _colors;
|
||||
bool _pausedEmoji = false;
|
||||
bool _pausedSpoiler = false;
|
||||
style::align _align = style::al_topleft;
|
||||
QPen _originalPen;
|
||||
QPen _originalPenSelected;
|
||||
QPen _quoteLinkPenOverride;
|
||||
const QPen *_currentPen = nullptr;
|
||||
const QPen *_currentPenSelected = nullptr;
|
||||
struct {
|
||||
bool spoiler = false;
|
||||
bool selectActiveBlock = false; // For monospace.
|
||||
} _background;
|
||||
int _yFrom = 0;
|
||||
int _yTo = 0;
|
||||
TextSelection _selection = { 0, 0 };
|
||||
bool _fullWidthSelection = true;
|
||||
HighlightInfoRequest *_highlight = nullptr;
|
||||
const QChar *_str = nullptr;
|
||||
mutable crl::time _cachedNow = 0;
|
||||
float64 _spoilerOpacity = 0.;
|
||||
QVarLengthArray<FixedRange> _spoilerRanges;
|
||||
QVarLengthArray<FixedRange> _spoilerSelectedRanges;
|
||||
QVarLengthArray<FixedRange> _highlightRanges;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerRects;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerSelectedRects;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _highlightRects;
|
||||
|
||||
std::optional<CustomEmoji::Context> _customEmojiContext;
|
||||
int _customEmojiSkip = 0;
|
||||
int _indexOfElidedBlock = -1; // For spoilers.
|
||||
|
||||
// current paragraph data
|
||||
Blocks::const_iterator _paragraphStartBlock;
|
||||
Qt::LayoutDirection _paragraphDirection = Qt::LayoutDirectionAuto;
|
||||
int _paragraphStart = 0;
|
||||
int _paragraphLength = 0;
|
||||
QVarLengthArray<QScriptAnalysis, 4096> _paragraphAnalysis;
|
||||
|
||||
// current quote data
|
||||
QuoteDetails *_quote = nullptr;
|
||||
Qt::LayoutDirection _quoteDirection = Qt::LayoutDirectionAuto;
|
||||
int _quoteShift = 0;
|
||||
int _quoteIndex = 0;
|
||||
QMargins _quotePadding;
|
||||
int _quoteLinesLeft = -1;
|
||||
int _quoteTop = 0;
|
||||
int _quoteLineTop = 0;
|
||||
QuotePaintCache *_quotePreCache = nullptr;
|
||||
QuotePaintCache *_quoteBlockquoteCache = nullptr;
|
||||
bool _quotePreValid = false;
|
||||
bool _quoteBlockquoteValid = false;
|
||||
|
||||
ClickHandlerPtr _quoteExpandLink;
|
||||
bool _quoteExpandLinkLookup = false;
|
||||
|
||||
// current line data
|
||||
style::font _f;
|
||||
int _startLeft = 0;
|
||||
int _startTop = 0;
|
||||
int _startLineWidth = 0;
|
||||
QFixed _x, _wLeft, _last_rPadding;
|
||||
int _y = 0;
|
||||
int _yDelta = 0;
|
||||
int _lineIndex = 0;
|
||||
int _lineHeight = 0;
|
||||
int _fontHeight = 0;
|
||||
bool _breakEverywhere = false;
|
||||
bool _elidedLine = false;
|
||||
|
||||
// elided hack support
|
||||
int _blocksSize = 0;
|
||||
int _elideSavedIndex = 0;
|
||||
std::optional<Block> _elideSavedBlock;
|
||||
|
||||
int _lineStart = 0;
|
||||
int _localFrom = 0;
|
||||
int _lineStartBlock = 0;
|
||||
QFixed _lineStartPadding = 0;
|
||||
QFixed _lineWidth = 0;
|
||||
|
||||
// link and symbol resolve
|
||||
QFixed _lookupX = 0;
|
||||
int _lookupY = 0;
|
||||
bool _lookupSymbol = false;
|
||||
bool _lookupLink = false;
|
||||
StateRequest _lookupRequest;
|
||||
StateResult _lookupResult;
|
||||
|
||||
bool _elisionMiddle = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
230
Telegram/lib_ui/ui/text/text_stack_engine.cpp
Normal file
230
Telegram/lib_ui/ui/text/text_stack_engine.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_stack_engine.h"
|
||||
|
||||
#include "ui/text/text_block.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <private/qharfbuzz_p.h>
|
||||
#endif // Qt < 6.0.0
|
||||
|
||||
namespace Ui::Text {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxItemLength = 4096;
|
||||
|
||||
} // namespace
|
||||
|
||||
StackEngine::StackEngine(
|
||||
not_null<const String*> t,
|
||||
gsl::span<QScriptAnalysis> analysis,
|
||||
int from,
|
||||
int till,
|
||||
int blockIndexHint)
|
||||
: StackEngine(
|
||||
t,
|
||||
from,
|
||||
((from > 0 || (till >= 0 && till < t->_text.size()))
|
||||
? QString::fromRawData(
|
||||
t->_text.constData() + from,
|
||||
((till < 0) ? int(t->_text.size()) : till) - from)
|
||||
: t->_text),
|
||||
analysis,
|
||||
blockIndexHint) {
|
||||
}
|
||||
|
||||
StackEngine::StackEngine(
|
||||
not_null<const String*> t,
|
||||
int offset,
|
||||
const QString &text,
|
||||
gsl::span<QScriptAnalysis> analysis,
|
||||
int blockIndexHint,
|
||||
int blockIndexLimit)
|
||||
: _t(t)
|
||||
, _text(text)
|
||||
, _analysis(analysis.data())
|
||||
, _offset(offset)
|
||||
, _positionEnd(_offset + _text.size())
|
||||
, _font(_t->_st->font)
|
||||
, _engine(_text, _font->f)
|
||||
, _tBlocks(_t->_blocks)
|
||||
, _bStart(begin(_tBlocks) + blockIndexHint)
|
||||
, _bEnd((blockIndexLimit >= 0)
|
||||
? (begin(_tBlocks) + blockIndexLimit)
|
||||
: end(_tBlocks))
|
||||
, _bCached(_bStart) {
|
||||
Expects(analysis.size() >= _text.size());
|
||||
|
||||
_engine.validate();
|
||||
itemize();
|
||||
}
|
||||
|
||||
std::vector<Block>::const_iterator StackEngine::adjustBlock(
|
||||
int offset) const {
|
||||
Expects(offset < _positionEnd);
|
||||
|
||||
if (blockPosition(_bCached) > offset) {
|
||||
_bCached = begin(_tBlocks);
|
||||
}
|
||||
Assert(_bCached != end(_tBlocks));
|
||||
for (auto i = _bCached + 1; blockPosition(i) <= offset; ++i) {
|
||||
_bCached = i;
|
||||
}
|
||||
return _bCached;
|
||||
}
|
||||
|
||||
int StackEngine::blockPosition(std::vector<Block>::const_iterator i) const {
|
||||
return (i == _bEnd) ? _positionEnd : (*i)->position();
|
||||
}
|
||||
|
||||
int StackEngine::blockEnd(std::vector<Block>::const_iterator i) const {
|
||||
return (i == _bEnd) ? _positionEnd : blockPosition(i + 1);
|
||||
}
|
||||
|
||||
void StackEngine::itemize() {
|
||||
const auto layoutData = _engine.layoutData;
|
||||
if (layoutData->items.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto length = layoutData->string.length();
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
|
||||
_bStart = adjustBlock(_offset);
|
||||
const auto chars = _engine.layoutData->string.constData();
|
||||
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QUnicodeTools::ScriptItemArray scriptItems;
|
||||
QUnicodeTools::initScripts(_engine.layoutData->string, &scriptItems);
|
||||
for (int i = 0; i < scriptItems.length(); ++i) {
|
||||
const auto &item = scriptItems.at(i);
|
||||
int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length;
|
||||
for (int j = item.position; j < end; ++j)
|
||||
_analysis[j].script = item.script;
|
||||
}
|
||||
#else // Qt >= 6.0.0
|
||||
QVarLengthArray<uchar> scripts(length);
|
||||
QUnicodeTools::initScripts(reinterpret_cast<const ushort*>(chars), length, scripts.data());
|
||||
for (int i = 0; i < length; ++i)
|
||||
_analysis[i].script = scripts.at(i);
|
||||
#endif // Qt < 6.0.0
|
||||
}
|
||||
|
||||
// Override script and flags for emoji and custom emoji blocks.
|
||||
const auto end = _offset + length;
|
||||
for (auto block = _bStart; blockPosition(block) < end; ++block) {
|
||||
const auto type = (*block)->type();
|
||||
const auto from = std::max(_offset, int(blockPosition(block)));
|
||||
const auto till = std::min(int(end), int(blockEnd(block)));
|
||||
if (till > from) {
|
||||
if (type == TextBlockType::Emoji
|
||||
|| type == TextBlockType::CustomEmoji
|
||||
|| type == TextBlockType::Skip) {
|
||||
for (auto i = from - _offset, count = till - _offset; i != count; ++i) {
|
||||
_analysis[i].script = QChar::Script_Common;
|
||||
_analysis[i].flags = (chars[i] == QChar::Space)
|
||||
? QScriptAnalysis::None
|
||||
: QScriptAnalysis::Object;
|
||||
}
|
||||
} else {
|
||||
for (auto i = from - _offset, count = till - _offset; i != count; ++i) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
_analysis[i].script = hbscript_to_script(script_to_hbscript(_analysis[i].script)); // retain the old behavior
|
||||
#endif // Qt < 6.0.0
|
||||
if (chars[i] == QChar::LineFeed) {
|
||||
_analysis[i].flags = QScriptAnalysis::LineOrParagraphSeparator;
|
||||
} else {
|
||||
_analysis[i].flags = QScriptAnalysis::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto &m_string = _engine.layoutData->string;
|
||||
auto m_analysis = _analysis;
|
||||
auto &m_items = _engine.layoutData->items;
|
||||
|
||||
auto start = 0;
|
||||
auto startBlock = _bStart;
|
||||
auto currentBlock = startBlock;
|
||||
auto nextBlock = currentBlock + 1;
|
||||
for (int i = 1; i != length; ++i) {
|
||||
while (blockPosition(nextBlock) <= _offset + i) {
|
||||
currentBlock = nextBlock++;
|
||||
}
|
||||
// According to the unicode spec we should be treating characters in the Common script
|
||||
// (punctuation, spaces, etc) as being the same script as the surrounding text for the
|
||||
// purpose of splitting up text. This is important because, for example, a fullstop
|
||||
// (0x2E) can be used to indicate an abbreviation and so must be treated as part of a
|
||||
// word. Thus it must be passed along with the word in languages that have to calculate
|
||||
// word breaks. For example the thai word "[lookup-in-git]." has no word breaks
|
||||
// but the word "[lookup-too]" does.
|
||||
// Unfortuntely because we split up the strings for both wordwrapping and for setting
|
||||
// the font and because Japanese and Chinese are also aliases of the script "Common",
|
||||
// doing this would break too many things. So instead we only pass the full stop
|
||||
// along, and nothing else.
|
||||
if (currentBlock != startBlock
|
||||
|| m_analysis[i].flags != m_analysis[start].flags) {
|
||||
// In emoji blocks we can have one item or two items.
|
||||
// First item is the emoji itself,
|
||||
// while the second item are the spaces after the emoji,
|
||||
// which fall in the same block, but have different flags.
|
||||
} else if ((*startBlock)->type() != TextBlockType::Text
|
||||
&& m_analysis[i].flags == m_analysis[start].flags) {
|
||||
// Otherwise, only text blocks may have arbitrary items.
|
||||
Assert(i - start < kMaxItemLength);
|
||||
continue;
|
||||
} else if (m_analysis[i].bidiLevel == m_analysis[start].bidiLevel
|
||||
&& m_analysis[i].flags == m_analysis[start].flags
|
||||
&& (m_analysis[i].script == m_analysis[start].script || m_string[i] == u'.')
|
||||
//&& m_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs
|
||||
&& i - start < kMaxItemLength) {
|
||||
continue;
|
||||
}
|
||||
m_items.append(QScriptItem(start, m_analysis[start]));
|
||||
start = i;
|
||||
startBlock = currentBlock;
|
||||
}
|
||||
m_items.append(QScriptItem(start, m_analysis[start]));
|
||||
}
|
||||
}
|
||||
|
||||
void StackEngine::updateFont(not_null<const AbstractBlock*> block) {
|
||||
const auto flags = block->flags();
|
||||
const auto newFont = WithFlags(_t->_st->font, flags);
|
||||
if (_font != newFont) {
|
||||
_font = (newFont->family() == _t->_st->font->family())
|
||||
? WithFlags(_t->_st->font, flags, newFont->flags())
|
||||
: newFont;
|
||||
_engine.fnt = _font->f;
|
||||
_engine.resetFontEngineCache();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Block>::const_iterator StackEngine::shapeGetBlock(int item) {
|
||||
auto &si = _engine.layoutData->items[item];
|
||||
const auto blockIt = adjustBlock(_offset + si.position);
|
||||
const auto block = blockIt->get();
|
||||
updateFont(block);
|
||||
_engine.shape(item);
|
||||
if (si.analysis.flags == QScriptAnalysis::Object) {
|
||||
si.width = block->objectWidth();
|
||||
}
|
||||
return blockIt;
|
||||
}
|
||||
|
||||
int StackEngine::blockIndex(int position) const {
|
||||
return int(adjustBlock(_offset + position) - begin(_tBlocks));
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
62
Telegram/lib_ui/ui/text/text_stack_engine.h
Normal file
62
Telegram/lib_ui/ui/text/text_stack_engine.h
Normal file
@@ -0,0 +1,62 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
|
||||
#include <private/qfontengine_p.h>
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
class StackEngine final {
|
||||
public:
|
||||
explicit StackEngine(
|
||||
not_null<const String*> t,
|
||||
gsl::span<QScriptAnalysis> analysis,
|
||||
int from = 0,
|
||||
int till = -1,
|
||||
int blockIndexHint = 0);
|
||||
explicit StackEngine(
|
||||
not_null<const String*> t,
|
||||
int offset,
|
||||
const QString &text,
|
||||
gsl::span<QScriptAnalysis> analysis,
|
||||
int blockIndexHint = 0,
|
||||
int blockIndexLimit = -1);
|
||||
|
||||
[[nodiscard]] QTextEngine &wrapped() {
|
||||
return _engine;
|
||||
}
|
||||
|
||||
void itemize();
|
||||
std::vector<Block>::const_iterator shapeGetBlock(int item);
|
||||
[[nodiscard]] int blockIndex(int position) const;
|
||||
|
||||
private:
|
||||
void updateFont(not_null<const AbstractBlock*> block);
|
||||
[[nodiscard]] std::vector<Block>::const_iterator adjustBlock(
|
||||
int offset) const;
|
||||
[[nodiscard]] int blockPosition(
|
||||
std::vector<Block>::const_iterator i) const;
|
||||
[[nodiscard]] int blockEnd(std::vector<Block>::const_iterator i) const;
|
||||
|
||||
const not_null<const String*> _t;
|
||||
const QString &_text;
|
||||
QScriptAnalysis *_analysis = nullptr;
|
||||
const int _offset = 0;
|
||||
const int _positionEnd = 0;
|
||||
style::font _font;
|
||||
QStackTextEngine _engine;
|
||||
|
||||
const std::vector<Block> &_tBlocks;
|
||||
std::vector<Block>::const_iterator _bStart;
|
||||
std::vector<Block>::const_iterator _bEnd;
|
||||
mutable std::vector<Block>::const_iterator _bCached;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
297
Telegram/lib_ui/ui/text/text_utilities.cpp
Normal file
297
Telegram/lib_ui/ui/text/text_utilities.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_utilities.h"
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "base/qt/qt_string_view.h"
|
||||
#include "ui/text/custom_emoji_instance.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
namespace Text {
|
||||
namespace {
|
||||
|
||||
struct IconEmojiData {
|
||||
base::flat_map<not_null<const style::IconEmoji*>, int> indices;
|
||||
std::vector<not_null<const style::IconEmoji*>> list;
|
||||
};
|
||||
|
||||
[[nodiscard]] TextWithEntities WithSingleEntity(
|
||||
const QString &text,
|
||||
EntityType type,
|
||||
const QString &data = QString()) {
|
||||
auto result = TextWithEntities{ text };
|
||||
result.entities.push_back({ type, 0, int(text.size()), data });
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] IconEmojiData &IconEmojiInfo() {
|
||||
static IconEmojiData result;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString IconEmojiPrefix() {
|
||||
return u"icon-emoji-"_q;
|
||||
}
|
||||
|
||||
class IconEmojiObject final : public CustomEmoji {
|
||||
public:
|
||||
explicit IconEmojiObject(not_null<const style::IconEmoji*> emoji);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const not_null<const style::IconEmoji*> _emoji;
|
||||
Ui::CustomEmoji::IconEmojiFrameCache _cache;
|
||||
|
||||
};
|
||||
|
||||
IconEmojiObject::IconEmojiObject(not_null<const style::IconEmoji*> emoji)
|
||||
: _emoji(emoji) {
|
||||
}
|
||||
|
||||
int IconEmojiObject::width() {
|
||||
return _emoji->padding.left()
|
||||
+ _emoji->icon.width()
|
||||
+ _emoji->padding.right();
|
||||
}
|
||||
|
||||
QString IconEmojiObject::entityData() {
|
||||
return IconEmojiPrefix()
|
||||
+ QString::number(IconEmojiInfo().indices[_emoji]);
|
||||
}
|
||||
|
||||
void IconEmojiObject::paint(QPainter &p, const Context &context) {
|
||||
Ui::CustomEmoji::PaintIconEmoji(p, context, _emoji, _cache);
|
||||
}
|
||||
|
||||
void IconEmojiObject::unload() {
|
||||
}
|
||||
|
||||
bool IconEmojiObject::ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IconEmojiObject::readyInDefaultState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextWithEntities Bold(const QString &text) {
|
||||
return WithSingleEntity(text, EntityType::Bold);
|
||||
}
|
||||
|
||||
TextWithEntities Semibold(const QString &text) {
|
||||
return WithSingleEntity(text, EntityType::Semibold);
|
||||
}
|
||||
|
||||
TextWithEntities Italic(const QString &text) {
|
||||
return WithSingleEntity(text, EntityType::Italic);
|
||||
}
|
||||
|
||||
TextWithEntities Underline(const QString &text) {
|
||||
return WithSingleEntity(text, EntityType::Underline);
|
||||
}
|
||||
|
||||
TextWithEntities StrikeOut(const QString &text) {
|
||||
return WithSingleEntity(text, EntityType::StrikeOut);
|
||||
}
|
||||
|
||||
TextWithEntities Link(const QString &text, const QString &url) {
|
||||
return WithSingleEntity(text, EntityType::CustomUrl, url);
|
||||
}
|
||||
|
||||
TextWithEntities Link(const QString &text, int index) {
|
||||
return Link(text, u"internal:index"_q + QChar(index));
|
||||
}
|
||||
|
||||
TextWithEntities Link(TextWithEntities text, const QString &url) {
|
||||
return Wrapped(std::move(text), EntityType::CustomUrl, url);
|
||||
}
|
||||
|
||||
TextWithEntities Link(TextWithEntities text, int index) {
|
||||
return Link(std::move(text), u"internal:index"_q + QChar(index));
|
||||
}
|
||||
|
||||
TextWithEntities Colorized(const QString &text, int index) {
|
||||
const auto data = index ? QString(QChar(index)) : QString();
|
||||
return WithSingleEntity(text, EntityType::Colorized, data);
|
||||
}
|
||||
|
||||
TextWithEntities Colorized(TextWithEntities text, int index) {
|
||||
const auto data = index ? QString(QChar(index)) : QString();
|
||||
return Wrapped(std::move(text), EntityType::Colorized, data);
|
||||
}
|
||||
|
||||
TextWithEntities Wrapped(
|
||||
TextWithEntities text,
|
||||
EntityType type,
|
||||
const QString &data) {
|
||||
text.entities.insert(
|
||||
text.entities.begin(),
|
||||
{ type, 0, int(text.text.size()), data });
|
||||
return text;
|
||||
}
|
||||
|
||||
TextWithEntities RichLangValue(const QString &text) {
|
||||
static const auto kStart = QRegularExpression("(\\*\\*|__)");
|
||||
|
||||
auto result = TextWithEntities();
|
||||
auto offset = 0;
|
||||
while (offset < text.size()) {
|
||||
const auto m = kStart.match(text, offset);
|
||||
if (!m.hasMatch()) {
|
||||
result.text.append(base::StringViewMid(text, offset));
|
||||
break;
|
||||
}
|
||||
const auto position = m.capturedStart();
|
||||
const auto from = m.capturedEnd();
|
||||
const auto tag = m.capturedView();
|
||||
const auto till = text.indexOf(tag, from + 1);
|
||||
if (till <= from) {
|
||||
offset = from;
|
||||
continue;
|
||||
}
|
||||
if (position > offset) {
|
||||
result.text.append(base::StringViewMid(text, offset, position - offset));
|
||||
}
|
||||
const auto type = (tag == qstr("__"))
|
||||
? EntityType::Italic
|
||||
: EntityType::Bold;
|
||||
result.entities.push_back({ type, int(result.text.size()), int(till - from) });
|
||||
result.text.append(base::StringViewMid(text, from, till - from));
|
||||
offset = till + tag.size();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TextWithEntities SingleCustomEmoji(QString data, QString text) {
|
||||
if (text.isEmpty()) {
|
||||
text = u"@"_q;
|
||||
}
|
||||
return {
|
||||
text,
|
||||
{ EntityInText(EntityType::CustomEmoji, 0, text.size(), data)},
|
||||
};
|
||||
}
|
||||
|
||||
TextWithEntities IconEmoji(
|
||||
not_null<const style::IconEmoji*> emoji,
|
||||
QString text) {
|
||||
const auto index = [&] {
|
||||
auto &info = IconEmojiInfo();
|
||||
const auto count = int(info.list.size());
|
||||
auto i = info.indices.emplace(emoji, count).first;
|
||||
if (i->second == count) {
|
||||
info.list.push_back(emoji);
|
||||
}
|
||||
return i->second;
|
||||
}();
|
||||
return SingleCustomEmoji(
|
||||
IconEmojiPrefix() + QString::number(index),
|
||||
text);
|
||||
}
|
||||
|
||||
std::unique_ptr<CustomEmoji> TryMakeSimpleEmoji(QStringView data) {
|
||||
const auto prefix = IconEmojiPrefix();
|
||||
if (!data.startsWith(prefix)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto &info = IconEmojiInfo();
|
||||
const auto index = data.mid(prefix.size()).toInt();
|
||||
return (index >= 0 && index < info.list.size())
|
||||
? std::make_unique<IconEmojiObject>(info.list[index])
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
TextWithEntities Mid(const TextWithEntities &text, int position, int n) {
|
||||
if (n == -1) {
|
||||
n = int(text.text.size()) - position;
|
||||
}
|
||||
const auto midEnd = (position + n);
|
||||
auto entities = ranges::views::all(
|
||||
text.entities
|
||||
) | ranges::views::filter([&](const EntityInText &entity) {
|
||||
// Intersects of ranges.
|
||||
const auto l1 = entity.offset();
|
||||
const auto r1 = entity.offset() + entity.length() - 1;
|
||||
const auto l2 = position;
|
||||
const auto r2 = midEnd - 1;
|
||||
return !(l1 > r2 || l2 > r1);
|
||||
}) | ranges::views::transform([&](const EntityInText &entity) {
|
||||
if ((entity.offset() == position) && (entity.length() == n)) {
|
||||
return entity;
|
||||
}
|
||||
const auto start = std::max(entity.offset(), position);
|
||||
const auto end = std::min(entity.offset() + entity.length(), midEnd);
|
||||
return EntityInText(
|
||||
entity.type(),
|
||||
start - position,
|
||||
end - start,
|
||||
entity.data());
|
||||
}) | ranges::to<EntitiesInText>();
|
||||
return {
|
||||
.text = text.text.mid(position, n),
|
||||
.entities = std::move(entities),
|
||||
};
|
||||
}
|
||||
|
||||
TextWithEntities Filtered(
|
||||
const TextWithEntities &text,
|
||||
const std::vector<EntityType> &types) {
|
||||
auto result = ranges::views::all(
|
||||
text.entities
|
||||
) | ranges::views::filter([&](const EntityInText &entity) {
|
||||
return ranges::contains(types, entity.type());
|
||||
}) | ranges::to<EntitiesInText>();
|
||||
return { .text = text.text, .entities = std::move(result) };
|
||||
}
|
||||
|
||||
QString FixAmpersandInAction(QString text) {
|
||||
return text.replace('&', u"&&"_q);
|
||||
}
|
||||
|
||||
TextWithEntities WrapEmailPattern(const QString &pattern) {
|
||||
constexpr auto kHidden = '*';
|
||||
const auto from = int(pattern.indexOf(kHidden));
|
||||
const auto to = int(pattern.lastIndexOf(kHidden));
|
||||
|
||||
if (from != -1 && to != -1 && from <= to) {
|
||||
const auto length = to - from + 1;
|
||||
auto result = TextWithEntities{ pattern };
|
||||
result.entities.push_back({ EntityType::Spoiler, from, length });
|
||||
return result;
|
||||
}
|
||||
return { pattern };
|
||||
}
|
||||
|
||||
QList<QStringView> Words(QStringView lower) {
|
||||
static const auto kRegWords = QRegularExpression(
|
||||
u"[\\W]"_q,
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
return lower.split(kRegWords, Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
QString StripUrlProtocol(const QString &link) {
|
||||
return link.startsWith(u"https://"_q)
|
||||
? link.mid(8)
|
||||
: link.startsWith(u"http://"_q)
|
||||
? link.mid(7)
|
||||
: link;
|
||||
}
|
||||
|
||||
} // namespace Text
|
||||
} // namespace Ui
|
||||
74
Telegram/lib_ui/ui/text/text_utilities.h
Normal file
74
Telegram/lib_ui/ui/text/text_utilities.h
Normal file
@@ -0,0 +1,74 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_entity.h"
|
||||
|
||||
namespace style {
|
||||
struct IconEmoji;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
class CustomEmoji;
|
||||
|
||||
[[nodiscard]] TextWithEntities Bold(const QString &text);
|
||||
[[nodiscard]] TextWithEntities Semibold(const QString &text);
|
||||
[[nodiscard]] TextWithEntities Italic(const QString &text);
|
||||
[[nodiscard]] TextWithEntities Underline(const QString &text);
|
||||
[[nodiscard]] TextWithEntities StrikeOut(const QString &text);
|
||||
[[nodiscard]] TextWithEntities Link(
|
||||
const QString &text,
|
||||
const QString &url = u"internal:action"_q);
|
||||
[[nodiscard]] TextWithEntities Link(const QString &text, int index);
|
||||
[[nodiscard]] TextWithEntities Link(
|
||||
TextWithEntities text,
|
||||
const QString &url = u"internal:action"_q);
|
||||
[[nodiscard]] TextWithEntities Link(TextWithEntities text, int index);
|
||||
[[nodiscard]] TextWithEntities Colorized(
|
||||
const QString &text,
|
||||
int index = 0);
|
||||
[[nodiscard]] TextWithEntities Colorized(
|
||||
TextWithEntities text,
|
||||
int index = 0);
|
||||
[[nodiscard]] TextWithEntities Wrapped(
|
||||
TextWithEntities text,
|
||||
EntityType type,
|
||||
const QString &data = QString());
|
||||
[[nodiscard]] TextWithEntities RichLangValue(const QString &text);
|
||||
[[nodiscard]] inline TextWithEntities WithEntities(const QString &text) {
|
||||
return { text };
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities SingleCustomEmoji(
|
||||
QString data,
|
||||
QString text = QString());
|
||||
|
||||
[[nodiscard]] TextWithEntities IconEmoji(
|
||||
not_null<const style::IconEmoji*> emoji,
|
||||
QString text = QString());
|
||||
|
||||
[[nodiscard]] std::unique_ptr<CustomEmoji> TryMakeSimpleEmoji(
|
||||
QStringView data);
|
||||
|
||||
[[nodiscard]] TextWithEntities Mid(
|
||||
const TextWithEntities &text,
|
||||
int position,
|
||||
int n = -1);
|
||||
[[nodiscard]] TextWithEntities Filtered(
|
||||
const TextWithEntities &result,
|
||||
const std::vector<EntityType> &types);
|
||||
|
||||
[[nodiscard]] QString FixAmpersandInAction(QString text);
|
||||
|
||||
[[nodiscard]] TextWithEntities WrapEmailPattern(const QString &);
|
||||
|
||||
[[nodiscard]] QList<QStringView> Words(QStringView lower);
|
||||
|
||||
[[nodiscard]] QString StripUrlProtocol(const QString &link);
|
||||
|
||||
} // namespace Ui::Text
|
||||
60
Telegram/lib_ui/ui/text/text_variant.cpp
Normal file
60
Telegram/lib_ui/ui/text/text_variant.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_variant.h"
|
||||
|
||||
namespace v::text {
|
||||
|
||||
bool is_plain(const data &d) {
|
||||
return v::is<v::null_t>(d)
|
||||
|| v::is<QString>(d)
|
||||
|| v::is<rpl::producer<QString>>(d);
|
||||
}
|
||||
|
||||
bool is_marked(const data &d) {
|
||||
return !is_plain(d);
|
||||
}
|
||||
|
||||
rpl::producer<QString> take_plain(
|
||||
data &&d,
|
||||
rpl::producer<QString> &&fallback) {
|
||||
using RplMarked = rpl::producer<TextWithEntities>;
|
||||
if (v::is_null(d)) {
|
||||
return std::move(fallback);
|
||||
} else if (const auto ptr = std::get_if<QString>(&d)) {
|
||||
return rpl::single(base::take(*ptr));
|
||||
} else if (const auto ptr = std::get_if<rpl::producer<QString>>(&d)) {
|
||||
return base::take(*ptr);
|
||||
} else if (const auto ptr = std::get_if<TextWithEntities>(&d)) {
|
||||
return rpl::single(base::take(*ptr).text);
|
||||
} else if (const auto ptr = std::get_if<RplMarked>(&d)) {
|
||||
return base::take(*ptr) | rpl::map([](const auto &marked) {
|
||||
return marked.text;
|
||||
});
|
||||
}
|
||||
Unexpected("Bad variant in take_plain.");
|
||||
}
|
||||
|
||||
rpl::producer<TextWithEntities> take_marked(
|
||||
data &&d,
|
||||
rpl::producer<TextWithEntities> &&fallback) {
|
||||
using RplMarked = rpl::producer<TextWithEntities>;
|
||||
if (v::is_null(d)) {
|
||||
return std::move(fallback);
|
||||
} else if (const auto ptr = std::get_if<QString>(&d)) {
|
||||
return rpl::single(TextWithEntities{ base::take(*ptr) });
|
||||
} else if (const auto ptr = std::get_if<rpl::producer<QString>>(&d)) {
|
||||
return base::take(*ptr) | rpl::map(TextWithEntities::Simple);
|
||||
} else if (const auto ptr = std::get_if<TextWithEntities>(&d)) {
|
||||
return rpl::single(base::take(*ptr));
|
||||
} else if (const auto ptr = std::get_if<RplMarked>(&d)) {
|
||||
return base::take(*ptr);
|
||||
}
|
||||
Unexpected("Bad variant in take_marked.");
|
||||
}
|
||||
|
||||
} // namespace v::text
|
||||
|
||||
30
Telegram/lib_ui/ui/text/text_variant.h
Normal file
30
Telegram/lib_ui/ui/text/text_variant.h
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_entity.h"
|
||||
|
||||
namespace v::text {
|
||||
|
||||
using data = std::variant<
|
||||
v::null_t,
|
||||
QString,
|
||||
rpl::producer<QString>,
|
||||
TextWithEntities,
|
||||
rpl::producer<TextWithEntities>>;
|
||||
|
||||
[[nodiscard]] bool is_plain(const data &d);
|
||||
[[nodiscard]] bool is_marked(const data &d);
|
||||
[[nodiscard]] rpl::producer<QString> take_plain(
|
||||
data &&d,
|
||||
rpl::producer<QString> &&fallback = rpl::never<QString>());
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> take_marked(
|
||||
data &&d,
|
||||
rpl::producer<TextWithEntities> &&fallback
|
||||
= rpl::never<TextWithEntities>());
|
||||
|
||||
} // namespace v::text
|
||||
90
Telegram/lib_ui/ui/text/text_word.h
Normal file
90
Telegram/lib_ui/ui/text/text_word.h
Normal file
@@ -0,0 +1,90 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/basic_types.h"
|
||||
|
||||
#include <private/qfixed_p.h>
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
class Word final {
|
||||
public:
|
||||
Word() = default;
|
||||
Word( // !newline
|
||||
uint16 position,
|
||||
bool unfinished,
|
||||
QFixed width,
|
||||
QFixed rbearing)
|
||||
: _position(position)
|
||||
, _rbearing_modulus(std::min(std::abs(rbearing.value()), 0x7FFF))
|
||||
, _rbearing_positive(rbearing.value() > 0 ? 1 : 0)
|
||||
, _unfinished(unfinished ? 1 : 0)
|
||||
, _qfixedwidth(width.value()) {
|
||||
}
|
||||
Word(uint16 position, int newlineBlockIndex)
|
||||
: _position(position)
|
||||
, _newline(1)
|
||||
, _newlineBlockIndex(newlineBlockIndex) {
|
||||
}
|
||||
|
||||
[[nodiscard]] bool newline() const {
|
||||
return _newline != 0;
|
||||
}
|
||||
[[nodiscard]] int newlineBlockIndex() const {
|
||||
return _newline ? _newlineBlockIndex : 0;
|
||||
}
|
||||
[[nodiscard]] bool unfinished() const {
|
||||
return _unfinished != 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint16 position() const {
|
||||
return _position;
|
||||
}
|
||||
[[nodiscard]] QFixed f_rbearing() const {
|
||||
return QFixed::fromFixed(
|
||||
int(_rbearing_modulus) * (_rbearing_positive ? 1 : -1));
|
||||
}
|
||||
[[nodiscard]] QFixed f_width() const {
|
||||
return _newline ? 0 : QFixed::fromFixed(_qfixedwidth);
|
||||
}
|
||||
[[nodiscard]] QFixed f_rpadding() const {
|
||||
return _rpadding;
|
||||
}
|
||||
|
||||
void add_rpadding(QFixed padding) {
|
||||
_rpadding += padding;
|
||||
}
|
||||
|
||||
private:
|
||||
uint16 _position = 0;
|
||||
uint16 _rbearing_modulus : 13 = 0;
|
||||
uint16 _rbearing_positive : 1 = 0;
|
||||
uint16 _unfinished : 1 = 0;
|
||||
uint16 _newline : 1 = 0;
|
||||
|
||||
// Right padding: spaces after the last content of the block (like a word).
|
||||
// This holds spaces after the end of the block, for example a text ending
|
||||
// with a space before a link has started. If text block has a leading spaces
|
||||
// (for example a text block after a link block) it is prepended with an empty
|
||||
// word that holds those spaces as a right padding.
|
||||
QFixed _rpadding;
|
||||
|
||||
union {
|
||||
int _qfixedwidth;
|
||||
int _newlineBlockIndex;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
using Words = std::vector<Word>;
|
||||
|
||||
[[nodiscard]] inline uint16 CountPosition(Words::const_iterator i) {
|
||||
return i->position();
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
371
Telegram/lib_ui/ui/text/text_word_parser.cpp
Normal file
371
Telegram/lib_ui/ui/text/text_word_parser.cpp
Normal file
@@ -0,0 +1,371 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/text/text_word_parser.h"
|
||||
|
||||
#include "ui/text/text_bidi_algorithm.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
// COPIED FROM qtextlayout.cpp AND MODIFIED
|
||||
namespace Ui::Text {
|
||||
|
||||
glyph_t WordParser::LineBreakHelper::currentGlyph() const {
|
||||
Q_ASSERT(currentPosition > 0);
|
||||
Q_ASSERT(logClusters[currentPosition - 1] < glyphs.numGlyphs);
|
||||
|
||||
return glyphs.glyphs[logClusters[currentPosition - 1]];
|
||||
}
|
||||
|
||||
void WordParser::LineBreakHelper::saveCurrentGlyph() {
|
||||
if (currentPosition > 0
|
||||
&& logClusters[currentPosition - 1] < glyphs.numGlyphs) {
|
||||
// needed to calculate right bearing later
|
||||
previousGlyph = currentGlyph();
|
||||
previousGlyphFontEngine = fontEngine;
|
||||
} else {
|
||||
previousGlyph = 0;
|
||||
previousGlyphFontEngine = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void WordParser::LineBreakHelper::calculateRightBearing(
|
||||
QFontEngine *engine,
|
||||
glyph_t glyph) {
|
||||
qreal rb;
|
||||
engine->getGlyphBearings(glyph, 0, &rb);
|
||||
|
||||
// We only care about negative right bearings, so we limit the range
|
||||
// of the bearing here so that we can assume it's negative in the rest
|
||||
// of the code, as well as use QFixed(1) as a sentinel to represent
|
||||
// the state where we have yet to compute the right bearing.
|
||||
rightBearing = qMin(QFixed::fromReal(rb), QFixed(0));
|
||||
}
|
||||
|
||||
void WordParser::LineBreakHelper::calculateRightBearing() {
|
||||
if (currentPosition > 0
|
||||
&& logClusters[currentPosition - 1] < glyphs.numGlyphs
|
||||
&& !whiteSpaceOrObject) {
|
||||
calculateRightBearing(fontEngine.data(), currentGlyph());
|
||||
} else {
|
||||
rightBearing = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void WordParser::LineBreakHelper::calculateRightBearingForPreviousGlyph() {
|
||||
if (previousGlyph > 0) {
|
||||
calculateRightBearing(previousGlyphFontEngine.data(), previousGlyph);
|
||||
} else {
|
||||
rightBearing = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We always calculate the right bearing right before it is needed.
|
||||
// So we don't need caching / optimizations referred to delayed right bearing calculations.
|
||||
|
||||
//static const QFixed RightBearingNotCalculated;
|
||||
|
||||
//inline void WordParser::LineBreakHelper::resetRightBearing()
|
||||
//{
|
||||
// rightBearing = RightBearingNotCalculated;
|
||||
//}
|
||||
|
||||
// We express the negative right bearing as an absolute number
|
||||
// so that it can be applied to the width using addition.
|
||||
QFixed WordParser::LineBreakHelper::negativeRightBearing() const {
|
||||
//if (rightBearing == RightBearingNotCalculated)
|
||||
// return QFixed(0);
|
||||
|
||||
return qAbs(rightBearing);
|
||||
}
|
||||
|
||||
void WordParser::addNextCluster(
|
||||
int &pos,
|
||||
int end,
|
||||
ScriptLine &line,
|
||||
int &glyphCount,
|
||||
const QScriptItem ¤t,
|
||||
const unsigned short *logClusters,
|
||||
const QGlyphLayout &glyphs) {
|
||||
int glyphPosition = logClusters[pos];
|
||||
do { // got to the first next cluster
|
||||
++pos;
|
||||
++line.length;
|
||||
} while (pos < end && logClusters[pos] == glyphPosition);
|
||||
do { // calculate the textWidth for the rest of the current cluster.
|
||||
if (!glyphs.attributes[glyphPosition].dontPrint)
|
||||
line.textWidth += glyphs.advances[glyphPosition];
|
||||
++glyphPosition;
|
||||
} while (glyphPosition < current.num_glyphs
|
||||
&& !glyphs.attributes[glyphPosition].clusterStart);
|
||||
|
||||
Q_ASSERT((pos == end && glyphPosition == current.num_glyphs)
|
||||
|| logClusters[pos] == glyphPosition);
|
||||
|
||||
++glyphCount;
|
||||
}
|
||||
|
||||
WordParser::BidiInitedAnalysis::BidiInitedAnalysis(not_null<String*> text)
|
||||
: list(text->_text.size()) {
|
||||
BidiAlgorithm bidi(
|
||||
text->_text.constData(),
|
||||
list.data(),
|
||||
text->_text.size(),
|
||||
false, // baseDirectionIsRtl
|
||||
begin(text->_blocks),
|
||||
end(text->_blocks),
|
||||
0); // offsetInBlocks
|
||||
bidi.process();
|
||||
}
|
||||
|
||||
WordParser::WordParser(not_null<String*> string)
|
||||
: _t(string)
|
||||
, _tText(_t->_text)
|
||||
, _tBlocks(_t->_blocks)
|
||||
, _tWords(_t->_words)
|
||||
, _analysis(_t)
|
||||
, _engine(_t, _analysis.list)
|
||||
, _e(_engine.wrapped()) {
|
||||
parse();
|
||||
}
|
||||
|
||||
void WordParser::parse() {
|
||||
_tWords.clear();
|
||||
if (_tText.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_newItem = _e.findItem(0);
|
||||
_attributes = _e.attributes();
|
||||
if (!_attributes) {
|
||||
return;
|
||||
}
|
||||
_lbh.logClusters = _e.layoutData->logClustersPtr;
|
||||
|
||||
while (_newItem < _e.layoutData->items.size()) {
|
||||
if (_newItem != _item) {
|
||||
_attributes = moveToNewItemGetAttributes();
|
||||
if (!_attributes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto ¤t = _e.layoutData->items[_item];
|
||||
const auto atSpaceBreak = [&] {
|
||||
for (auto index = _lbh.currentPosition; index < _itemEnd; ++index) {
|
||||
if (!_attributes[index].whiteSpace) {
|
||||
return false;
|
||||
} else if (isSpaceBreak(_attributes, index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (current.analysis.flags == QScriptAnalysis::LineOrParagraphSeparator) {
|
||||
pushAccumulatedWord();
|
||||
processSingleGlyphItem();
|
||||
pushNewline(_wordStart, _engine.blockIndex(_wordStart));
|
||||
wordProcessed(_itemEnd);
|
||||
} else if (current.analysis.flags == QScriptAnalysis::Object) {
|
||||
pushAccumulatedWord();
|
||||
processSingleGlyphItem(current.width);
|
||||
_lbh.calculateRightBearing();
|
||||
pushFinishedWord(
|
||||
_wordStart,
|
||||
_lbh.tmpData.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
wordProcessed(_itemEnd);
|
||||
} else if (atSpaceBreak) {
|
||||
pushAccumulatedWord();
|
||||
accumulateWhitespaces();
|
||||
ensureWordForRightPadding();
|
||||
_tWords.back().add_rpadding(_lbh.spaceData.textWidth);
|
||||
wordProcessed(_lbh.currentPosition, true);
|
||||
} else {
|
||||
_lbh.whiteSpaceOrObject = false;
|
||||
do {
|
||||
addNextCluster(
|
||||
_lbh.currentPosition,
|
||||
_itemEnd,
|
||||
_lbh.tmpData,
|
||||
_lbh.glyphCount,
|
||||
current,
|
||||
_lbh.logClusters,
|
||||
_lbh.glyphs);
|
||||
|
||||
if (_lbh.currentPosition >= _e.layoutData->string.length()
|
||||
|| isSpaceBreak(_attributes, _lbh.currentPosition)
|
||||
|| isLineBreak(_attributes, _lbh.currentPosition)) {
|
||||
maybeStartUnfinishedWord();
|
||||
_lbh.calculateRightBearing();
|
||||
pushFinishedWord(
|
||||
_wordStart,
|
||||
_lbh.tmpData.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
wordProcessed(_lbh.currentPosition);
|
||||
break;
|
||||
} else if (_attributes[_lbh.currentPosition].graphemeBoundary) {
|
||||
maybeStartUnfinishedWord();
|
||||
if (_addingEachGrapheme) {
|
||||
_lbh.calculateRightBearing();
|
||||
pushUnfinishedWord(
|
||||
_wordStart,
|
||||
_lbh.tmpData.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
wordContinued(_lbh.currentPosition);
|
||||
} else {
|
||||
_lastGraphemeBoundaryPosition = _lbh.currentPosition;
|
||||
_lastGraphemeBoundaryLine = _lbh.tmpData;
|
||||
_lbh.saveCurrentGlyph();
|
||||
}
|
||||
}
|
||||
} while (_lbh.currentPosition < _itemEnd);
|
||||
}
|
||||
if (_lbh.currentPosition == _itemEnd)
|
||||
_newItem = _item + 1;
|
||||
}
|
||||
if (!_tWords.empty()) {
|
||||
_tWords.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
const QCharAttributes *WordParser::moveToNewItemGetAttributes() {
|
||||
_item = _newItem;
|
||||
auto &si = _e.layoutData->items[_item];
|
||||
auto result = _e.attributes();
|
||||
if (!si.num_glyphs) {
|
||||
_engine.shapeGetBlock(_item);
|
||||
result = _e.attributes();
|
||||
if (!result) {
|
||||
return nullptr;
|
||||
}
|
||||
_lbh.logClusters = _e.layoutData->logClustersPtr;
|
||||
}
|
||||
_lbh.currentPosition = si.position;
|
||||
_itemEnd = si.position + _e.length(_item);
|
||||
_lbh.glyphs = _e.shapedGlyphs(&si);
|
||||
const auto fontEngine = _e.fontEngine(si);
|
||||
if (_lbh.fontEngine != fontEngine) {
|
||||
_lbh.fontEngine = fontEngine;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void WordParser::pushAccumulatedWord() {
|
||||
if (_wordStart < _lbh.currentPosition) {
|
||||
_lbh.calculateRightBearing();
|
||||
pushFinishedWord(
|
||||
_wordStart,
|
||||
_lbh.tmpData.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
wordProcessed(_lbh.currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void WordParser::processSingleGlyphItem(QFixed added) {
|
||||
_lbh.whiteSpaceOrObject = true;
|
||||
++_lbh.tmpData.length;
|
||||
_lbh.tmpData.textWidth += added;
|
||||
|
||||
_newItem = _item + 1;
|
||||
++_lbh.glyphCount;
|
||||
}
|
||||
|
||||
void WordParser::wordProcessed(int nextWordStart, bool spaces) {
|
||||
wordContinued(nextWordStart, spaces);
|
||||
_addingEachGrapheme = false;
|
||||
_lastGraphemeBoundaryPosition = -1;
|
||||
_lastGraphemeBoundaryLine = ScriptLine();
|
||||
}
|
||||
|
||||
void WordParser::wordContinued(int nextPartStart, bool spaces) {
|
||||
if (spaces) {
|
||||
_lbh.spaceData.textWidth = 0;
|
||||
_lbh.spaceData.length = 0;
|
||||
} else {
|
||||
_lbh.tmpData.textWidth = 0;
|
||||
_lbh.tmpData.length = 0;
|
||||
}
|
||||
_wordStart = nextPartStart;
|
||||
}
|
||||
|
||||
void WordParser::accumulateWhitespaces() {
|
||||
const auto ¤t = _e.layoutData->items[_item];
|
||||
|
||||
_lbh.whiteSpaceOrObject = true;
|
||||
while (_lbh.currentPosition < _itemEnd
|
||||
&& _attributes[_lbh.currentPosition].whiteSpace)
|
||||
addNextCluster(
|
||||
_lbh.currentPosition,
|
||||
_itemEnd,
|
||||
_lbh.spaceData,
|
||||
_lbh.glyphCount,
|
||||
current,
|
||||
_lbh.logClusters,
|
||||
_lbh.glyphs);
|
||||
}
|
||||
|
||||
void WordParser::ensureWordForRightPadding() {
|
||||
if (_tWords.empty()) {
|
||||
_lbh.calculateRightBearing();
|
||||
pushFinishedWord(
|
||||
_wordStart,
|
||||
_lbh.tmpData.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
}
|
||||
}
|
||||
|
||||
void WordParser::maybeStartUnfinishedWord() {
|
||||
if (!_addingEachGrapheme && _lbh.tmpData.textWidth > _t->_minResizeWidth) {
|
||||
if (_lastGraphemeBoundaryPosition >= 0) {
|
||||
_lbh.calculateRightBearingForPreviousGlyph();
|
||||
pushUnfinishedWord(
|
||||
_wordStart,
|
||||
_lastGraphemeBoundaryLine.textWidth,
|
||||
-_lbh.negativeRightBearing());
|
||||
_lbh.tmpData.textWidth -= _lastGraphemeBoundaryLine.textWidth;
|
||||
_lbh.tmpData.length -= _lastGraphemeBoundaryLine.length;
|
||||
_wordStart = _lastGraphemeBoundaryPosition;
|
||||
}
|
||||
_addingEachGrapheme = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WordParser::pushFinishedWord(
|
||||
uint16 position,
|
||||
QFixed width,
|
||||
QFixed rbearing) {
|
||||
const auto unfinished = false;
|
||||
_tWords.push_back(Word(position, unfinished, width, rbearing));
|
||||
}
|
||||
|
||||
void WordParser::pushUnfinishedWord(
|
||||
uint16 position,
|
||||
QFixed width,
|
||||
QFixed rbearing) {
|
||||
const auto unfinished = true;
|
||||
_tWords.push_back(Word(position, unfinished, width, rbearing));
|
||||
}
|
||||
|
||||
void WordParser::pushNewline(uint16 position, int newlineBlockIndex) {
|
||||
_tWords.push_back(Word(position, newlineBlockIndex));
|
||||
}
|
||||
|
||||
bool WordParser::isLineBreak(
|
||||
const QCharAttributes *attributes,
|
||||
int index) const {
|
||||
// Don't break by '/' or '.' in the middle of the word.
|
||||
// In case of a line break or white space it'll allow break anyway.
|
||||
return attributes[index].lineBreak
|
||||
&& (index <= 0
|
||||
|| (_tText[index - 1] != '/' && _tText[index - 1] != '.'));
|
||||
}
|
||||
|
||||
bool WordParser::isSpaceBreak(
|
||||
const QCharAttributes *attributes,
|
||||
int index) const {
|
||||
// Don't break on
|
||||
return attributes[index].whiteSpace && (_tText[index] != QChar::Nbsp);
|
||||
}
|
||||
|
||||
} // namespace Ui::Text
|
||||
126
Telegram/lib_ui/ui/text/text_word_parser.h
Normal file
126
Telegram/lib_ui/ui/text/text_word_parser.h
Normal file
@@ -0,0 +1,126 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_block.h"
|
||||
#include "ui/text/text_stack_engine.h"
|
||||
#include "ui/text/text_word.h"
|
||||
|
||||
struct QGlyphLayout;
|
||||
struct QScriptItem;
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
class WordParser {
|
||||
public:
|
||||
explicit WordParser(not_null<String*> string);
|
||||
|
||||
private:
|
||||
struct ScriptLine {
|
||||
int length = 0;
|
||||
QFixed textWidth;
|
||||
};
|
||||
struct LineBreakHelper {
|
||||
ScriptLine tmpData;
|
||||
ScriptLine spaceData;
|
||||
|
||||
QGlyphLayout glyphs;
|
||||
|
||||
int glyphCount = 0;
|
||||
int maxGlyphs = INT_MAX;
|
||||
int currentPosition = 0;
|
||||
|
||||
glyph_t previousGlyph = 0;
|
||||
QExplicitlySharedDataPointer<QFontEngine> previousGlyphFontEngine;
|
||||
|
||||
QFixed rightBearing;
|
||||
|
||||
QExplicitlySharedDataPointer<QFontEngine> fontEngine;
|
||||
const unsigned short *logClusters = nullptr;
|
||||
|
||||
bool whiteSpaceOrObject = true;
|
||||
|
||||
glyph_t currentGlyph() const;
|
||||
void saveCurrentGlyph();
|
||||
void calculateRightBearing(QFontEngine *engine, glyph_t glyph);
|
||||
void calculateRightBearing();
|
||||
void calculateRightBearingForPreviousGlyph();
|
||||
|
||||
// We always calculate the right bearing right before it is needed.
|
||||
// So we don't need caching / optimizations referred to
|
||||
// delayed right bearing calculations.
|
||||
|
||||
//static const QFixed RightBearingNotCalculated;
|
||||
|
||||
//inline void resetRightBearing()
|
||||
//{
|
||||
// rightBearing = RightBearingNotCalculated;
|
||||
//}
|
||||
|
||||
// We express the negative right bearing as an absolute number
|
||||
// so that it can be applied to the width using addition.
|
||||
QFixed negativeRightBearing() const;
|
||||
|
||||
};
|
||||
struct BidiInitedAnalysis {
|
||||
explicit BidiInitedAnalysis(not_null<String*> text);
|
||||
|
||||
QVarLengthArray<QScriptAnalysis, 4096> list;
|
||||
};
|
||||
|
||||
void parse();
|
||||
|
||||
const QCharAttributes *moveToNewItemGetAttributes();
|
||||
|
||||
void pushAccumulatedWord();
|
||||
void processSingleGlyphItem(QFixed added = 0);
|
||||
void wordProcessed(int nextWordStart, bool spaces = false);
|
||||
void wordContinued(int nextPartStart, bool spaces = false);
|
||||
void accumulateWhitespaces();
|
||||
void ensureWordForRightPadding();
|
||||
void maybeStartUnfinishedWord();
|
||||
void pushFinishedWord(uint16 position, QFixed width, QFixed rbearing);
|
||||
void pushUnfinishedWord(uint16 position, QFixed width, QFixed rbearing);
|
||||
void pushNewline(uint16 position, int newlineBlockIndex);
|
||||
|
||||
void addNextCluster(
|
||||
int &pos,
|
||||
int end,
|
||||
ScriptLine &line,
|
||||
int &glyphCount,
|
||||
const QScriptItem ¤t,
|
||||
const unsigned short *logClusters,
|
||||
const QGlyphLayout &glyphs);
|
||||
|
||||
[[nodiscard]] bool isLineBreak(
|
||||
const QCharAttributes *attributes,
|
||||
int index) const;
|
||||
[[nodiscard]] bool isSpaceBreak(
|
||||
const QCharAttributes *attributes,
|
||||
int index) const;
|
||||
|
||||
const not_null<String*> _t;
|
||||
QString &_tText;
|
||||
std::vector<Block> &_tBlocks;
|
||||
std::vector<Word> &_tWords;
|
||||
BidiInitedAnalysis _analysis;
|
||||
StackEngine _engine;
|
||||
QTextEngine &_e;
|
||||
LineBreakHelper _lbh;
|
||||
const QCharAttributes *_attributes = nullptr;
|
||||
int _wordStart = 0;
|
||||
bool _addingEachGrapheme = false;
|
||||
int _lastGraphemeBoundaryPosition = -1;
|
||||
ScriptLine _lastGraphemeBoundaryLine;
|
||||
int _item = -1;
|
||||
int _newItem = -1;
|
||||
int _itemEnd = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Text
|
||||
Reference in New Issue
Block a user