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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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 };
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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 &quoteStyle(
not_null<QuoteDetails*> quote) const;
[[nodiscard]] QMargins quotePadding(QuoteDetails *quote) const;
[[nodiscard]] int quoteMinWidth(QuoteDetails *quote) const;
[[nodiscard]] const QString &quoteHeaderText(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());
}

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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 &quotes = _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 &quotes = _t->ensureQuotes()->list;
auto &quote = 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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 &current,
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 &current = _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 &current = _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 &nbsp;
return attributes[index].whiteSpace && (_tText[index] != QChar::Nbsp);
}
} // namespace Ui::Text

View 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 &current,
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