Created RichText namespace, and tweaked scrollbar behaviour
This commit is contained in:
parent
e0143f49f0
commit
435d9346a9
17 changed files with 163 additions and 139 deletions
0
src/RichText/ParagraphStyleData.cpp
Normal file
0
src/RichText/ParagraphStyleData.cpp
Normal file
6
src/RichText/ParagraphStyleData.hpp
Normal file
6
src/RichText/ParagraphStyleData.hpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
struct ParagraphStyleData {
|
||||
|
||||
};
|
||||
|
||||
907
src/RichText/TextBox.cpp
Normal file
907
src/RichText/TextBox.cpp
Normal file
|
|
@ -0,0 +1,907 @@
|
|||
#include "TextBox.hpp"
|
||||
#include "TextStyleModifier.hpp"
|
||||
#include <limits>
|
||||
#include <modules/skparagraph/include/DartTypes.h>
|
||||
#include <modules/skparagraph/include/Metrics.h>
|
||||
#include <modules/skparagraph/src/TextLine.h>
|
||||
#include <modules/skunicode/include/SkUnicode.h>
|
||||
#include <modules/skunicode/include/SkUnicode_icu.h>
|
||||
#include <modules/skparagraph/src/ParagraphBuilderImpl.h>
|
||||
#include <modules/skparagraph/src/ParagraphImpl.h>
|
||||
#include <Helpers/MathExtras.hpp>
|
||||
#include <src/base/SkUTF.h>
|
||||
#include <cereal/types/string.hpp>
|
||||
|
||||
namespace RichText {
|
||||
|
||||
void TextData::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(paragraphs);
|
||||
a(static_cast<uint32_t>(tStyleMods.size()));
|
||||
for(auto& [pos, modsInPos] : tStyleMods) {
|
||||
a(pos, static_cast<uint16_t>(modsInPos.size()));
|
||||
for(auto& [modType, modifier] : modsInPos)
|
||||
a(modType, *modifier);
|
||||
}
|
||||
}
|
||||
|
||||
void TextData::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(paragraphs);
|
||||
uint32_t tStyleModCount;
|
||||
a(tStyleModCount);
|
||||
tStyleMods.clear();
|
||||
for(uint32_t i = 0; i < tStyleModCount; i++) {
|
||||
auto& modsInPos = tStyleMods.emplace_back();
|
||||
uint16_t modsInPosSize;
|
||||
a(modsInPos.pos, modsInPosSize);
|
||||
for(uint16_t j = 0; j < modsInPosSize; j++) {
|
||||
TextStyleModifier::ModifierType modType;
|
||||
a(modType);
|
||||
auto modifier = TextStyleModifier::allocate_modifier(modType);
|
||||
a(*modifier);
|
||||
modsInPos.mods[modType] = modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TextPosition::operator<(const TextPosition& o) const {
|
||||
return (this->fParagraphIndex < o.fParagraphIndex) || (this->fParagraphIndex == o.fParagraphIndex && this->fTextByteIndex < o.fTextByteIndex);
|
||||
}
|
||||
|
||||
bool TextPosition::operator>(const TextPosition& o) const {
|
||||
return !(*this < o) && !(*this == o);
|
||||
}
|
||||
|
||||
bool TextPosition::operator>=(const TextPosition& o) const {
|
||||
return (*this > o) || (*this == o);
|
||||
}
|
||||
|
||||
bool TextPosition::operator<=(const TextPosition& o) const {
|
||||
return (*this < o) || (*this == o);
|
||||
}
|
||||
|
||||
TextBox::TextBox() {
|
||||
paragraphs.emplace_back();
|
||||
}
|
||||
|
||||
void TextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift, const std::optional<TextStyleModifier::ModifierMap>& inputModMap) {
|
||||
bool moved = false;
|
||||
|
||||
switch(in) {
|
||||
case InputKey::LEFT:
|
||||
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::LEFT_WORD : Movement::LEFT, cur.pos, nullptr, true);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::RIGHT:
|
||||
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::RIGHT_WORD : Movement::RIGHT, cur.pos, nullptr, true);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::UP:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos, &cur.previousX);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::DOWN:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos, &cur.previousX);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::HOME:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::HOME, cur.pos);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::END:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::END, cur.pos);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::BACKSPACE:
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.pos, move(ctrl ? Movement::LEFT_WORD : Movement::LEFT, cur.pos));
|
||||
break;
|
||||
case InputKey::DELETE:
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
else
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.pos, move(ctrl ? Movement::RIGHT_WORD : Movement::RIGHT, cur.pos));
|
||||
break;
|
||||
case InputKey::ENTER:
|
||||
process_text_input(cur, "\n", inputModMap);
|
||||
break;
|
||||
case InputKey::TAB:
|
||||
process_text_input(cur, "\t", inputModMap);
|
||||
break;
|
||||
case InputKey::SELECT_ALL:
|
||||
cur.selectionBeginPos = move(Movement::HOME, cur.pos);
|
||||
cur.pos = cur.selectionEndPos = move(Movement::END, cur.pos);
|
||||
break;
|
||||
}
|
||||
|
||||
if(moved && !shift)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
|
||||
if(in != InputKey::UP && in != InputKey::DOWN)
|
||||
cur.previousX = std::nullopt;
|
||||
|
||||
inputChangedTextBox = true;
|
||||
}
|
||||
|
||||
void TextBox::process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift) {
|
||||
if(clicked || held) {
|
||||
rebuild();
|
||||
|
||||
Cursor oldCursor = cur;
|
||||
|
||||
cur.pos = get_text_pos_closest_to_point(pos);
|
||||
cur.selectionBeginPos = cur.pos;
|
||||
if(clicked && !shift)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
cur.previousX = std::nullopt;
|
||||
|
||||
inputChangedTextBox |= (oldCursor != cur);
|
||||
}
|
||||
}
|
||||
|
||||
std::string TextBox::process_copy(Cursor& cur) {
|
||||
return get_text_between(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
}
|
||||
|
||||
std::string TextBox::process_cut(Cursor& cur) {
|
||||
std::string toRet = process_copy(cur);
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos) {
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.previousX = std::nullopt;
|
||||
inputChangedTextBox = true;
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void TextBox::process_text_input(Cursor& cur, const std::string& in, const std::optional<TextStyleModifier::ModifierMap>& inputModMap) {
|
||||
if(!in.empty()) {
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = insert(cur.pos, in, inputModMap);
|
||||
cur.previousX = std::nullopt;
|
||||
inputChangedTextBox = true;
|
||||
}
|
||||
}
|
||||
|
||||
size_t TextBox::count_grapheme(const std::string& text) {
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes);
|
||||
breakIterator->setText(text.c_str(), text.size());
|
||||
size_t count = 0;
|
||||
while(!breakIterator->isDone()) {
|
||||
breakIterator->next();
|
||||
count++;
|
||||
}
|
||||
return count - 1;
|
||||
}
|
||||
|
||||
size_t TextBox::next_grapheme(const std::string& text, size_t textBytePos) {
|
||||
if(textBytePos == text.size())
|
||||
return text.size();
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes);
|
||||
breakIterator->setText(text.c_str() + textBytePos, text.size() - textBytePos);
|
||||
size_t count = breakIterator->next();
|
||||
return count + textBytePos;
|
||||
}
|
||||
|
||||
size_t TextBox::prev_grapheme(const std::string& text, size_t textBytePos) {
|
||||
if(textBytePos == 0)
|
||||
return 0;
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes);
|
||||
breakIterator->setText(text.c_str(), text.size());
|
||||
size_t toRet = 0;
|
||||
for(;;) {
|
||||
size_t potentialNext = breakIterator->next();
|
||||
if(potentialNext >= textBytePos)
|
||||
return toRet;
|
||||
toRet = potentialNext;
|
||||
}
|
||||
}
|
||||
|
||||
TextPosition TextBox::get_text_pos_from_byte_pos(const std::string& text, size_t textBytePos) {
|
||||
TextPosition toRet{0, 0};
|
||||
for(size_t i = 0; i < textBytePos; i++) {
|
||||
char c = text[i];
|
||||
if(c == '\n') {
|
||||
toRet.fTextByteIndex = 0;
|
||||
toRet.fParagraphIndex++;
|
||||
}
|
||||
else
|
||||
toRet.fTextByteIndex++;
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
size_t TextBox::get_byte_pos_from_text_pos(TextPosition textPos) {
|
||||
size_t toRet = 0;
|
||||
for(size_t pIndex = 0; pIndex < textPos.fParagraphIndex; pIndex++)
|
||||
toRet += paragraphs[pIndex].text.size() + 1; // Add 1 byte for newline
|
||||
toRet += textPos.fTextByteIndex;
|
||||
return toRet;
|
||||
}
|
||||
|
||||
TextPosition TextBox::get_text_pos_closest_to_point(Vector2f point) {
|
||||
rebuild();
|
||||
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
|
||||
size_t pIndex = 0;
|
||||
|
||||
for(; pIndex < (paragraphs.size() - 1) && point.y() > paragraphs[pIndex].p->getHeight(); pIndex++)
|
||||
point.y() -= paragraphs[pIndex].p->getHeight();
|
||||
|
||||
ParagraphData& pData = paragraphs[pIndex];
|
||||
|
||||
bool foundGlyph = pData.p->getClosestGlyphClusterAt(point.x(), point.y(), &glyphInfo);
|
||||
if(foundGlyph) {
|
||||
TextPosition toRet = {pIndex, 0};
|
||||
toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start;
|
||||
|
||||
if(glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr) {
|
||||
if(std::abs(glyphInfo.fBounds.right() - point.x()) < std::abs(glyphInfo.fBounds.left() - point.x()))
|
||||
toRet.fTextByteIndex = next_grapheme(pData.text, toRet.fTextByteIndex);
|
||||
}
|
||||
else {
|
||||
if(std::abs(glyphInfo.fBounds.left() - point.x()) < std::abs(glyphInfo.fBounds.right() - point.x()))
|
||||
toRet.fTextByteIndex = next_grapheme(pData.text, toRet.fTextByteIndex);
|
||||
}
|
||||
|
||||
// Space at the end of a line will bring the cursor to the next line, check if the cursor moved to the next line
|
||||
if(toRet.fTextByteIndex != pData.text.size()) {
|
||||
skia::textlayout::LineMetrics lineMetrics;
|
||||
int lineNumber = pData.p->getLineNumberAt(toRet.fTextByteIndex);
|
||||
if(lineNumber > 0) {
|
||||
bool lineMetricsFound = pData.p->getLineMetricsAt(lineNumber, &lineMetrics);
|
||||
if(lineMetricsFound) {
|
||||
float top = lineMetrics.fBaseline - lineMetrics.fAscent;
|
||||
if(point.y() < top)
|
||||
toRet.fTextByteIndex = prev_grapheme(pData.text, toRet.fTextByteIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toRet;
|
||||
}
|
||||
else
|
||||
return TextPosition{pIndex, 0};
|
||||
}
|
||||
|
||||
TextData TextBox::get_rich_text_data() {
|
||||
TextData toRet;
|
||||
for(auto& p : paragraphs) {
|
||||
toRet.paragraphs.emplace_back();
|
||||
toRet.paragraphs.back().text = p.text;
|
||||
}
|
||||
toRet.tStyleMods = tStyleMods;
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void TextBox::clear_text() {
|
||||
paragraphs.clear();
|
||||
tStyleMods.clear();
|
||||
paragraphs.emplace_back();
|
||||
}
|
||||
|
||||
void TextBox::set_string(const std::string& str) {
|
||||
clear_text();
|
||||
insert({0, 0}, str);
|
||||
}
|
||||
|
||||
void TextBox::set_rich_text_data(const TextData& richText) {
|
||||
paragraphs.clear();
|
||||
for(auto& p : richText.paragraphs) {
|
||||
paragraphs.emplace_back();
|
||||
paragraphs.back().text = p.text;
|
||||
}
|
||||
if(richText.paragraphs.empty())
|
||||
paragraphs.emplace_back();
|
||||
tStyleMods = richText.tStyleMods;
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
|
||||
std::string TextBox::get_string() {
|
||||
return get_text_between({0, 0}, move(TextBox::Movement::END, {0, 0}));
|
||||
}
|
||||
|
||||
std::string TextBox::get_text_between(TextPosition p1, TextPosition p2) {
|
||||
p1 = move(Movement::NOWHERE, p1);
|
||||
p2 = move(Movement::NOWHERE, p2);
|
||||
|
||||
if(p1 == p2)
|
||||
return "";
|
||||
|
||||
TextPosition start = std::min(p1, p2);
|
||||
TextPosition end = std::max(p1, p2);
|
||||
|
||||
std::string toRet;
|
||||
|
||||
for(size_t pIndex = start.fParagraphIndex; pIndex <= end.fParagraphIndex; pIndex++) {
|
||||
size_t textIndexStart = pIndex == start.fParagraphIndex ? start.fTextByteIndex : 0;
|
||||
size_t textIndexEnd = pIndex == end.fParagraphIndex ? end.fTextByteIndex : paragraphs[pIndex].text.size();
|
||||
toRet += paragraphs[pIndex].text.substr(textIndexStart, textIndexEnd - textIndexStart);
|
||||
if(pIndex != end.fParagraphIndex)
|
||||
toRet += '\n';
|
||||
}
|
||||
|
||||
return toRet;
|
||||
}
|
||||
|
||||
|
||||
void TextBox::set_initial_text_style(const skia::textlayout::TextStyle& tStyle) {
|
||||
if(!tStyle.equals(initialTStyle)) {
|
||||
initialTStyle = tStyle;
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::set_initial_text_style_modifier(const std::shared_ptr<TextStyleModifier>& modifier) {
|
||||
if(tStyleMods.empty() || tStyleMods[0].pos != TextPosition{0, 0}) {
|
||||
if(tStyleMods.empty())
|
||||
tStyleMods.emplace_back();
|
||||
else
|
||||
tStyleMods.insert(tStyleMods.begin(), {});
|
||||
tStyleMods[0].pos = {0, 0};
|
||||
}
|
||||
tStyleMods[0].mods[modifier->get_type()] = modifier;
|
||||
remove_duplicate_text_style_mods();
|
||||
}
|
||||
|
||||
TextStyleModifier::ModifierMap TextBox::get_mods_used_at_pos(TextPosition p) {
|
||||
p = move(Movement::NOWHERE, p);
|
||||
|
||||
TextStyleModifier::ModifierMap toRet = TextStyleModifier::get_default_modifiers();
|
||||
|
||||
for(auto& [pos, tStyleModInPos] : tStyleMods) {
|
||||
if(pos <= p) {
|
||||
for(auto& [modType, tStyle] : tStyleModInPos)
|
||||
toRet[modType] = tStyle;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return toRet;
|
||||
}
|
||||
|
||||
std::shared_ptr<TextStyleModifier> TextBox::get_last_text_style_mod_before_pos(TextPosition pos, TextStyleModifier::ModifierType modType) {
|
||||
std::shared_ptr<TextStyleModifier> lastModOfThisTypeBeforeEnd = TextStyleModifier::get_default_modifier(modType);
|
||||
for(auto& [tPos, tStyleModsInPos] : tStyleMods) {
|
||||
if(tPos <= pos) {
|
||||
auto containedOfSameType = tStyleModsInPos.find(modType);
|
||||
if(containedOfSameType != tStyleModsInPos.end())
|
||||
lastModOfThisTypeBeforeEnd = containedOfSameType->second;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
return lastModOfThisTypeBeforeEnd;
|
||||
}
|
||||
|
||||
void TextBox::erase_if_over_all_styles_until_pos(TextPosition pos, const std::function<bool(TextPosition, const std::shared_ptr<TextStyleModifier>&)>& func) {
|
||||
for(auto& [tPos, tStyleModsInPos] : tStyleMods) {
|
||||
if(tPos <= pos) {
|
||||
std::erase_if(tStyleModsInPos, [&](auto& item) {
|
||||
return func(tPos, item.second);
|
||||
});
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr<TextStyleModifier>& modifier) {
|
||||
p1 = move(Movement::NOWHERE, p1);
|
||||
p2 = move(Movement::NOWHERE, p2);
|
||||
if(p1 != p2) {
|
||||
TextPosition start = std::min(p1, p2);
|
||||
TextPosition end = std::max(p1, p2);
|
||||
auto lastModOfThisTypeBeforeEnd = get_last_text_style_mod_before_pos(end, modifier->get_type());
|
||||
assert(lastModOfThisTypeBeforeEnd != nullptr);
|
||||
erase_if_over_all_styles_until_pos(end, [&](TextPosition p, const std::shared_ptr<TextStyleModifier>& modToCheck) {
|
||||
return p >= start && modifier->get_type() == modToCheck->get_type();
|
||||
});
|
||||
insert_style_at_pos(start, modifier);
|
||||
insert_style_at_pos(end, lastModOfThisTypeBeforeEnd); // Even if the end is the literal end of the text (which wont be seen), the end style must still be placed at the end so that it can merge with and delete the start style when the styled text is erased
|
||||
remove_duplicate_text_style_mods();
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::insert_style_at_pos(TextPosition pos, const std::shared_ptr<TextStyleModifier>& modifier) {
|
||||
size_t indexToPlaceAt = 0;
|
||||
bool finishedInsertingStyle = false;
|
||||
for(auto& [tPos, tStyleModsInPos] : tStyleMods) {
|
||||
if(tPos > pos)
|
||||
break;
|
||||
else if(tPos == pos) {
|
||||
tStyleModsInPos[modifier->get_type()] = modifier;
|
||||
finishedInsertingStyle = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
indexToPlaceAt++;
|
||||
}
|
||||
if(!finishedInsertingStyle) {
|
||||
auto it = tStyleMods.insert(tStyleMods.begin() + indexToPlaceAt, PositionedTextStyleMod{});
|
||||
it->pos = pos;
|
||||
it->mods[modifier->get_type()] = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
TextPosition TextBox::move(Movement movement, TextPosition pos, std::optional<float>* previousX, bool flipDependingOnTextDirection) {
|
||||
if(pos.fParagraphIndex >= paragraphs.size()) {
|
||||
pos.fParagraphIndex = paragraphs.size() - 1;
|
||||
pos.fTextByteIndex = paragraphs[pos.fParagraphIndex].text.size();
|
||||
}
|
||||
|
||||
if(pos.fTextByteIndex >= paragraphs[pos.fParagraphIndex].text.size())
|
||||
pos.fTextByteIndex = paragraphs[pos.fParagraphIndex].text.size();
|
||||
|
||||
if(movement != Movement::NOWHERE && movement != Movement::HOME && movement != Movement::END) {
|
||||
rebuild();
|
||||
if(flipDependingOnTextDirection && paragraphs[pos.fParagraphIndex].pStyle.getTextDirection() == skia::textlayout::TextDirection::kRtl) {
|
||||
if(movement == Movement::LEFT)
|
||||
movement = Movement::RIGHT;
|
||||
else if(movement == Movement::RIGHT)
|
||||
movement = Movement::LEFT;
|
||||
else if(movement == Movement::LEFT_WORD)
|
||||
movement = Movement::RIGHT_WORD;
|
||||
else if(movement == Movement::RIGHT_WORD)
|
||||
movement = Movement::LEFT_WORD;
|
||||
}
|
||||
}
|
||||
|
||||
switch (movement) {
|
||||
case Movement::NOWHERE:
|
||||
break;
|
||||
case Movement::LEFT: {
|
||||
if(pos.fTextByteIndex == 0) {
|
||||
if(pos.fParagraphIndex != 0) {
|
||||
pos.fParagraphIndex--;
|
||||
pos.fTextByteIndex = paragraphs[pos.fParagraphIndex].text.size();
|
||||
}
|
||||
}
|
||||
else
|
||||
pos.fTextByteIndex = prev_grapheme(paragraphs[pos.fParagraphIndex].text, pos.fTextByteIndex);
|
||||
break;
|
||||
}
|
||||
case Movement::RIGHT: {
|
||||
if(pos.fTextByteIndex == paragraphs[pos.fParagraphIndex].text.size()) {
|
||||
if(pos.fParagraphIndex != paragraphs.size() - 1) {
|
||||
pos.fParagraphIndex++;
|
||||
pos.fTextByteIndex = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
pos.fTextByteIndex = next_grapheme(paragraphs[pos.fParagraphIndex].text, pos.fTextByteIndex);
|
||||
break;
|
||||
}
|
||||
case Movement::UP: {
|
||||
assert(previousX != nullptr);
|
||||
|
||||
int lineNumber = paragraphs[pos.fParagraphIndex].p->getLineNumberAt(pos.fTextByteIndex == paragraphs[pos.fParagraphIndex].text.size() ? prev_grapheme(paragraphs[pos.fParagraphIndex].text, pos.fTextByteIndex) : pos.fTextByteIndex);
|
||||
if(lineNumber <= 0 && pos.fParagraphIndex == 0) {
|
||||
pos.fTextByteIndex = 0;
|
||||
if(previousX != nullptr)
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
}
|
||||
else {
|
||||
if(lineNumber <= 0 && paragraphs[pos.fParagraphIndex - 1].text.empty()) {
|
||||
if(previousX != nullptr && !previousX->has_value())
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
pos = TextPosition{pos.fParagraphIndex - 1, 0};
|
||||
}
|
||||
else {
|
||||
skia::textlayout::LineMetrics prevLineMetrics;
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
if(lineNumber <= 0) {
|
||||
pos.fParagraphIndex--;
|
||||
lineNumber = paragraphs[pos.fParagraphIndex].p->lineNumber();
|
||||
}
|
||||
paragraphs[pos.fParagraphIndex].p->getLineMetricsAt(lineNumber - 1, &prevLineMetrics);
|
||||
Vector2f pointToCheck = {(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), prevLineMetrics.fBaseline + paragraphs[pos.fParagraphIndex].heightOffset};
|
||||
pos = get_text_pos_closest_to_point(pointToCheck);
|
||||
|
||||
if(previousX != nullptr && !previousX->has_value())
|
||||
*previousX = pointToCheck.x();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::DOWN: {
|
||||
assert(previousX != nullptr);
|
||||
|
||||
int lineNumber = paragraphs[pos.fParagraphIndex].p->getLineNumberAt(pos.fTextByteIndex);
|
||||
if((lineNumber == -1 || lineNumber == static_cast<int>(paragraphs[pos.fParagraphIndex].p->lineNumber() - 1)) && pos.fParagraphIndex == (paragraphs.size() - 1)) {
|
||||
pos.fTextByteIndex = paragraphs[pos.fParagraphIndex].text.size();
|
||||
if(previousX != nullptr)
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
}
|
||||
else {
|
||||
if((lineNumber == -1 || lineNumber == static_cast<int>(paragraphs[pos.fParagraphIndex].p->lineNumber() - 1)) && paragraphs[pos.fParagraphIndex + 1].text.empty()) {
|
||||
if(previousX != nullptr && !previousX->has_value())
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
pos = TextPosition{pos.fParagraphIndex + 1, 0};
|
||||
}
|
||||
else {
|
||||
skia::textlayout::LineMetrics nextLineMetrics;
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
if(lineNumber == -1 || lineNumber == static_cast<int>(paragraphs[pos.fParagraphIndex].p->lineNumber() - 1)) {
|
||||
pos.fParagraphIndex++;
|
||||
lineNumber = -1;
|
||||
}
|
||||
paragraphs[pos.fParagraphIndex].p->getLineMetricsAt(lineNumber + 1, &nextLineMetrics);
|
||||
Vector2f pointToCheck = {(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), nextLineMetrics.fBaseline + paragraphs[pos.fParagraphIndex].heightOffset};
|
||||
pos = get_text_pos_closest_to_point(pointToCheck);
|
||||
|
||||
if(previousX != nullptr && !previousX->has_value())
|
||||
*previousX = pointToCheck.x();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::LEFT_WORD: {
|
||||
if(pos == TextPosition{0, 0})
|
||||
break;
|
||||
|
||||
std::string fullText = get_string();
|
||||
size_t bytePosInFullText = get_byte_pos_from_text_pos(pos);
|
||||
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
|
||||
size_t byteIndexToRet = 0;
|
||||
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
||||
breakIterator->setText(fullText.c_str(), fullText.size());
|
||||
|
||||
while(!breakIterator->isDone()) {
|
||||
size_t nextByteIndex = breakIterator->next();
|
||||
if(nextByteIndex >= bytePosInFullText)
|
||||
return get_text_pos_from_byte_pos(fullText, byteIndexToRet);
|
||||
|
||||
const char* tPtr = fullText.c_str() + nextByteIndex;
|
||||
SkUnichar u = SkUTF::NextUTF8(&tPtr, fullText.c_str() + fullText.size());
|
||||
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
||||
byteIndexToRet = nextByteIndex;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::RIGHT_WORD: {
|
||||
TextPosition endPos = move(Movement::END, {0, 0});
|
||||
if(pos == endPos)
|
||||
break;
|
||||
|
||||
std::string fullText = get_string();
|
||||
size_t bytePosInFullText = get_byte_pos_from_text_pos(pos);
|
||||
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
||||
|
||||
breakIterator->setText(fullText.c_str() + bytePosInFullText, fullText.size() - bytePosInFullText);
|
||||
|
||||
while(!breakIterator->isDone()) {
|
||||
size_t p = breakIterator->next() + bytePosInFullText;
|
||||
if(p == fullText.size())
|
||||
return get_text_pos_from_byte_pos(fullText, p);
|
||||
const char* tPtr = fullText.c_str() + p;
|
||||
SkUnichar u = SkUTF::NextUTF8(&tPtr, fullText.c_str() + fullText.size());
|
||||
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
||||
return get_text_pos_from_byte_pos(fullText, p);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Movement::HOME:
|
||||
pos.fTextByteIndex = 0;
|
||||
pos.fParagraphIndex = 0;
|
||||
break;
|
||||
case Movement::END:
|
||||
pos.fTextByteIndex = paragraphs.back().text.size();
|
||||
pos.fParagraphIndex = paragraphs.size() - 1;
|
||||
break;
|
||||
}
|
||||
return pos;
|
||||
|
||||
}
|
||||
|
||||
void TextBox::set_width(float newWidth) {
|
||||
if(width != newWidth) {
|
||||
width = std::max(newWidth, 4.0f);
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::set_allow_newlines(bool allow) {
|
||||
newlinesAllowed = allow;
|
||||
}
|
||||
|
||||
void TextBox::set_font_collection(const sk_sp<skia::textlayout::FontCollection>& fC) {
|
||||
if(fontCollection != fC) {
|
||||
fontCollection = fC;
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::rebuild() {
|
||||
if(needsRebuild) {
|
||||
float heightOffset = 0.0f;
|
||||
|
||||
auto nextTStyleModIt = tStyleMods.begin();
|
||||
|
||||
skia::textlayout::TextStyle tStyle = initialTStyle;
|
||||
|
||||
for(size_t pIndex = 0; pIndex < paragraphs.size(); pIndex++) {
|
||||
ParagraphData& pData = paragraphs[pIndex];
|
||||
pData.pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft);
|
||||
pData.pStyle.setTextDirection(skia::textlayout::TextDirection::kLtr);
|
||||
pData.pStyle.setTextStyle(tStyle);
|
||||
size_t tIndex = 0;
|
||||
skia::textlayout::ParagraphBuilderImpl a(pData.pStyle, fontCollection, SkUnicodes::ICU::Make());
|
||||
|
||||
for(;;) {
|
||||
if(nextTStyleModIt == tStyleMods.end() || nextTStyleModIt->pos.fParagraphIndex != pIndex) {
|
||||
a.pushStyle(tStyle);
|
||||
a.addText(pData.text.c_str() + tIndex, pData.text.length() - tIndex);
|
||||
a.pop();
|
||||
break;
|
||||
}
|
||||
else {
|
||||
auto& nextTStyleMod = *nextTStyleModIt;
|
||||
|
||||
// Print with previous style
|
||||
|
||||
a.pushStyle(tStyle);
|
||||
a.addText(pData.text.c_str() + tIndex, nextTStyleMod.pos.fTextByteIndex - tIndex);
|
||||
a.pop();
|
||||
tIndex = nextTStyleMod.pos.fTextByteIndex;
|
||||
|
||||
// Modify tStyle with new data
|
||||
for(auto& [modType, modifier] : nextTStyleMod.mods)
|
||||
modifier->modify_text_style(tStyle);
|
||||
|
||||
++nextTStyleModIt;
|
||||
}
|
||||
}
|
||||
|
||||
pData.p = a.Build();
|
||||
pData.p->layout(width);
|
||||
pData.heightOffset = heightOffset;
|
||||
heightOffset += pData.p->getHeight();
|
||||
}
|
||||
|
||||
//std::cout << "size: " << tStyleMods.size() << std::endl;
|
||||
//for(auto& s : tStyleMods)
|
||||
// std::cout << s.pos.fParagraphIndex << " " << s.pos.fTextByteIndex << std::endl;
|
||||
|
||||
needsRebuild = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::remove_duplicate_text_style_mods() {
|
||||
TextStyleModifier::ModifierMap previouslyAppliedMods = TextStyleModifier::get_default_modifiers();
|
||||
|
||||
std::erase_if(tStyleMods, [&previouslyAppliedMods](PositionedTextStyleMod& tStyleModsInPos) {
|
||||
std::erase_if(tStyleModsInPos.mods, [&previouslyAppliedMods](const std::pair<TextStyleModifier::ModifierType, std::shared_ptr<TextStyleModifier>>& tStyleModPair) {
|
||||
auto [placedIt, insertionTookPlace] = previouslyAppliedMods.emplace(tStyleModPair.first, tStyleModPair.second);
|
||||
if(!insertionTookPlace) {
|
||||
if(placedIt->second->equivalent(*tStyleModPair.second))
|
||||
return true;
|
||||
placedIt->second = tStyleModPair.second;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return tStyleModsInPos.mods.empty();
|
||||
});
|
||||
}
|
||||
|
||||
void TextBox::set_tab_space_width(unsigned newTabWidth) {
|
||||
tabWidth = newTabWidth;
|
||||
}
|
||||
|
||||
TextPosition TextBox::insert(TextPosition pos, std::string_view textToInsert, const std::optional<TextStyleModifier::ModifierMap>& inputModMap) {
|
||||
TextPosition oldPos = pos = move(Movement::NOWHERE, pos);
|
||||
|
||||
for(char c : textToInsert) {
|
||||
if(c == '\n') {
|
||||
if(newlinesAllowed) {
|
||||
for(auto& [p, tStylesInPos] : tStyleMods) {
|
||||
if(p.fParagraphIndex > pos.fParagraphIndex)
|
||||
p.fParagraphIndex++;
|
||||
else if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex && !(p.fTextByteIndex == pos.fTextByteIndex && p.fTextByteIndex == 0)) {
|
||||
p.fTextByteIndex = p.fTextByteIndex - pos.fTextByteIndex;
|
||||
p.fParagraphIndex++;
|
||||
}
|
||||
}
|
||||
paragraphs.insert(paragraphs.begin() + pos.fParagraphIndex + 1, ParagraphData{});
|
||||
paragraphs[pos.fParagraphIndex + 1].text = paragraphs[pos.fParagraphIndex].text.substr(pos.fTextByteIndex, paragraphs[pos.fParagraphIndex].text.size() - pos.fTextByteIndex);
|
||||
paragraphs[pos.fParagraphIndex].text.erase(pos.fTextByteIndex, paragraphs[pos.fParagraphIndex].text.size() - pos.fTextByteIndex);
|
||||
pos.fParagraphIndex++;
|
||||
pos.fTextByteIndex = 0;
|
||||
}
|
||||
}
|
||||
else if(c == '\t') {
|
||||
for(auto& [p, tStylesInPos] : tStyleMods) {
|
||||
if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex && !(p.fTextByteIndex == pos.fTextByteIndex && p.fTextByteIndex == 0))
|
||||
p.fTextByteIndex += tabWidth;
|
||||
}
|
||||
paragraphs[pos.fParagraphIndex].text.insert(pos.fTextByteIndex, std::string(tabWidth, ' '));
|
||||
pos.fTextByteIndex += tabWidth;
|
||||
}
|
||||
else {
|
||||
for(auto& [p, tStylesInPos] : tStyleMods) {
|
||||
if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex && !(p.fTextByteIndex == pos.fTextByteIndex && p.fTextByteIndex == 0))
|
||||
p.fTextByteIndex++;
|
||||
}
|
||||
paragraphs[pos.fParagraphIndex].text.insert(paragraphs[pos.fParagraphIndex].text.begin() + pos.fTextByteIndex, c);
|
||||
pos.fTextByteIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if(!textToInsert.empty()) {
|
||||
remove_duplicate_text_style_mods();
|
||||
if(inputModMap.has_value()) {
|
||||
for(const auto& [modType, modifier] : inputModMap.value())
|
||||
set_text_style_modifier_between(oldPos, pos, modifier);
|
||||
}
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
TextPosition TextBox::remove(TextPosition p1, TextPosition p2) {
|
||||
p1 = move(Movement::NOWHERE, p1);
|
||||
p2 = move(Movement::NOWHERE, p2);
|
||||
|
||||
TextPosition start = std::min(p1, p2);
|
||||
TextPosition end = std::max(p1, p2);
|
||||
if(start == end || start.fParagraphIndex == paragraphs.size())
|
||||
return start;
|
||||
if(start.fParagraphIndex == end.fParagraphIndex) {
|
||||
assert(end.fTextByteIndex > start.fTextByteIndex);
|
||||
paragraphs[start.fParagraphIndex].text.erase(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
|
||||
}
|
||||
else {
|
||||
assert(end.fParagraphIndex < paragraphs.size());
|
||||
auto& paragraph = paragraphs[start.fParagraphIndex];
|
||||
paragraph.text.erase(start.fTextByteIndex, paragraph.text.size() - start.fTextByteIndex);
|
||||
paragraph.text.insert(start.fTextByteIndex, paragraphs[end.fParagraphIndex].text.substr(end.fTextByteIndex));
|
||||
paragraphs.erase(paragraphs.begin() + start.fParagraphIndex + 1, paragraphs.begin() + end.fParagraphIndex + 1);
|
||||
}
|
||||
|
||||
for(int32_t i = 0; i < static_cast<int32_t>(tStyleMods.size()); i++) {
|
||||
PositionedTextStyleMod& tStyleMod = tStyleMods[i];
|
||||
auto& p = tStyleMod.pos;
|
||||
if(p > start) {
|
||||
if(end > p)
|
||||
p = start;
|
||||
else if(start.fParagraphIndex == p.fParagraphIndex)
|
||||
p.fTextByteIndex -= (end.fTextByteIndex - start.fTextByteIndex);
|
||||
else if(end.fParagraphIndex == p.fParagraphIndex) {
|
||||
p.fParagraphIndex = start.fParagraphIndex;
|
||||
p.fTextByteIndex = start.fTextByteIndex + (p.fTextByteIndex - end.fTextByteIndex);
|
||||
}
|
||||
else
|
||||
p.fParagraphIndex -= (end.fParagraphIndex - start.fParagraphIndex);
|
||||
}
|
||||
if(i > 0 && tStyleMods[i - 1].pos == p) {
|
||||
auto& tStyleModsToMergeWith = tStyleMods[i - 1].mods;
|
||||
for(auto& [modType, tStyle] : tStyleMod.mods)
|
||||
tStyleModsToMergeWith[modType] = tStyle;
|
||||
tStyleMods.erase(tStyleMods.begin() + i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
remove_duplicate_text_style_mods();
|
||||
inputChangedTextBox = true;
|
||||
needsRebuild = true;
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
SkRect TextBox::get_cursor_rect(TextPosition pos) {
|
||||
pos = move(Movement::NOWHERE, pos);
|
||||
|
||||
SkPoint topPoint;
|
||||
float height;
|
||||
|
||||
const ParagraphData& pData = paragraphs[pos.fParagraphIndex];
|
||||
if(pData.text.empty()) {
|
||||
bool isAlignLeft = (pData.pStyle.getTextAlign() == skia::textlayout::TextAlign::kLeft) || (pData.pStyle.getTextAlign() == skia::textlayout::TextAlign::kJustify && pData.pStyle.getTextDirection() == skia::textlayout::TextDirection::kLtr);
|
||||
topPoint = {isAlignLeft ? 0.0f : pData.p->getMaxWidth(), pData.heightOffset};
|
||||
height = pData.p->getHeight();
|
||||
}
|
||||
else {
|
||||
if(pos.fTextByteIndex == 0) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool exists = pData.p->getGlyphClusterAt(pos.fTextByteIndex, &glyphInfo);
|
||||
if(exists) {
|
||||
bool isLtr = glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr;
|
||||
topPoint = (isLtr ? glyphInfo.fBounds.TL() : glyphInfo.fBounds.TR()) + SkPoint{0.0f, pData.heightOffset};
|
||||
height = glyphInfo.fBounds.height();
|
||||
}
|
||||
else {
|
||||
topPoint = {0.0f, pData.heightOffset};
|
||||
height = 0.0f;
|
||||
}
|
||||
}
|
||||
else {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool exists = pData.p->getGlyphClusterAt(prev_grapheme(pData.text, pos.fTextByteIndex), &glyphInfo);
|
||||
if(exists) {
|
||||
bool isLtr = glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr;
|
||||
topPoint = (isLtr ? glyphInfo.fBounds.TR() : glyphInfo.fBounds.TL()) + SkPoint{0.0f, pData.heightOffset};
|
||||
height = glyphInfo.fBounds.height();
|
||||
}
|
||||
else {
|
||||
topPoint = {0.0f, pData.heightOffset};
|
||||
height = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float CURSOR_WIDTH = 3.0f;
|
||||
|
||||
return SkRect::MakeXYWH(topPoint.x() - CURSOR_WIDTH * 0.5f, topPoint.y(), CURSOR_WIDTH, height);
|
||||
}
|
||||
|
||||
void TextBox::rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function<void(const SkRect& r)> f) {
|
||||
p1 = move(Movement::NOWHERE, p1);
|
||||
p2 = move(Movement::NOWHERE, p2);
|
||||
|
||||
constexpr float SELECTION_RECT_EXTRA_AREA = 1.0f;
|
||||
|
||||
TextPosition start = std::min(p1, p2);
|
||||
TextPosition end = std::max(p1, p2);
|
||||
|
||||
for(size_t p = start.fParagraphIndex; p <= end.fParagraphIndex; p++) {
|
||||
size_t tStart = p == start.fParagraphIndex ? start.fTextByteIndex : 0;
|
||||
size_t tEnd = p == end.fParagraphIndex ? end.fTextByteIndex : paragraphs[p].text.size();
|
||||
ParagraphData& pData = paragraphs[p];
|
||||
for(size_t t = tStart; t < tEnd; t++) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
if(pData.p->getGlyphClusterAt(t, &glyphInfo)) {
|
||||
int lineNumber = pData.p->getLineNumberAt(t);
|
||||
if(lineNumber == -1)
|
||||
break;
|
||||
skia::textlayout::LineMetrics lineMetrics;
|
||||
if(pData.p->getLineMetricsAt(lineNumber, &lineMetrics)) {
|
||||
SkRect r = SkRect::MakeLTRB(glyphInfo.fBounds.left() - SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline - lineMetrics.fAscent - SELECTION_RECT_EXTRA_AREA + pData.heightOffset, glyphInfo.fBounds.right() + SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline + lineMetrics.fDescent + pData.heightOffset + SELECTION_RECT_EXTRA_AREA);
|
||||
f(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) {
|
||||
rebuild();
|
||||
|
||||
if(paintOpts.cursor.has_value()) {
|
||||
auto& cur = paintOpts.cursor.value();
|
||||
SkPaint p{SkColor4f{paintOpts.cursorColor.x(), paintOpts.cursorColor.y(), paintOpts.cursorColor.z(), 1.0f}};
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos) {
|
||||
canvas->saveLayerAlphaf(nullptr, 0.6f);
|
||||
rects_between_text_positions_func(cur.selectionBeginPos, cur.selectionEndPos, [&](const SkRect& rect) {
|
||||
canvas->drawRect(rect, p);
|
||||
});
|
||||
canvas->restore();
|
||||
}
|
||||
canvas->drawRect(get_cursor_rect(cur.pos), p);
|
||||
}
|
||||
|
||||
for(auto& pData : paragraphs)
|
||||
pData.p->paint(canvas, 0.0f, pData.heightOffset);
|
||||
}
|
||||
|
||||
}
|
||||
163
src/RichText/TextBox.hpp
Normal file
163
src/RichText/TextBox.hpp
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cwchar>
|
||||
#include <include/core/SkCanvas.h>
|
||||
#include <modules/skparagraph/include/FontCollection.h>
|
||||
#include <modules/skparagraph/include/Paragraph.h>
|
||||
#include <modules/skparagraph/include/TextStyle.h>
|
||||
#include "../SharedTypes.hpp"
|
||||
#include "TextStyleModifier.hpp"
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
|
||||
namespace RichText {
|
||||
|
||||
struct TextPosition {
|
||||
// Set to uint32_t values because theyll be saved and sent over networks. Size has to be consistent
|
||||
size_t fParagraphIndex = std::numeric_limits<uint32_t>::max();
|
||||
size_t fTextByteIndex = std::numeric_limits<uint32_t>::max();
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a((uint32_t)fParagraphIndex, (uint32_t)fTextByteIndex);
|
||||
}
|
||||
bool operator==(const TextPosition& o) const = default;
|
||||
bool operator<(const TextPosition& o) const;
|
||||
bool operator>(const TextPosition& o) const;
|
||||
bool operator>=(const TextPosition& o) const;
|
||||
bool operator<=(const TextPosition& o) const;
|
||||
};
|
||||
|
||||
struct PositionedTextStyleMod {
|
||||
TextPosition pos;
|
||||
TextStyleModifier::ModifierMap mods;
|
||||
};
|
||||
|
||||
typedef std::vector<PositionedTextStyleMod> TextStyleModContainer;
|
||||
|
||||
struct TextData {
|
||||
struct Paragraph {
|
||||
std::string text;
|
||||
template<typename Archive> void serialize(Archive& a) {
|
||||
a(text);
|
||||
}
|
||||
};
|
||||
void save(cereal::PortableBinaryOutputArchive& a) const;
|
||||
void load(cereal::PortableBinaryInputArchive& a);
|
||||
TextStyleModContainer tStyleMods;
|
||||
std::vector<Paragraph> paragraphs;
|
||||
};
|
||||
|
||||
class TextBox {
|
||||
public:
|
||||
TextBox();
|
||||
|
||||
bool inputChangedTextBox = false;
|
||||
|
||||
enum class Movement {
|
||||
NOWHERE,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT_WORD,
|
||||
RIGHT_WORD,
|
||||
HOME,
|
||||
END
|
||||
};
|
||||
|
||||
struct Cursor {
|
||||
TextPosition pos = {0, 0};
|
||||
TextPosition selectionBeginPos = {0, 0};
|
||||
TextPosition selectionEndPos = {0, 0};
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(pos, selectionBeginPos, selectionEndPos);
|
||||
}
|
||||
std::optional<float> previousX;
|
||||
bool operator==(const Cursor& o) const = default;
|
||||
bool operator!=(const Cursor& o) const = default;
|
||||
};
|
||||
|
||||
struct PaintOpts {
|
||||
Vector3f cursorColor = {1, 0, 0};
|
||||
std::optional<Cursor> cursor;
|
||||
};
|
||||
|
||||
TextPosition move(Movement movement, TextPosition pos, std::optional<float>* previousX = nullptr, bool flipDependingOnTextDirection = false);
|
||||
void set_width(float newWidth);
|
||||
TextPosition insert(TextPosition pos, std::string_view textToInsert, const std::optional<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
|
||||
TextPosition remove(TextPosition p1, TextPosition p2);
|
||||
void set_font_collection(const sk_sp<skia::textlayout::FontCollection>& fC);
|
||||
void paint(SkCanvas* canvas, const PaintOpts& paintOpts);
|
||||
void set_allow_newlines(bool allow);
|
||||
|
||||
enum class InputKey {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
UP,
|
||||
DOWN,
|
||||
HOME,
|
||||
END,
|
||||
BACKSPACE,
|
||||
DELETE,
|
||||
ENTER,
|
||||
TAB,
|
||||
SELECT_ALL
|
||||
};
|
||||
|
||||
void set_initial_text_style(const skia::textlayout::TextStyle& tStyle);
|
||||
void set_initial_text_style_modifier(const std::shared_ptr<TextStyleModifier>& modifier);
|
||||
void set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr<TextStyleModifier>& modifier);
|
||||
TextStyleModifier::ModifierMap get_mods_used_at_pos(TextPosition p);
|
||||
|
||||
TextData get_rich_text_data();
|
||||
void set_rich_text_data(const TextData& richText);
|
||||
void clear_text();
|
||||
void set_string(const std::string& str);
|
||||
|
||||
void process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift);
|
||||
void process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift, const std::optional<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
|
||||
std::string process_copy(Cursor& cur);
|
||||
std::string process_cut(Cursor& cur);
|
||||
std::string get_text_between(TextPosition p1, TextPosition p2);
|
||||
std::string get_string();
|
||||
void set_tab_space_width(unsigned newTabWidth);
|
||||
void process_text_input(Cursor& cur, const std::string& in, const std::optional<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
|
||||
|
||||
private:
|
||||
static size_t count_grapheme(const std::string& text);
|
||||
static size_t next_grapheme(const std::string& text, size_t textBytePos);
|
||||
static size_t prev_grapheme(const std::string& text, size_t textBytePos);
|
||||
static TextPosition get_text_pos_from_byte_pos(const std::string& text, size_t textBytePos);
|
||||
|
||||
size_t get_byte_pos_from_text_pos(TextPosition textPos);
|
||||
|
||||
void rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function<void(const SkRect& r)> f);
|
||||
|
||||
SkRect get_cursor_rect(TextPosition pos);
|
||||
|
||||
TextPosition get_text_pos_closest_to_point(Vector2f point);
|
||||
std::shared_ptr<TextStyleModifier> get_last_text_style_mod_before_pos(TextPosition pos, TextStyleModifier::ModifierType modType);
|
||||
void erase_if_over_all_styles_until_pos(TextPosition pos, const std::function<bool(TextPosition, const std::shared_ptr<TextStyleModifier>&)>& func);
|
||||
void insert_style_at_pos(TextPosition pos, const std::shared_ptr<TextStyleModifier>& modifier);
|
||||
|
||||
void remove_duplicate_text_style_mods();
|
||||
|
||||
void rebuild();
|
||||
|
||||
bool newlinesAllowed = true;
|
||||
float width = 0.0f;
|
||||
bool needsRebuild = true;
|
||||
unsigned tabWidth = 8;
|
||||
sk_sp<skia::textlayout::FontCollection> fontCollection;
|
||||
|
||||
struct ParagraphData {
|
||||
std::string text;
|
||||
std::unique_ptr<skia::textlayout::Paragraph> p;
|
||||
float heightOffset;
|
||||
skia::textlayout::ParagraphStyle pStyle;
|
||||
};
|
||||
|
||||
skia::textlayout::TextStyle initialTStyle;
|
||||
TextStyleModContainer tStyleMods;
|
||||
std::vector<ParagraphData> paragraphs;
|
||||
};
|
||||
|
||||
}
|
||||
229
src/RichText/TextStyleModifier.cpp
Normal file
229
src/RichText/TextStyleModifier.cpp
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#include "TextStyleModifier.hpp"
|
||||
#include <Helpers/ConvertVec.hpp>
|
||||
#include <Helpers/Serializers.hpp>
|
||||
#include <include/core/SkFontStyle.h>
|
||||
#include <modules/skparagraph/include/TextStyle.h>
|
||||
|
||||
#define WEIGHT_VALUE_MODIFIER 100
|
||||
|
||||
namespace RichText {
|
||||
|
||||
TextStyleModifier::ModifierMap TextStyleModifier::defaultMods;
|
||||
|
||||
std::shared_ptr<TextStyleModifier> TextStyleModifier::allocate_modifier(ModifierType type) {
|
||||
switch(type) {
|
||||
case ModifierType::SIZE:
|
||||
return std::make_shared<SizeTextStyleModifier>();
|
||||
case ModifierType::COLOR:
|
||||
return std::make_shared<ColorTextStyleModifier>();
|
||||
case ModifierType::WEIGHT:
|
||||
return std::make_shared<WeightTextStyleModifier>();
|
||||
case ModifierType::SLANT:
|
||||
return std::make_shared<SlantTextStyleModifier>();
|
||||
case ModifierType::DECORATION:
|
||||
return std::make_shared<DecorationTextStyleModifier>();
|
||||
case ModifierType::FONT_FAMILIES:
|
||||
return std::make_shared<FontFamiliesTextStyleModifier>();
|
||||
case ModifierType::COUNT:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TextStyleModifier> TextStyleModifier::get_default_modifier(ModifierType type) {
|
||||
auto& defaultMods = get_default_modifiers();
|
||||
auto it = defaultMods.find(type);
|
||||
if(it == defaultMods.end())
|
||||
return nullptr;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
const TextStyleModifier::ModifierMap& TextStyleModifier::get_default_modifiers() {
|
||||
if(defaultMods.empty()) {
|
||||
defaultMods[ModifierType::WEIGHT] = std::make_shared<WeightTextStyleModifier>(SkFontStyle::kNormal_Weight);
|
||||
defaultMods[ModifierType::SLANT] = std::make_shared<SlantTextStyleModifier>(SkFontStyle::kUpright_Slant);
|
||||
defaultMods[ModifierType::DECORATION] = std::make_shared<DecorationTextStyleModifier>(skia::textlayout::TextDecoration::kNoDecoration);
|
||||
}
|
||||
return defaultMods;
|
||||
}
|
||||
|
||||
bool TextStyleModifier::equivalent(TextStyleModifier& modifier) const {
|
||||
if(modifier.get_type() != get_type())
|
||||
return false;
|
||||
return equivalent_data(modifier);
|
||||
}
|
||||
|
||||
TextStyleModifier::~TextStyleModifier() {}
|
||||
|
||||
|
||||
|
||||
WeightTextStyleModifier::WeightTextStyleModifier(SkFontStyle::Weight initWeight): weightValue(initWeight / WEIGHT_VALUE_MODIFIER) {}
|
||||
|
||||
TextStyleModifier::ModifierType WeightTextStyleModifier::get_type() const {
|
||||
return ModifierType::WEIGHT;
|
||||
}
|
||||
|
||||
SkFontStyle::Weight WeightTextStyleModifier::get_weight() const {
|
||||
return static_cast<SkFontStyle::Weight>(static_cast<int>(weightValue) * WEIGHT_VALUE_MODIFIER);
|
||||
}
|
||||
|
||||
void WeightTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(weightValue);
|
||||
}
|
||||
|
||||
void WeightTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(weightValue);
|
||||
}
|
||||
|
||||
void WeightTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
style.setFontStyle(SkFontStyle(static_cast<int>(weightValue) * WEIGHT_VALUE_MODIFIER, style.getFontStyle().width(), style.getFontStyle().slant()));
|
||||
}
|
||||
|
||||
bool WeightTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return weightValue == static_cast<WeightTextStyleModifier&>(modifier).weightValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
SlantTextStyleModifier::SlantTextStyleModifier(uint8_t initSlantValue): slantValue(initSlantValue) {}
|
||||
|
||||
TextStyleModifier::ModifierType SlantTextStyleModifier::get_type() const {
|
||||
return ModifierType::SLANT;
|
||||
}
|
||||
|
||||
SkFontStyle::Slant SlantTextStyleModifier::get_slant() const {
|
||||
return static_cast<SkFontStyle::Slant>(slantValue);
|
||||
}
|
||||
|
||||
void SlantTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(slantValue);
|
||||
}
|
||||
|
||||
void SlantTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(slantValue);
|
||||
}
|
||||
|
||||
void SlantTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
style.setFontStyle(SkFontStyle(style.getFontStyle().weight(), style.getFontStyle().width(), static_cast<SkFontStyle::Slant>(slantValue)));
|
||||
}
|
||||
|
||||
bool SlantTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return slantValue == static_cast<SlantTextStyleModifier&>(modifier).slantValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ColorTextStyleModifier::ColorTextStyleModifier(const Vector4f& initColor): color(initColor) {}
|
||||
|
||||
TextStyleModifier::ModifierType ColorTextStyleModifier::get_type() const {
|
||||
return ModifierType::COLOR;
|
||||
}
|
||||
|
||||
const Vector4f& ColorTextStyleModifier::get_color() const {
|
||||
return color;
|
||||
}
|
||||
|
||||
void ColorTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(color);
|
||||
}
|
||||
|
||||
void ColorTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(color);
|
||||
}
|
||||
|
||||
void ColorTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
style.setForegroundPaint(SkPaint{convert_vec4<SkColor4f>(color)});
|
||||
style.setDecorationColor(convert_vec4<SkColor4f>(color).toSkColor());
|
||||
}
|
||||
|
||||
bool ColorTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return color == static_cast<ColorTextStyleModifier&>(modifier).color;
|
||||
}
|
||||
|
||||
|
||||
|
||||
SizeTextStyleModifier::SizeTextStyleModifier(float initSize): size(initSize) {}
|
||||
|
||||
TextStyleModifier::ModifierType SizeTextStyleModifier::get_type() const {
|
||||
return ModifierType::SIZE;
|
||||
}
|
||||
|
||||
float SizeTextStyleModifier::get_size() const {
|
||||
return size;
|
||||
}
|
||||
|
||||
void SizeTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(size);
|
||||
}
|
||||
|
||||
void SizeTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(size);
|
||||
}
|
||||
|
||||
void SizeTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
style.setFontSize(size);
|
||||
}
|
||||
|
||||
bool SizeTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return size == static_cast<SizeTextStyleModifier&>(modifier).size;
|
||||
}
|
||||
|
||||
|
||||
|
||||
FontFamiliesTextStyleModifier::FontFamiliesTextStyleModifier(const std::vector<SkString>& initFamilies): families(initFamilies) {}
|
||||
|
||||
TextStyleModifier::ModifierType FontFamiliesTextStyleModifier::get_type() const {
|
||||
return ModifierType::FONT_FAMILIES;
|
||||
}
|
||||
|
||||
const std::vector<SkString>& FontFamiliesTextStyleModifier::get_families() const {
|
||||
return families;
|
||||
}
|
||||
|
||||
void FontFamiliesTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(families);
|
||||
}
|
||||
|
||||
void FontFamiliesTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(families);
|
||||
}
|
||||
|
||||
void FontFamiliesTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
auto familiesToPush = families;
|
||||
familiesToPush.emplace_back("Roboto");
|
||||
style.setFontFamilies(familiesToPush);
|
||||
}
|
||||
|
||||
bool FontFamiliesTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return families == static_cast<FontFamiliesTextStyleModifier&>(modifier).families;
|
||||
}
|
||||
|
||||
|
||||
|
||||
DecorationTextStyleModifier::DecorationTextStyleModifier(uint8_t initDecorationValue): decorationValue(initDecorationValue) {}
|
||||
|
||||
TextStyleModifier::ModifierType DecorationTextStyleModifier::get_type() const {
|
||||
return ModifierType::DECORATION;
|
||||
}
|
||||
|
||||
uint8_t DecorationTextStyleModifier::get_decoration_value() const {
|
||||
return decorationValue;
|
||||
}
|
||||
|
||||
void DecorationTextStyleModifier::save(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(decorationValue);
|
||||
}
|
||||
|
||||
void DecorationTextStyleModifier::load(cereal::PortableBinaryInputArchive& a) {
|
||||
a(decorationValue);
|
||||
}
|
||||
|
||||
void DecorationTextStyleModifier::modify_text_style(skia::textlayout::TextStyle& style) const {
|
||||
style.setDecoration(static_cast<skia::textlayout::TextDecoration>(decorationValue));
|
||||
}
|
||||
|
||||
bool DecorationTextStyleModifier::equivalent_data(TextStyleModifier& modifier) const {
|
||||
return decorationValue == static_cast<DecorationTextStyleModifier&>(modifier).decorationValue;
|
||||
}
|
||||
|
||||
}
|
||||
123
src/RichText/TextStyleModifier.hpp
Normal file
123
src/RichText/TextStyleModifier.hpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <modules/skparagraph/include/Paragraph.h>
|
||||
#include <modules/skparagraph/include/TextStyle.h>
|
||||
#include "cereal/archives/portable_binary.hpp"
|
||||
#include "../SharedTypes.hpp"
|
||||
|
||||
namespace RichText {
|
||||
|
||||
class TextStyleModifier {
|
||||
public:
|
||||
enum class ModifierType : uint16_t {
|
||||
SIZE = 0,
|
||||
COLOR,
|
||||
WEIGHT,
|
||||
SLANT,
|
||||
DECORATION,
|
||||
FONT_FAMILIES,
|
||||
|
||||
COUNT
|
||||
};
|
||||
|
||||
typedef std::unordered_map<ModifierType, std::shared_ptr<TextStyleModifier>> ModifierMap;
|
||||
|
||||
static std::shared_ptr<TextStyleModifier> allocate_modifier(ModifierType type);
|
||||
static std::shared_ptr<TextStyleModifier> get_default_modifier(ModifierType type);
|
||||
static const ModifierMap& get_default_modifiers();
|
||||
virtual ModifierType get_type() const = 0;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const = 0;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) = 0;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const = 0;
|
||||
bool equivalent(TextStyleModifier& modifier) const;
|
||||
virtual ~TextStyleModifier();
|
||||
private:
|
||||
static ModifierMap defaultMods;
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const = 0;
|
||||
};
|
||||
|
||||
class DecorationTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
DecorationTextStyleModifier() = default;
|
||||
DecorationTextStyleModifier(uint8_t initDecorationValue);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
uint8_t get_decoration_value() const;
|
||||
private:
|
||||
uint8_t decorationValue;
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
};
|
||||
|
||||
class WeightTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
WeightTextStyleModifier() = default;
|
||||
WeightTextStyleModifier(SkFontStyle::Weight initWeight);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
SkFontStyle::Weight get_weight() const;
|
||||
private:
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
uint8_t weightValue;
|
||||
};
|
||||
|
||||
class SlantTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
SlantTextStyleModifier() = default;
|
||||
SlantTextStyleModifier(uint8_t initSlantValue);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
SkFontStyle::Slant get_slant() const;
|
||||
private:
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
uint8_t slantValue;
|
||||
};
|
||||
|
||||
class ColorTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
ColorTextStyleModifier() = default;
|
||||
ColorTextStyleModifier(const Vector4f& initColor);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
const Vector4f& get_color() const;
|
||||
private:
|
||||
Vector4f color;
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
};
|
||||
|
||||
class SizeTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
SizeTextStyleModifier() = default;
|
||||
SizeTextStyleModifier(float initSize);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
float get_size() const;
|
||||
private:
|
||||
float size;
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
};
|
||||
|
||||
class FontFamiliesTextStyleModifier : public TextStyleModifier {
|
||||
public:
|
||||
FontFamiliesTextStyleModifier() = default;
|
||||
FontFamiliesTextStyleModifier(const std::vector<SkString>& initFamilies);
|
||||
virtual ModifierType get_type() const override;
|
||||
virtual void save(cereal::PortableBinaryOutputArchive& a) const override;
|
||||
virtual void load(cereal::PortableBinaryInputArchive& a) override;
|
||||
virtual void modify_text_style(skia::textlayout::TextStyle& style) const override;
|
||||
const std::vector<SkString>& get_families() const;
|
||||
private:
|
||||
std::vector<SkString> families;
|
||||
virtual bool equivalent_data(TextStyleModifier& modifier) const override;
|
||||
};
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue