Optimize simple Ui::Text::String instances.

This commit is contained in:
John Preston 2023-10-12 13:29:17 +04:00
parent 44f8d862ff
commit ab5057f001
17 changed files with 291 additions and 159 deletions

View file

@ -165,13 +165,13 @@ PRIVATE
ui/text/text_custom_emoji.h ui/text/text_custom_emoji.h
ui/text/text_entity.cpp ui/text/text_entity.cpp
ui/text/text_entity.h ui/text/text_entity.h
ui/text/text_extended_data.cpp
ui/text/text_extended_data.h
ui/text/text_isolated_emoji.h ui/text/text_isolated_emoji.h
ui/text/text_parser.cpp ui/text/text_parser.cpp
ui/text/text_parser.h ui/text/text_parser.h
ui/text/text_renderer.cpp ui/text/text_renderer.cpp
ui/text/text_renderer.h ui/text/text_renderer.h
ui/text/text_spoiler_data.cpp
ui/text/text_spoiler_data.h
ui/text/text_utilities.cpp ui/text/text_utilities.cpp
ui/text/text_utilities.h ui/text/text_utilities.h
ui/text/text_variant.cpp ui/text/text_variant.cpp

View file

@ -6,10 +6,11 @@
// //
#include "ui/text/text.h" #include "ui/text/text.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/text/text_extended_data.h"
#include "ui/text/text_isolated_emoji.h" #include "ui/text/text_isolated_emoji.h"
#include "ui/text/text_parser.h" #include "ui/text/text_parser.h"
#include "ui/text/text_renderer.h" #include "ui/text/text_renderer.h"
#include "ui/text/text_spoiler_data.h"
#include "ui/basic_click_handlers.h" #include "ui/basic_click_handlers.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
@ -91,12 +92,19 @@ const TextParseOptions kPlainTextOptions = {
namespace Ui::Text { namespace Ui::Text {
struct SpoilerMessCache::Entry {
SpoilerMessCached mess;
QColor color;
};
SpoilerMessCache::SpoilerMessCache(int capacity) : _capacity(capacity) { SpoilerMessCache::SpoilerMessCache(int capacity) : _capacity(capacity) {
Expects(capacity > 0); Expects(capacity > 0);
_cache.reserve(capacity); _cache.reserve(capacity);
} }
SpoilerMessCache::~SpoilerMessCache() = default;
not_null<SpoilerMessCached*> SpoilerMessCache::lookup(QColor color) { not_null<SpoilerMessCached*> SpoilerMessCache::lookup(QColor color) {
for (auto &entry : _cache) { for (auto &entry : _cache) {
if (entry.color == color) { if (entry.color == color) {
@ -139,25 +147,25 @@ GeometryDescriptor SimpleGeometry(
bool elisionOneLine, bool elisionOneLine,
bool elisionBreakEverywhere) { bool elisionBreakEverywhere) {
constexpr auto wrap = []( constexpr auto wrap = [](
Fn<LineGeometry(LineGeometry, uint16)> layout, Fn<LineGeometry(LineGeometry)> layout,
bool breakEverywhere = false) { bool breakEverywhere = false) {
return GeometryDescriptor{ std::move(layout), breakEverywhere }; return GeometryDescriptor{ std::move(layout), breakEverywhere };
}; };
// Try to minimize captured values (to minimize Fn allocations). // Try to minimize captured values (to minimize Fn allocations).
if (!elisionOneLine && !elisionHeight) { if (!elisionOneLine && !elisionHeight) {
return wrap([=](LineGeometry line, uint16 positino) { return wrap([=](LineGeometry line) {
line.width = availableWidth; line.width = availableWidth;
return line; return line;
}); });
} else if (elisionOneLine) { } else if (elisionOneLine) {
return wrap([=](LineGeometry line, uint16 position) { return wrap([=](LineGeometry line) {
line.elided = true; line.elided = true;
line.width = availableWidth - elisionRemoveFromEnd; line.width = availableWidth - elisionRemoveFromEnd;
return line; return line;
}, elisionBreakEverywhere); }, elisionBreakEverywhere);
} else if (!elisionRemoveFromEnd) { } else if (!elisionRemoveFromEnd) {
return wrap([=](LineGeometry line, uint16 position) { return wrap([=](LineGeometry line) {
if (line.top + fontHeight * 2 > elisionHeight) { if (line.top + fontHeight * 2 > elisionHeight) {
line.elided = true; line.elided = true;
} }
@ -165,7 +173,7 @@ GeometryDescriptor SimpleGeometry(
return line; return line;
}); });
} else { } else {
return wrap([=](LineGeometry line, uint16 position) { return wrap([=](LineGeometry line) {
if (line.top + fontHeight * 2 > elisionHeight) { if (line.top + fontHeight * 2 > elisionHeight) {
line.elided = true; line.elided = true;
line.width = availableWidth - elisionRemoveFromEnd; line.width = availableWidth - elisionRemoveFromEnd;
@ -177,27 +185,44 @@ GeometryDescriptor SimpleGeometry(
} }
}; };
String::SpoilerDataWrap::SpoilerDataWrap() noexcept = default; String::ExtendedWrap::ExtendedWrap() noexcept = default;
String::SpoilerDataWrap::SpoilerDataWrap(SpoilerDataWrap &&other) noexcept String::ExtendedWrap::ExtendedWrap(ExtendedWrap &&other) noexcept
: data(std::move(other.data)) { : unique_ptr(std::move(other)) {
adjustFrom(&other); adjustFrom(&other);
} }
String::SpoilerDataWrap &String::SpoilerDataWrap::operator=( String::ExtendedWrap &String::ExtendedWrap::operator=(
SpoilerDataWrap &&other) noexcept { ExtendedWrap &&other) noexcept {
data = std::move(other.data); *static_cast<unique_ptr*>(this) = std::move(other);
adjustFrom(&other); adjustFrom(&other);
return *this; return *this;
} }
void String::SpoilerDataWrap::adjustFrom(const SpoilerDataWrap *other) { String::ExtendedWrap::ExtendedWrap(
if (data && data->link) { std::unique_ptr<ExtendedData> &&other) noexcept
: unique_ptr(std::move(other)) {
Assert(!get() || !get()->spoiler);
}
String::ExtendedWrap &String::ExtendedWrap::operator=(
std::unique_ptr<ExtendedData> &&other) noexcept {
*static_cast<unique_ptr*>(this) = std::move(other);
Assert(!get() || !get()->spoiler);
return *this;
}
String::ExtendedWrap::~ExtendedWrap() = default;
void String::ExtendedWrap::adjustFrom(const ExtendedWrap *other) {
const auto data = get();
if (data && data->spoiler) {
const auto raw = [](auto pointer) { const auto raw = [](auto pointer) {
return reinterpret_cast<quintptr>(pointer); return reinterpret_cast<quintptr>(pointer);
}; };
data->link->setText(reinterpret_cast<String*>( const auto otherText = raw(data->spoiler->link->text().get());
raw(data->link->text().get()) + raw(this) - raw(other))); data->spoiler->link->setText(
reinterpret_cast<String*>(otherText + raw(this) - raw(other)));
} }
} }
@ -243,31 +268,35 @@ void String::setText(const style::TextStyle &st, const QString &text, const Text
void String::recountNaturalSize( void String::recountNaturalSize(
bool initial, bool initial,
Qt::LayoutDirection optionsDirection) { Qt::LayoutDirection optionsDirection) {
NewlineBlock *lastNewline = 0; auto lastNewline = (NewlineBlock*)nullptr;
auto lastNewlineStart = 0;
const auto computeParagraphDirection = [&](int paragraphEnd) {
const auto direction = (optionsDirection != Qt::LayoutDirectionAuto)
? optionsDirection
: StringDirection(_text, lastNewlineStart, paragraphEnd);
if (lastNewline) {
lastNewline->_paragraphLTR = (direction == Qt::LeftToRight);
lastNewline->_paragraphRTL = (direction == Qt::RightToLeft);
} else {
_startParagraphLTR = (direction == Qt::LeftToRight);
_startParagraphRTL = (direction == Qt::RightToLeft);
}
};
_maxWidth = _minHeight = 0; _maxWidth = _minHeight = 0;
int32 lineHeight = 0; int32 lineHeight = 0;
int32 lastNewlineStart = 0; QFixed maxWidth = 0;
QFixed _width = 0, last_rBearing = 0, last_rPadding = 0; QFixed width = 0, last_rBearing = 0, last_rPadding = 0;
for (auto &block : _blocks) { for (auto &block : _blocks) {
auto b = block.get(); auto b = block.get();
auto _btype = b->type(); auto _btype = b->type();
auto blockHeight = CountBlockHeight(b, _st); auto blockHeight = CountBlockHeight(b, _st);
if (_btype == TextBlockType::Newline) { if (_btype == TextBlockType::Newline) {
if (!lineHeight) lineHeight = blockHeight; if (!lineHeight) {
lineHeight = blockHeight;
}
if (initial) { if (initial) {
Qt::LayoutDirection direction = optionsDirection; computeParagraphDirection(b->position());
if (direction == Qt::LayoutDirectionAuto) {
direction = StringDirection(
_text,
lastNewlineStart,
b->position());
}
if (lastNewline) {
lastNewline->_nextDirection = direction;
} else {
_startDirection = direction;
}
} }
lastNewlineStart = b->position(); lastNewlineStart = b->position();
lastNewline = &block.unsafe<NewlineBlock>(); lastNewline = &block.unsafe<NewlineBlock>();
@ -277,8 +306,8 @@ void String::recountNaturalSize(
last_rBearing = b->f_rbearing(); last_rBearing = b->f_rbearing();
last_rPadding = b->f_rpadding(); last_rPadding = b->f_rpadding();
accumulate_max(_maxWidth, _width); accumulate_max(maxWidth, width);
_width = (b->f_width() - last_rBearing); width = (b->f_width() - last_rBearing);
continue; continue;
} }
@ -291,9 +320,9 @@ void String::recountNaturalSize(
// But when we layout block and we're sure that _maxWidth is enough // But when we layout block and we're sure that _maxWidth is enough
// for all the blocks to fit on their line we check each block, even the // for all the blocks to fit on their line we check each block, even the
// intermediate one with a large negative right bearing. // intermediate one with a large negative right bearing.
accumulate_max(_maxWidth, _width); accumulate_max(maxWidth, width);
_width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing); width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing);
lineHeight = qMax(lineHeight, blockHeight); lineHeight = qMax(lineHeight, blockHeight);
last_rBearing = b__f_rbearing; last_rBearing = b__f_rbearing;
@ -301,21 +330,14 @@ void String::recountNaturalSize(
continue; continue;
} }
if (initial) { if (initial) {
Qt::LayoutDirection direction = optionsDirection; computeParagraphDirection(_text.size());
if (direction == Qt::LayoutDirectionAuto) {
direction = StringDirection(_text, lastNewlineStart, _text.size());
}
if (lastNewline) {
lastNewline->_nextDirection = direction;
} else {
_startDirection = direction;
}
} }
if (_width > 0) { if (width > 0) {
if (!lineHeight) lineHeight = CountBlockHeight(_blocks.back().get(), _st); if (!lineHeight) lineHeight = CountBlockHeight(_blocks.back().get(), _st);
_minHeight += lineHeight; _minHeight += lineHeight;
accumulate_max(_maxWidth, _width); accumulate_max(maxWidth, width);
} }
_maxWidth = maxWidth.ceil().toInt();
} }
int String::countMaxMonospaceWidth() const { int String::countMaxMonospaceWidth() const {
@ -405,13 +427,14 @@ void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &t
} }
void String::setLink(uint16 index, const ClickHandlerPtr &link) { void String::setLink(uint16 index, const ClickHandlerPtr &link) {
if (index > 0 && index <= _links.size()) { const auto extended = _extended.get();
_links[index - 1] = link; if (extended && index > 0 && index <= extended->links.size()) {
extended->links[index - 1] = link;
} }
} }
void String::setSpoilerRevealed(bool revealed, anim::type animated) { void String::setSpoilerRevealed(bool revealed, anim::type animated) {
const auto data = _spoiler.data.get(); const auto data = _extended ? _extended->spoiler.get() : nullptr;
if (!data) { if (!data) {
return; return;
} else if (data->revealed == revealed) { } else if (data->revealed == revealed) {
@ -436,19 +459,19 @@ void String::setSpoilerRevealed(bool revealed, anim::type animated) {
} }
void String::setSpoilerLinkFilter(Fn<bool(const ClickContext&)> filter) { void String::setSpoilerLinkFilter(Fn<bool(const ClickContext&)> filter) {
Expects(_spoiler.data != nullptr); Expects(_extended && _extended->spoiler);
_spoiler.data->link = std::make_shared<SpoilerClickHandler>( _extended->spoiler->link = std::make_shared<SpoilerClickHandler>(
this, this,
std::move(filter)); std::move(filter));
} }
bool String::hasLinks() const { bool String::hasLinks() const {
return !_links.isEmpty(); return _extended && !_extended->links.empty();
} }
bool String::hasSpoilers() const { bool String::hasSpoilers() const {
return (_spoiler.data != nullptr); return _extended && (_extended->spoiler != nullptr);
} }
bool String::hasSkipBlock() const { bool String::hasSkipBlock() const {
@ -490,7 +513,7 @@ bool String::removeSkipBlock() {
int String::countWidth(int width, bool breakEverywhere) const { int String::countWidth(int width, bool breakEverywhere) const {
if (QFixed(width) >= _maxWidth) { if (QFixed(width) >= _maxWidth) {
return _maxWidth.ceil().toInt(); return _maxWidth;
} }
QFixed maxLineWidth = 0; QFixed maxLineWidth = 0;
@ -822,6 +845,13 @@ bool String::isEmpty() const {
return _blocks.empty() || _blocks[0]->type() == TextBlockType::Skip; return _blocks.empty() || _blocks[0]->type() == TextBlockType::Skip;
} }
not_null<ExtendedData*> String::ensureExtended() {
if (!_extended) {
_extended = std::make_unique<ExtendedData>();
}
return _extended.get();
}
uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const { uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const {
return (i + 1 == e) ? _text.size() : (*(i + 1))->position(); return (i + 1 == e) ? _text.size() : (*(i + 1))->position();
} }
@ -857,7 +887,9 @@ void String::enumerateText(
return 0; return 0;
} }
const auto result = (*i)->linkIndex(); const auto result = (*i)->linkIndex();
return (result && _links.at(result - 1)) ? result : 0; return (result && _extended && _extended->links[result - 1])
? result
: 0;
}(); }();
if (blockLinkIndex != linkIndex) { if (blockLinkIndex != linkIndex) {
if (linkIndex) { if (linkIndex) {
@ -866,9 +898,11 @@ void String::enumerateText(
if (rangeTo > rangeFrom) { // handle click handler if (rangeTo > rangeFrom) { // handle click handler
const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom); const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
// Ignore links that are partially copied. // Ignore links that are partially copied.
const auto handler = (linkPosition != rangeFrom || blockPosition != rangeTo) const auto handler = (linkPosition != rangeFrom
|| blockPosition != rangeTo
|| !_extended)
? nullptr ? nullptr
: _links.at(linkIndex - 1); : _extended->links[linkIndex - 1];
const auto type = handler const auto type = handler
? handler->getTextEntity().type ? handler->getTextEntity().type
: EntityType::Invalid; : EntityType::Invalid;
@ -878,7 +912,9 @@ void String::enumerateText(
linkIndex = blockLinkIndex; linkIndex = blockLinkIndex;
if (linkIndex) { if (linkIndex) {
linkPosition = blockPosition; linkPosition = blockPosition;
const auto handler = _links.at(linkIndex - 1); const auto handler = _extended
? _extended->links[linkIndex - 1]
: nullptr;
clickHandlerStartCallback(handler clickHandlerStartCallback(handler
? handler->getTextEntity().type ? handler->getTextEntity().type
: EntityType::Invalid); : EntityType::Invalid);
@ -916,7 +952,7 @@ void String::enumerateText(
} }
bool String::hasPersistentAnimation() const { bool String::hasPersistentAnimation() const {
return _hasCustomEmoji || _spoiler.data; return _hasCustomEmoji || hasSpoilers();
} }
void String::unloadPersistentAnimation() { void String::unloadPersistentAnimation() {
@ -958,6 +994,11 @@ bool String::hasNotEmojiAndSpaces() const {
return _hasNotEmojiAndSpaces; return _hasNotEmojiAndSpaces;
} }
const base::flat_map<int, int> &String::modifications() const {
static const auto kEmpty = base::flat_map<int, int>();
return _extended ? _extended->modifications : kEmpty;
}
QString String::toString(TextSelection selection) const { QString String::toString(TextSelection selection) const {
return toText(selection, false, false).rich.text; return toText(selection, false, false).rich.text;
} }
@ -1141,16 +1182,13 @@ IsolatedEmoji String::toIsolatedEmoji() const {
} }
void String::clear() { void String::clear() {
clearFields();
_text.clear(); _text.clear();
}
void String::clearFields() {
_blocks.clear(); _blocks.clear();
_links.clear(); _extended = nullptr;
_spoiler.data = nullptr;
_maxWidth = _minHeight = 0; _maxWidth = _minHeight = 0;
_startDirection = Qt::LayoutDirectionAuto; _startParagraphIndex = 0;
_startParagraphLTR = false;
_startParagraphRTL = false;
} }
bool IsBad(QChar ch) { bool IsBad(QChar ch) {

View file

@ -7,12 +7,12 @@
#pragma once #pragma once
#include "ui/text/text_entity.h" #include "ui/text/text_entity.h"
#include "ui/text/text_block.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/click_handler.h" #include "ui/click_handler.h"
#include "base/flags.h" #include "base/flags.h"
#include "ui/style/style_core_types.h"
#include <crl/crl_time.h>
#include <private/qfixed_p.h>
#include <any> #include <any>
class Painter; class Painter;
@ -22,11 +22,18 @@ enum class type : uchar;
} // namespace anim } // namespace anim
namespace style { namespace style {
struct TextStyle;
struct TextPalette; struct TextPalette;
} // namespace style } // namespace style
namespace Ui { namespace Ui {
class SpoilerMessCached;
extern const QString kQEllipsis; extern const QString kQEllipsis;
inline constexpr auto kQFixedMax = (INT_MAX / 256);
} // namespace Ui } // namespace Ui
struct TextParseOptions { struct TextParseOptions {
@ -68,9 +75,12 @@ static constexpr TextSelection AllTextSelection = { 0, 0xFFFF };
namespace Ui::Text { namespace Ui::Text {
struct Block;
struct IsolatedEmoji; struct IsolatedEmoji;
struct OnlyCustomEmoji; struct OnlyCustomEmoji;
struct SpoilerData; struct SpoilerData;
struct ParagraphDetails;
struct ExtendedData;
struct StateRequest { struct StateRequest {
enum class Flag { enum class Flag {
@ -108,15 +118,13 @@ struct StateRequestElided : StateRequest {
class SpoilerMessCache { class SpoilerMessCache {
public: public:
explicit SpoilerMessCache(int capacity); explicit SpoilerMessCache(int capacity);
~SpoilerMessCache();
[[nodiscard]] not_null<SpoilerMessCached*> lookup(QColor color); [[nodiscard]] not_null<SpoilerMessCached*> lookup(QColor color);
void reset(); void reset();
private: private:
struct Entry { struct Entry;
SpoilerMessCached mess;
QColor color;
};
std::vector<Entry> _cache; std::vector<Entry> _cache;
const int _capacity = 0; const int _capacity = 0;
@ -135,7 +143,7 @@ struct LineGeometry {
bool elided = false; bool elided = false;
}; };
struct GeometryDescriptor { struct GeometryDescriptor {
Fn<LineGeometry(LineGeometry line, uint16 position)> layout; Fn<LineGeometry(LineGeometry line)> layout;
bool breakEverywhere = false; bool breakEverywhere = false;
}; };
@ -176,17 +184,17 @@ struct PaintContext {
class String { class String {
public: public:
String(int32 minResizeWidth = QFIXED_MAX); String(int minResizeWidth = kQFixedMax);
String( String(
const style::TextStyle &st, const style::TextStyle &st,
const QString &text, const QString &text,
const TextParseOptions &options = kDefaultTextOptions, const TextParseOptions &options = kDefaultTextOptions,
int32 minResizeWidth = QFIXED_MAX); int minResizeWidth = kQFixedMax);
String( String(
const style::TextStyle &st, const style::TextStyle &st,
const TextWithEntities &textWithEntities, const TextWithEntities &textWithEntities,
const TextParseOptions &options = kMarkupTextOptions, const TextParseOptions &options = kMarkupTextOptions,
int32 minResizeWidth = QFIXED_MAX, int minResizeWidth = kQFixedMax,
const std::any &context = {}); const std::any &context = {});
String(String &&other); String(String &&other);
String &operator=(String &&other); String &operator=(String &&other);
@ -238,7 +246,7 @@ public:
bool removeSkipBlock(); bool removeSkipBlock();
[[nodiscard]] int maxWidth() const { [[nodiscard]] int maxWidth() const {
return _maxWidth.ceil().toInt(); return _maxWidth;
} }
[[nodiscard]] int minHeight() const { [[nodiscard]] int minHeight() const {
return _minHeight; return _minHeight;
@ -293,9 +301,7 @@ public:
[[nodiscard]] OnlyCustomEmoji toOnlyCustomEmoji() const; [[nodiscard]] OnlyCustomEmoji toOnlyCustomEmoji() const;
[[nodiscard]] bool hasNotEmojiAndSpaces() const; [[nodiscard]] bool hasNotEmojiAndSpaces() const;
[[nodiscard]] const base::flat_map<int, int> &modifications() const { [[nodiscard]] const base::flat_map<int, int> &modifications() const;
return _modifications;
}
[[nodiscard]] const style::TextStyle *style() const { [[nodiscard]] const style::TextStyle *style() const {
return _st; return _st;
@ -305,64 +311,80 @@ public:
private: private:
using TextBlocks = std::vector<Block>; using TextBlocks = std::vector<Block>;
using TextLinks = QVector<ClickHandlerPtr>;
class SpoilerDataWrap { class ExtendedWrap : public std::unique_ptr<ExtendedData> {
public: public:
SpoilerDataWrap() noexcept; ExtendedWrap() noexcept;
SpoilerDataWrap(SpoilerDataWrap &&other) noexcept; ExtendedWrap(ExtendedWrap &&other) noexcept;
SpoilerDataWrap &operator=(SpoilerDataWrap &&other) noexcept; ExtendedWrap &operator=(ExtendedWrap &&other) noexcept;
~ExtendedWrap();
std::unique_ptr<SpoilerData> data; ExtendedWrap(
std::unique_ptr<ExtendedData> &&other) noexcept;
ExtendedWrap &operator=(
std::unique_ptr<ExtendedData> &&other) noexcept;
private: private:
void adjustFrom(const SpoilerDataWrap *other); void adjustFrom(const ExtendedWrap *other);
}; };
uint16 countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const; [[nodiscard]] not_null<ExtendedData*> ensureExtended();
uint16 countBlockLength(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const;
[[nodiscard]] uint16 countBlockEnd(
const TextBlocks::const_iterator &i,
const TextBlocks::const_iterator &e) const;
[[nodiscard]] uint16 countBlockLength(
const TextBlocks::const_iterator &i,
const TextBlocks::const_iterator &e) const;
// Template method for originalText(), originalTextWithEntities(). // Template method for originalText(), originalTextWithEntities().
template <typename AppendPartCallback, typename ClickHandlerStartCallback, typename ClickHandlerFinishCallback, typename FlagsChangeCallback> template <
void enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const; 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(). // Template method for countWidth(), countHeight(), countLineWidths().
// callback(lineWidth, lineHeight) will be called for all lines with: // callback(lineWidth, lineHeight) will be called for all lines with:
// QFixed lineWidth, int lineHeight // QFixed lineWidth, int lineHeight
template <typename Callback> template <typename Callback>
void enumerateLines(int w, bool breakEverywhere, Callback callback) const; void enumerateLines(
int w,
bool breakEverywhere,
Callback callback) const;
void recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto); void recountNaturalSize(
bool initial,
Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto);
// clear() deletes all blocks and calls this method [[nodiscard]] TextForMimeData toText(
// it is also called from move constructor / assignment operator
void clearFields();
TextForMimeData toText(
TextSelection selection, TextSelection selection,
bool composeExpanded, bool composeExpanded,
bool composeEntities) const; bool composeEntities) const;
QFixed _minResizeWidth; const style::TextStyle *_st = nullptr;
QFixed _maxWidth = 0; QString _text;
int32 _minHeight = 0; TextBlocks _blocks;
ExtendedWrap _extended;
int _minResizeWidth = 0;
int _maxWidth = 0;
int _minHeight = 0;
int16 _startParagraphIndex = 0;
bool _startParagraphLTR : 1 = false;
bool _startParagraphRTL : 1 = false;
bool _hasCustomEmoji : 1 = false; bool _hasCustomEmoji : 1 = false;
bool _isIsolatedEmoji : 1 = false; bool _isIsolatedEmoji : 1 = false;
bool _isOnlyCustomEmoji : 1 = false; bool _isOnlyCustomEmoji : 1 = false;
bool _hasNotEmojiAndSpaces : 1 = false; bool _hasNotEmojiAndSpaces : 1 = false;
QString _text;
base::flat_map<int, int> _modifications;
const style::TextStyle *_st = nullptr;
TextBlocks _blocks;
TextLinks _links;
Qt::LayoutDirection _startDirection = Qt::LayoutDirectionAuto;
SpoilerDataWrap _spoiler;
friend class Parser; friend class Parser;
friend class Renderer; friend class Renderer;

View file

@ -409,6 +409,14 @@ style::font WithFlags(
return result; return result;
} }
Qt::LayoutDirection UnpackParagraphDirection(bool ltr, bool rtl) {
return ltr
? Qt::LeftToRight
: rtl
? Qt::RightToLeft
: Qt::LayoutDirectionAuto;
}
AbstractBlock::AbstractBlock( AbstractBlock::AbstractBlock(
const style::font &font, const style::font &font,
const QString &text, const QString &text,
@ -580,10 +588,6 @@ NewlineBlock::NewlineBlock(
colorIndex) { colorIndex) {
} }
Qt::LayoutDirection NewlineBlock::nextDirection() const {
return _nextDirection;
}
SkipBlock::SkipBlock( SkipBlock::SkipBlock(
const style::font &font, const style::font &font,
const QString &text, const QString &text,

View file

@ -11,10 +11,10 @@
#include "ui/style/style_core.h" #include "ui/style/style_core.h"
#include "ui/emoji_config.h" #include "ui/emoji_config.h"
#include <private/qfixed_p.h>
#include <crl/crl_time.h> #include <crl/crl_time.h>
#include <private/qfixed_p.h>
namespace style { namespace style {
struct TextStyle; struct TextStyle;
} // namespace style } // namespace style
@ -49,6 +49,10 @@ using TextBlockFlags = base::flags<TextBlockFlag>;
TextBlockFlags flags, TextBlockFlags flags,
uint32 fontFlags = 0); uint32 fontFlags = 0);
[[nodiscard]] Qt::LayoutDirection UnpackParagraphDirection(
bool ltr,
bool rtl);
class AbstractBlock { class AbstractBlock {
public: public:
[[nodiscard]] uint16 position() const; [[nodiscard]] uint16 position() const;
@ -103,10 +107,17 @@ public:
uint16 linkIndex, uint16 linkIndex,
uint16 colorIndex); uint16 colorIndex);
[[nodiscard]] Qt::LayoutDirection nextDirection() const; [[nodiscard]] int16 paragraphIndex() const {
return _paragraphIndex;
}
[[nodiscard]] Qt::LayoutDirection paragraphDirection() const {
return UnpackParagraphDirection(_paragraphLTR, _paragraphRTL);
}
private: private:
Qt::LayoutDirection _nextDirection = Qt::LayoutDirectionAuto; int16 _paragraphIndex = 0;
bool _paragraphLTR : 1 = false;
bool _paragraphRTL : 1 = false;
friend class String; friend class String;
friend class Parser; friend class Parser;

View file

@ -4,7 +4,7 @@
// For license and copyright information please follow this link: // For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL // https://github.com/desktop-app/legal/blob/master/LEGAL
// //
#include "ui/text/text_spoiler_data.h" #include "ui/text/text_extended_data.h"
#include "ui/text/text.h" #include "ui/text/text.h"

View file

@ -42,4 +42,22 @@ struct SpoilerData {
bool revealed = false; bool revealed = false;
}; };
struct ParagraphDetails {
QString language;
ClickHandlerPtr copy;
int copyWidth = 0;
int maxWidth = 0;
int minHeight = 0;
int scrollLeft = 0;
bool blockquote = false;
bool pre = false;
};
struct ExtendedData {
std::vector<ClickHandlerPtr> links;
std::vector<ParagraphDetails> paragraphs;
std::unique_ptr<SpoilerData> spoiler;
base::flat_map<int, int> modifications;
};
} // namespace Ui::Text } // namespace Ui::Text

View file

@ -7,6 +7,7 @@
#pragma once #pragma once
#include "base/variant.h" #include "base/variant.h"
#include "ui/emoji_config.h"
namespace Ui::Text { namespace Ui::Text {

View file

@ -8,11 +8,12 @@
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "ui/integration.h" #include "ui/integration.h"
#include "ui/text/text_extended_data.h"
#include "ui/text/text_isolated_emoji.h" #include "ui/text/text_isolated_emoji.h"
#include "ui/text/text_spoiler_data.h"
#include "styles/style_basic.h" #include "styles/style_basic.h"
#include <QtCore/QUrl> #include <QtCore/QUrl>
#include <private/qfixed_p.h>
namespace Ui::Text { namespace Ui::Text {
namespace { namespace {
@ -182,7 +183,7 @@ void Parser::createBlock(int32 skipBack) {
if (_newlineAwaited) { if (_newlineAwaited) {
_newlineAwaited = false; _newlineAwaited = false;
if (!newline) { if (!newline) {
++_t->_modifications[_blockStart]; updateModifications(_blockStart, 1);
_t->_text.insert(_blockStart, QChar::LineFeed); _t->_text.insert(_blockStart, QChar::LineFeed);
createBlock(skipBack - length); createBlock(skipBack - length);
} }
@ -223,7 +224,7 @@ void Parser::createBlock(int32 skipBack) {
void Parser::createNewlineBlock(bool fromOriginalText) { void Parser::createNewlineBlock(bool fromOriginalText) {
if (!fromOriginalText) { if (!fromOriginalText) {
++_t->_modifications[_t->_text.size()]; updateModifications(_t->_text.size(), 1);
} }
_t->_text.push_back(QChar::LineFeed); _t->_text.push_back(QChar::LineFeed);
_allowDiacritic = false; _allowDiacritic = false;
@ -500,7 +501,7 @@ void Parser::parseCurrentChar() {
} }
if (skip) { if (skip) {
--_t->_modifications[_t->_text.size()]; updateModifications(_t->_text.size(), -1);
_ch = 0; _ch = 0;
_allowDiacritic = false; _allowDiacritic = false;
} else { } else {
@ -549,7 +550,7 @@ void Parser::parseEmojiFromCurrent() {
Assert(!_t->_text.isEmpty()); Assert(!_t->_text.isEmpty());
const auto last = _t->_text[_t->_text.size() - 1]; const auto last = _t->_text[_t->_text.size() - 1];
if (last.unicode() != Emoji::kPostfix) { if (last.unicode() != Emoji::kPostfix) {
++_t->_modifications[_t->_text.size()]; updateModifications(_t->_text.size(), 1);
_t->_text.push_back(QChar(Emoji::kPostfix)); _t->_text.push_back(QChar(Emoji::kPostfix));
++len; ++len;
} }
@ -579,16 +580,22 @@ bool Parser::isLinkEntity(const EntityInText &entity) const {
return ranges::find(urls, type) != std::end(urls); return ranges::find(urls, type) != std::end(urls);
} }
void Parser::updateModifications(int index, int delta) {
_t->ensureExtended()->modifications[index] += delta;
}
void Parser::parse(const TextParseOptions &options) { void Parser::parse(const TextParseOptions &options) {
skipBadEntities(); skipBadEntities();
trimSourceRange(); trimSourceRange();
_t->_text.resize(0); _t->_text.resize(0);
_t->_modifications = {}; if (_t->_extended) {
base::take(_t->_extended->modifications);
}
_t->_text.reserve(_end - _ptr); _t->_text.reserve(_end - _ptr);
if (_ptr > _start) { if (_ptr > _start) {
_t->_modifications[0] = -(_ptr - _start); updateModifications(0, -(_ptr - _start));
} }
for (; _ptr <= _end; ++_ptr) { for (; _ptr <= _end; ++_ptr) {
@ -631,7 +638,12 @@ void Parser::trimSourceRange() {
// } // }
void Parser::finalize(const TextParseOptions &options) { void Parser::finalize(const TextParseOptions &options) {
_t->_links.resize(_maxLinkIndex + _maxShiftedLinkIndex); auto links = (_maxLinkIndex || _maxShiftedLinkIndex)
? &_t->ensureExtended()->links
: nullptr;
if (links) {
links->resize(_maxLinkIndex + _maxShiftedLinkIndex);
}
auto counterCustomIndex = uint16(0); auto counterCustomIndex = uint16(0);
auto currentIndex = uint16(0); // Current the latest index of _t->_links. auto currentIndex = uint16(0); // Current the latest index of _t->_links.
struct { struct {
@ -687,8 +699,9 @@ void Parser::finalize(const TextParseOptions &options) {
} }
} }
if (block->flags() & TextBlockFlag::Spoiler) { if (block->flags() & TextBlockFlag::Spoiler) {
if (!_t->_spoiler.data) { auto &spoiler = _t->ensureExtended()->spoiler;
_t->_spoiler.data = std::make_unique<SpoilerData>( if (!spoiler) {
spoiler = std::make_unique<SpoilerData>(
Integration::Instance().createSpoilerRepaint(_context)); Integration::Instance().createSpoilerRepaint(_context));
} }
} }
@ -709,7 +722,10 @@ void Parser::finalize(const TextParseOptions &options) {
const auto handler = Integration::Instance().createLinkHandler( const auto handler = Integration::Instance().createLinkHandler(
_monos[monoIndex - 1], _monos[monoIndex - 1],
_context); _context);
_t->_links.resize(currentIndex); if (!links) {
links = &_t->ensureExtended()->links;
}
links->resize(currentIndex);
if (handler) { if (handler) {
_t->setLink(currentIndex, handler); _t->setLink(currentIndex, handler);
} }
@ -740,7 +756,9 @@ void Parser::finalize(const TextParseOptions &options) {
} }
block->setLinkIndex(usedIndex()); block->setLinkIndex(usedIndex());
_t->_links.resize(std::max(usedIndex(), uint16(_t->_links.size()))); if (links) {
links->resize(std::max(usedIndex(), uint16(links->size())));
}
const auto handler = Integration::Instance().createLinkHandler( const auto handler = Integration::Instance().createLinkHandler(
_links[realIndex - 1], _links[realIndex - 1],
_context); _context);
@ -749,10 +767,11 @@ void Parser::finalize(const TextParseOptions &options) {
} }
lastHandlerIndex.lnk = realIndex; lastHandlerIndex.lnk = realIndex;
} }
if (!_t->_hasCustomEmoji || _t->_spoiler.data) { const auto hasSpoiler = (_t->_extended && _t->_extended->spoiler);
if (!_t->_hasCustomEmoji || hasSpoiler) {
_t->_isOnlyCustomEmoji = false; _t->_isOnlyCustomEmoji = false;
} }
if (_t->_blocks.empty() || _t->_spoiler.data) { if (_t->_blocks.empty() || hasSpoiler) {
_t->_isIsolatedEmoji = false; _t->_isIsolatedEmoji = false;
} }
if (!_t->_hasNotEmojiAndSpaces && spacesCheckFrom != uint16(-1)) { if (!_t->_hasNotEmojiAndSpaces && spacesCheckFrom != uint16(-1)) {
@ -765,10 +784,12 @@ void Parser::finalize(const TextParseOptions &options) {
} }
} }
} }
_t->_links.squeeze();
_t->_blocks.shrink_to_fit();
_t->_text.squeeze(); _t->_text.squeeze();
_t->_modifications.shrink_to_fit(); _t->_blocks.shrink_to_fit();
if (const auto extended = _t->_extended.get()) {
extended->links.shrink_to_fit();
extended->modifications.shrink_to_fit();
}
} }
void Parser::computeLinkText( void Parser::computeLinkText(

View file

@ -7,6 +7,7 @@
#pragma once #pragma once
#include "ui/text/text.h" #include "ui/text/text.h"
#include "ui/text/text_block.h"
namespace Ui::Text { namespace Ui::Text {
@ -80,6 +81,8 @@ private:
QString *outLinkText, QString *outLinkText,
EntityLinkShown *outShown); EntityLinkShown *outShown);
void updateModifications(int index, int delta);
const not_null<String*> _t; const not_null<String*> _t;
const TextWithEntities _source; const TextWithEntities _source;
const std::any &_context; const std::any &_context;

View file

@ -6,7 +6,7 @@
// //
#include "ui/text/text_renderer.h" #include "ui/text/text_renderer.h"
#include "ui/text/text_spoiler_data.h" #include "ui/text/text_extended_data.h"
#include "styles/style_basic.h" #include "styles/style_basic.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
@ -155,7 +155,7 @@ bool Distinct(FixedRange a, FixedRange b) {
Renderer::Renderer(const Ui::Text::String &t) Renderer::Renderer(const Ui::Text::String &t)
: _t(&t) : _t(&t)
, _spoiler(_t->_spoiler.data.get()) { , _spoiler(_t->_extended ? _t->_extended->spoiler.get() : nullptr) {
} }
Renderer::~Renderer() { Renderer::~Renderer() {
@ -229,7 +229,11 @@ void Renderer::enumerate() {
_startTop = _y; _startTop = _y;
if ((*_t->_blocks.cbegin())->type() != TextBlockType::Newline) { if ((*_t->_blocks.cbegin())->type() != TextBlockType::Newline) {
initNextParagraph(_t->_blocks.cbegin(), _t->_startDirection); initNextParagraph(
_t->_blocks.cbegin(),
UnpackParagraphDirection(
_t->_startParagraphLTR,
_t->_startParagraphRTL));
} }
_lineHeight = 0; _lineHeight = 0;
@ -267,7 +271,7 @@ void Renderer::enumerate() {
initNextParagraph( initNextParagraph(
i + 1, i + 1,
static_cast<const NewlineBlock*>(b)->nextDirection()); static_cast<const NewlineBlock*>(b)->paragraphDirection());
longWordLine = true; longWordLine = true;
continue; continue;
@ -465,7 +469,7 @@ void Renderer::initNextLine() {
.left = 0, .left = 0,
.top = (_y - _startTop), .top = (_y - _startTop),
.width = _paragraphWidthRemaining.ceil().toInt(), .width = _paragraphWidthRemaining.ceil().toInt(),
}, _lineStart); });
_x = _startLeft + line.left; _x = _startLeft + line.left;
_y = _startTop + line.top; _y = _startTop + line.top;
_lineWidth = _wLeft = line.width; _lineWidth = _wLeft = line.width;
@ -1415,7 +1419,9 @@ void Renderer::eSetFont(const AbstractBlock *block) {
? false ? false
: (underline == st::kLinkUnderlineActive) : (underline == st::kLinkUnderlineActive)
? ((_palette && _palette->linkAlwaysActive) ? ((_palette && _palette->linkAlwaysActive)
|| ClickHandler::showAsActive(_t->_links.at(index - 1))) || ClickHandler::showAsActive(_t->_extended
? _t->_extended->links[index - 1]
: nullptr))
: true; : true;
return underlined ? _t->_st->font->underline() : _t->_st->font; return underlined ? _t->_st->font->underline() : _t->_st->font;
} }
@ -2016,8 +2022,10 @@ void Renderer::applyBlockProperties(const AbstractBlock *block) {
if (isMono if (isMono
&& block->linkIndex() && block->linkIndex()
&& (!_background.spoiler || _spoiler->revealed)) { && (!_background.spoiler || _spoiler->revealed)) {
_background.selectActiveBlock = ClickHandler::showAsPressed( const auto pressed = ClickHandler::showAsPressed(_t->_extended
_t->_links.at(block->linkIndex() - 1)); ? _t->_extended->links[block->linkIndex() - 1]
: nullptr);
_background.selectActiveBlock = pressed;
} }
if (const auto color = block->colorIndex()) { if (const auto color = block->colorIndex()) {
@ -2050,9 +2058,9 @@ ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const {
&& (block->flags() & TextBlockFlag::Spoiler)) && (block->flags() & TextBlockFlag::Spoiler))
? _spoiler->link ? _spoiler->link
: ClickHandlerPtr(); : ClickHandlerPtr();
return (spoilerLink || !block->linkIndex()) return (spoilerLink || !block->linkIndex() || !_t->_extended)
? spoilerLink ? spoilerLink
: _t->_links.at(block->linkIndex() - 1); : _t->_extended->links[block->linkIndex() - 1];
} }
} // namespace Ui::Text } // namespace Ui::Text

View file

@ -7,6 +7,8 @@
#pragma once #pragma once
#include "ui/text/text.h" #include "ui/text/text.h"
#include "ui/text/text_block.h"
#include "ui/text/text_custom_emoji.h"
#include <private/qtextengine_p.h> #include <private/qtextengine_p.h>
@ -15,6 +17,8 @@ struct QScriptLine;
namespace Ui::Text { namespace Ui::Text {
struct AbstractBlock;
struct FixedRange { struct FixedRange {
QFixed from; QFixed from;
QFixed till; QFixed till;

View file

@ -45,7 +45,7 @@ Widget::Widget(QWidget *parent, const Config &config)
, _maxTextWidth(widthWithoutPadding(_st->maxWidth)) , _maxTextWidth(widthWithoutPadding(_st->maxWidth))
, _maxTextHeight( , _maxTextHeight(
config.st->style.font->height * (_multiline ? config.maxLines : 1)) config.st->style.font->height * (_multiline ? config.maxLines : 1))
, _text(_multiline ? widthWithoutPadding(config.st->minWidth) : QFIXED_MAX) , _text(_multiline ? widthWithoutPadding(config.st->minWidth) : kQFixedMax)
, _clickHandlerFilter(config.filter) { , _clickHandlerFilter(config.filter) {
const auto toastOptions = TextParseOptions{ const auto toastOptions = TextParseOptions{
TextParseMultiline, TextParseMultiline,

View file

@ -544,7 +544,7 @@ int Checkbox::countTextMinWidth() const {
+ _st.textPosition.x(); + _st.textPosition.x();
return (_st.width > 0) return (_st.width > 0)
? std::max(_st.width - leftSkip, 1) ? std::max(_st.width - leftSkip, 1)
: QFIXED_MAX; : kQFixedMax;
} }
QRect Checkbox::checkRect() const { QRect Checkbox::checkRect() const {

View file

@ -199,7 +199,7 @@ FlatLabel::FlatLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
const style::PopupMenu &stMenu) const style::PopupMenu &stMenu)
: RpWidget(parent) : RpWidget(parent)
, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _text(st.minWidth ? st.minWidth : kQFixedMax)
, _st(st) , _st(st)
, _stMenu(stMenu) { , _stMenu(stMenu) {
init(); init();
@ -211,7 +211,7 @@ FlatLabel::FlatLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
const style::PopupMenu &stMenu) const style::PopupMenu &stMenu)
: RpWidget(parent) : RpWidget(parent)
, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _text(st.minWidth ? st.minWidth : kQFixedMax)
, _st(st) , _st(st)
, _stMenu(stMenu) { , _stMenu(stMenu) {
setText(text); setText(text);
@ -224,7 +224,7 @@ FlatLabel::FlatLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
const style::PopupMenu &stMenu) const style::PopupMenu &stMenu)
: RpWidget(parent) : RpWidget(parent)
, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _text(st.minWidth ? st.minWidth : kQFixedMax)
, _st(st) , _st(st)
, _stMenu(stMenu) { , _stMenu(stMenu) {
textUpdated(); textUpdated();
@ -242,7 +242,7 @@ FlatLabel::FlatLabel(
const style::FlatLabel &st, const style::FlatLabel &st,
const style::PopupMenu &stMenu) const style::PopupMenu &stMenu)
: RpWidget(parent) : RpWidget(parent)
, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _text(st.minWidth ? st.minWidth : kQFixedMax)
, _st(st) , _st(st)
, _stMenu(stMenu) , _stMenu(stMenu)
, _touchSelectTimer([=] { touchSelect(); }) { , _touchSelectTimer([=] { touchSelect(); }) {

View file

@ -7,6 +7,7 @@
#include "ui/widgets/time_input.h" #include "ui/widgets/time_input.h"
#include "ui/widgets/fields/time_part_input.h" #include "ui/widgets/fields/time_part_input.h"
#include "base/qt/qt_string_view.h"
#include "base/invoke_queued.h" #include "base/invoke_queued.h"
#include <QtCore/QRegularExpression> #include <QtCore/QRegularExpression>

View file

@ -159,7 +159,8 @@ void Tooltip::paintEvent(QPaintEvent *e) {
p.fillRect(QRect(0, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder); p.fillRect(QRect(0, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
p.fillRect(QRect(width() - st::lineWidth, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder); p.fillRect(QRect(width() - st::lineWidth, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
} }
int32 lines = qFloor((height() - 2 * st::lineWidth - _st->textPadding.top() - _st->textPadding.bottom()) / _st->textStyle.font->height); const auto lines = (height() - 2 * st::lineWidth - _st->textPadding.top() - _st->textPadding.bottom())
/ _st->textStyle.font->height;
p.setPen(_st->textFg); p.setPen(_st->textFg);
_text.drawElided(p, st::lineWidth + _st->textPadding.left(), st::lineWidth + _st->textPadding.top(), width() - 2 * st::lineWidth - _st->textPadding.left() - _st->textPadding.right(), lines); _text.drawElided(p, st::lineWidth + _st->textPadding.left(), st::lineWidth + _st->textPadding.top(), width() - 2 * st::lineWidth - _st->textPadding.left() - _st->textPadding.right(), lines);