2025-10-16 21:22:08 +03:00
|
|
|
#include "RichTextBox.hpp"
|
2025-10-18 13:32:12 +03:00
|
|
|
#include <limits>
|
2025-10-20 11:41:22 +03:00
|
|
|
#include <modules/skparagraph/include/DartTypes.h>
|
2025-10-18 13:32:12 +03:00
|
|
|
#include <modules/skparagraph/include/Metrics.h>
|
|
|
|
|
#include <modules/skparagraph/src/TextLine.h>
|
|
|
|
|
#include <modules/skunicode/include/SkUnicode.h>
|
2025-10-16 21:22:08 +03:00
|
|
|
#include <modules/skunicode/include/SkUnicode_icu.h>
|
|
|
|
|
#include <modules/skparagraph/src/ParagraphBuilderImpl.h>
|
|
|
|
|
#include <modules/skparagraph/src/ParagraphImpl.h>
|
|
|
|
|
#include <Helpers/MathExtras.hpp>
|
2025-10-18 13:32:12 +03:00
|
|
|
#include <src/base/SkUTF.h>
|
|
|
|
|
|
|
|
|
|
bool RichTextBox::TextPosition::operator<(const RichTextBox::TextPosition& o) const {
|
|
|
|
|
return (this->fParagraphIndex < o.fParagraphIndex) || (this->fParagraphIndex == o.fParagraphIndex && this->fTextByteIndex < o.fTextByteIndex);
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
|
|
|
|
|
RichTextBox::RichTextBox() {
|
2025-10-19 15:54:00 +03:00
|
|
|
paragraphs.emplace_back();
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift) {
|
|
|
|
|
bool moved = false;
|
|
|
|
|
|
|
|
|
|
switch(in) {
|
|
|
|
|
case InputKey::LEFT:
|
2025-10-20 11:41:22 +03:00
|
|
|
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::LEFT_WORD : Movement::LEFT, cur.pos, nullptr, true);
|
2025-10-16 21:22:08 +03:00
|
|
|
moved = true;
|
|
|
|
|
break;
|
|
|
|
|
case InputKey::RIGHT:
|
2025-10-20 11:41:22 +03:00
|
|
|
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::RIGHT_WORD : Movement::RIGHT, cur.pos, nullptr, true);
|
2025-10-16 21:22:08 +03:00
|
|
|
moved = true;
|
|
|
|
|
break;
|
|
|
|
|
case InputKey::UP:
|
2025-10-18 13:32:12 +03:00
|
|
|
cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos, &cur.previousX);
|
2025-10-16 21:22:08 +03:00
|
|
|
moved = true;
|
|
|
|
|
break;
|
|
|
|
|
case InputKey::DOWN:
|
2025-10-18 13:32:12 +03:00
|
|
|
cur.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos, &cur.previousX);
|
2025-10-16 21:22:08 +03:00
|
|
|
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(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(Movement::RIGHT, cur.pos));
|
|
|
|
|
break;
|
|
|
|
|
case InputKey::ENTER:
|
2025-10-20 17:30:36 +03:00
|
|
|
process_text_input(cur, "\n");
|
|
|
|
|
break;
|
|
|
|
|
case InputKey::TAB:
|
|
|
|
|
process_text_input(cur, "\t");
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
case InputKey::SELECT_ALL:
|
2025-10-18 13:32:12 +03:00
|
|
|
cur.selectionBeginPos = {0, 0};
|
2025-10-19 15:54:00 +03:00
|
|
|
cur.pos = cur.selectionEndPos = {paragraphs.back().text.size(), paragraphs.size() - 1};
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(moved && !shift)
|
|
|
|
|
cur.selectionEndPos = cur.selectionBeginPos;
|
2025-10-18 13:32:12 +03:00
|
|
|
|
|
|
|
|
if(in != InputKey::UP && in != InputKey::DOWN)
|
|
|
|
|
cur.previousX = std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift) {
|
|
|
|
|
if(clicked || held) {
|
|
|
|
|
rebuild();
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
cur.pos = get_text_pos_closest_to_point(pos);
|
|
|
|
|
cur.selectionBeginPos = cur.pos;
|
|
|
|
|
if(clicked && !shift)
|
|
|
|
|
cur.selectionEndPos = cur.selectionBeginPos;
|
|
|
|
|
cur.previousX = std::nullopt;
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string RichTextBox::process_copy(Cursor& cur) {
|
2025-10-19 15:54:00 +03:00
|
|
|
return get_text_between(cur.selectionBeginPos, cur.selectionEndPos);
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string RichTextBox::process_cut(Cursor& cur) {
|
|
|
|
|
std::string toRet = process_copy(cur);
|
2025-10-18 13:32:12 +03:00
|
|
|
if(cur.selectionBeginPos != cur.selectionEndPos) {
|
2025-10-16 21:22:08 +03:00
|
|
|
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
2025-10-18 13:32:12 +03:00
|
|
|
cur.previousX = std::nullopt;
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
return toRet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::process_text_input(Cursor& cur, const std::string& in) {
|
|
|
|
|
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);
|
2025-10-18 13:32:12 +03:00
|
|
|
cur.previousX = std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t RichTextBox::count_grapheme(const std::string& text) {
|
2025-10-18 13:32:12 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t RichTextBox::next_grapheme(const std::string& text, size_t textBytePos) {
|
2025-10-18 13:32:12 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t RichTextBox::prev_grapheme(const std::string& text, size_t textBytePos) {
|
2025-10-18 13:32:12 +03:00
|
|
|
if(textBytePos == 0)
|
|
|
|
|
return 0;
|
2025-10-19 15:54:00 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RichTextBox::TextPosition RichTextBox::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;
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t RichTextBox::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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RichTextBox::TextPosition RichTextBox::get_text_pos_closest_to_point(Vector2f point) {
|
|
|
|
|
rebuild();
|
|
|
|
|
|
2025-10-18 13:32:12 +03:00
|
|
|
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
2025-10-19 15:54:00 +03:00
|
|
|
|
|
|
|
|
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);
|
2025-10-18 13:32:12 +03:00
|
|
|
if(foundGlyph) {
|
2025-10-19 15:54:00 +03:00
|
|
|
TextPosition toRet = {0, pIndex};
|
2025-10-18 13:32:12 +03:00
|
|
|
toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start;
|
2025-10-20 11:41:22 +03:00
|
|
|
|
|
|
|
|
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;
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
2025-10-19 15:54:00 +03:00
|
|
|
else
|
|
|
|
|
return TextPosition{0, pIndex};
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 17:31:55 +03:00
|
|
|
RichTextBox::RichTextData RichTextBox::get_rich_text_data() {
|
|
|
|
|
return {get_string()};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::clear_text() {
|
|
|
|
|
paragraphs.clear();
|
|
|
|
|
paragraphs.emplace_back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::set_string(const std::string& str) {
|
|
|
|
|
clear_text();
|
|
|
|
|
insert({0, 0}, str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::set_rich_text_data(const RichTextData& richText) {
|
|
|
|
|
set_string(richText.text);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 17:30:36 +03:00
|
|
|
std::string RichTextBox::get_string() {
|
|
|
|
|
return get_text_between({0, 0}, move(RichTextBox::Movement::END, {0, 0}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
std::string RichTextBox::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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 11:41:22 +03:00
|
|
|
RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, std::optional<float>* previousX, bool flipDependingOnTextDirection) {
|
2025-10-19 15:54:00 +03:00
|
|
|
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();
|
2025-10-16 21:22:08 +03:00
|
|
|
|
2025-10-25 17:31:55 +03:00
|
|
|
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;
|
|
|
|
|
}
|
2025-10-20 11:41:22 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-16 21:22:08 +03:00
|
|
|
switch (movement) {
|
2025-10-19 15:54:00 +03:00
|
|
|
case Movement::NOWHERE:
|
|
|
|
|
break;
|
2025-10-18 13:32:12 +03:00
|
|
|
case Movement::LEFT: {
|
2025-10-19 15:54:00 +03:00
|
|
|
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);
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
|
|
|
|
case Movement::RIGHT: {
|
2025-10-19 15:54:00 +03:00
|
|
|
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);
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
case Movement::UP: {
|
2025-10-18 13:32:12 +03:00
|
|
|
assert(previousX != nullptr);
|
2025-10-19 15:54:00 +03:00
|
|
|
|
|
|
|
|
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) {
|
2025-10-18 13:32:12 +03:00
|
|
|
pos.fTextByteIndex = 0;
|
|
|
|
|
if(previousX != nullptr)
|
|
|
|
|
*previousX = get_cursor_rect(pos).centerX();
|
|
|
|
|
}
|
|
|
|
|
else {
|
2025-10-19 15:54:00 +03:00
|
|
|
if(lineNumber <= 0 && paragraphs[pos.fParagraphIndex - 1].text.empty()) {
|
|
|
|
|
if(previousX != nullptr && !previousX->has_value())
|
|
|
|
|
*previousX = get_cursor_rect(pos).centerX();
|
|
|
|
|
pos = TextPosition{0, pos.fParagraphIndex - 1};
|
|
|
|
|
}
|
|
|
|
|
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();
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Movement::DOWN: {
|
2025-10-18 13:32:12 +03:00
|
|
|
assert(previousX != nullptr);
|
2025-10-19 15:54:00 +03:00
|
|
|
|
|
|
|
|
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();
|
2025-10-18 13:32:12 +03:00
|
|
|
if(previousX != nullptr)
|
|
|
|
|
*previousX = get_cursor_rect(pos).centerX();
|
|
|
|
|
}
|
|
|
|
|
else {
|
2025-10-19 15:54:00 +03:00
|
|
|
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{0, pos.fParagraphIndex + 1};
|
|
|
|
|
}
|
|
|
|
|
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();
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Movement::LEFT_WORD: {
|
2025-10-19 15:54:00 +03:00
|
|
|
if(pos == TextPosition{0, 0})
|
2025-10-18 13:32:12 +03:00
|
|
|
break;
|
2025-10-19 15:54:00 +03:00
|
|
|
|
2025-10-20 17:30:36 +03:00
|
|
|
std::string fullText = get_string();
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t bytePosInFullText = get_byte_pos_from_text_pos(pos);
|
|
|
|
|
|
2025-10-18 13:32:12 +03:00
|
|
|
auto u = SkUnicodes::ICU::Make();
|
|
|
|
|
|
|
|
|
|
size_t byteIndexToRet = 0;
|
|
|
|
|
|
|
|
|
|
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
2025-10-19 15:54:00 +03:00
|
|
|
breakIterator->setText(fullText.c_str(), fullText.size());
|
2025-10-18 13:32:12 +03:00
|
|
|
|
|
|
|
|
while(!breakIterator->isDone()) {
|
|
|
|
|
size_t nextByteIndex = breakIterator->next();
|
2025-10-19 15:54:00 +03:00
|
|
|
if(nextByteIndex >= bytePosInFullText)
|
|
|
|
|
return get_text_pos_from_byte_pos(fullText, byteIndexToRet);
|
2025-10-18 13:32:12 +03:00
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
const char* tPtr = fullText.c_str() + nextByteIndex;
|
|
|
|
|
SkUnichar u = SkUTF::NextUTF8(&tPtr, fullText.c_str() + fullText.size());
|
2025-10-18 13:32:12 +03:00
|
|
|
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
|
|
|
|
byteIndexToRet = nextByteIndex;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Movement::RIGHT_WORD: {
|
2025-10-19 15:54:00 +03:00
|
|
|
TextPosition endPos = move(Movement::END, {0, 0});
|
|
|
|
|
if(pos == endPos)
|
|
|
|
|
break;
|
|
|
|
|
|
2025-10-20 17:30:36 +03:00
|
|
|
std::string fullText = get_string();
|
2025-10-19 15:54:00 +03:00
|
|
|
size_t bytePosInFullText = get_byte_pos_from_text_pos(pos);
|
|
|
|
|
|
2025-10-18 13:32:12 +03:00
|
|
|
auto u = SkUnicodes::ICU::Make();
|
|
|
|
|
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
2025-10-19 15:54:00 +03:00
|
|
|
|
|
|
|
|
breakIterator->setText(fullText.c_str() + bytePosInFullText, fullText.size() - bytePosInFullText);
|
|
|
|
|
|
2025-10-18 13:32:12 +03:00
|
|
|
while(!breakIterator->isDone()) {
|
2025-10-19 15:54:00 +03:00
|
|
|
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());
|
2025-10-18 13:32:12 +03:00
|
|
|
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
2025-10-19 15:54:00 +03:00
|
|
|
return get_text_pos_from_byte_pos(fullText, p);
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
2025-10-19 15:54:00 +03:00
|
|
|
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Movement::HOME:
|
2025-10-18 13:32:12 +03:00
|
|
|
pos.fTextByteIndex = 0;
|
2025-10-19 15:54:00 +03:00
|
|
|
pos.fParagraphIndex = 0;
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
case Movement::END:
|
2025-10-19 15:54:00 +03:00
|
|
|
pos.fTextByteIndex = paragraphs.back().text.size();
|
|
|
|
|
pos.fParagraphIndex = paragraphs.size() - 1;
|
2025-10-16 21:22:08 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return pos;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::set_width(float newWidth) {
|
2025-10-18 13:32:12 +03:00
|
|
|
if(width != newWidth) {
|
|
|
|
|
width = newWidth;
|
|
|
|
|
needsRebuild = true;
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::set_allow_newlines(bool allow) {
|
2025-10-20 17:30:36 +03:00
|
|
|
newlinesAllowed = allow;
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::set_font_collection(const sk_sp<skia::textlayout::FontCollection>& fC) {
|
2025-10-25 17:31:55 +03:00
|
|
|
if(fontCollection != fC) {
|
|
|
|
|
fontCollection = fC;
|
|
|
|
|
needsRebuild = true;
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RichTextBox::rebuild() {
|
|
|
|
|
if(needsRebuild) {
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
float heightOffset = 0.0f;
|
|
|
|
|
for(ParagraphData& pData : paragraphs) {
|
2025-10-20 11:41:22 +03:00
|
|
|
pData.pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft);
|
|
|
|
|
pData.pStyle.setTextDirection(skia::textlayout::TextDirection::kLtr);
|
|
|
|
|
skia::textlayout::TextStyle tStyle;
|
2025-11-01 18:07:39 +03:00
|
|
|
tStyle.setFontSize(16.0f);
|
2025-10-20 11:41:22 +03:00
|
|
|
tStyle.setFontFamilies({SkString{"Roboto"}});
|
|
|
|
|
pData.pStyle.setTextStyle(tStyle);
|
|
|
|
|
|
|
|
|
|
skia::textlayout::ParagraphBuilderImpl a(pData.pStyle, fontCollection, SkUnicodes::ICU::Make());
|
2025-10-19 15:54:00 +03:00
|
|
|
a.addText(pData.text.c_str(), pData.text.length());
|
|
|
|
|
pData.p = a.Build();
|
|
|
|
|
pData.p->layout(width);
|
|
|
|
|
pData.heightOffset = heightOffset;
|
|
|
|
|
heightOffset += pData.p->getHeight();
|
|
|
|
|
}
|
2025-10-18 13:32:12 +03:00
|
|
|
needsRebuild = false;
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 17:30:36 +03:00
|
|
|
void RichTextBox::set_tab_space_width(unsigned newTabWidth) {
|
|
|
|
|
tabWidth = newTabWidth;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
RichTextBox::TextPosition RichTextBox::insert(TextPosition pos, std::string_view textToInsert) {
|
|
|
|
|
pos = move(Movement::NOWHERE, pos);
|
|
|
|
|
|
|
|
|
|
for(char c : textToInsert) {
|
|
|
|
|
if(c == '\n') {
|
2025-10-20 17:30:36 +03:00
|
|
|
if(newlinesAllowed) {
|
|
|
|
|
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') {
|
|
|
|
|
paragraphs[pos.fParagraphIndex].text.insert(pos.fTextByteIndex, std::string(tabWidth, ' '));
|
|
|
|
|
pos.fTextByteIndex += tabWidth;
|
2025-10-19 15:54:00 +03:00
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
paragraphs[pos.fParagraphIndex].text.insert(paragraphs[pos.fParagraphIndex].text.begin() + pos.fTextByteIndex, c);
|
|
|
|
|
pos.fTextByteIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 21:22:08 +03:00
|
|
|
needsRebuild = true;
|
2025-10-19 15:54:00 +03:00
|
|
|
return pos;
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
RichTextBox::TextPosition RichTextBox::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);
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
needsRebuild = true;
|
2025-10-19 15:54:00 +03:00
|
|
|
return start;
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-18 13:32:12 +03:00
|
|
|
SkRect RichTextBox::get_cursor_rect(TextPosition pos) {
|
2025-10-19 15:54:00 +03:00
|
|
|
pos = move(Movement::NOWHERE, pos);
|
|
|
|
|
|
2025-10-20 11:41:22 +03:00
|
|
|
SkPoint topPoint;
|
2025-10-16 21:22:08 +03:00
|
|
|
float height;
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
const ParagraphData& pData = paragraphs[pos.fParagraphIndex];
|
|
|
|
|
if(pData.text.empty()) {
|
2025-10-20 11:41:22 +03:00
|
|
|
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};
|
2025-10-19 15:54:00 +03:00
|
|
|
height = pData.p->getHeight();
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
2025-10-18 13:32:12 +03:00
|
|
|
else {
|
2025-10-19 15:54:00 +03:00
|
|
|
if(pos.fTextByteIndex == pData.text.size()) {
|
2025-10-18 13:32:12 +03:00
|
|
|
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
2025-10-19 15:54:00 +03:00
|
|
|
bool exists = pData.p->getGlyphClusterAt(prev_grapheme(pData.text, pos.fTextByteIndex), &glyphInfo);
|
2025-10-18 13:32:12 +03:00
|
|
|
if(exists) {
|
2025-10-20 11:41:22 +03:00
|
|
|
bool isLtr = glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr;
|
|
|
|
|
topPoint = (isLtr ? glyphInfo.fBounds.TR() : glyphInfo.fBounds.TL()) + SkPoint{0.0f, pData.heightOffset};
|
2025-10-18 13:32:12 +03:00
|
|
|
height = glyphInfo.fBounds.height();
|
|
|
|
|
}
|
|
|
|
|
else {
|
2025-10-20 11:41:22 +03:00
|
|
|
topPoint = {0.0f, pData.heightOffset};
|
2025-10-18 13:32:12 +03:00
|
|
|
height = 0.0f;
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
else {
|
2025-10-18 13:32:12 +03:00
|
|
|
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
2025-10-19 15:54:00 +03:00
|
|
|
bool exists = pData.p->getGlyphClusterAt(pos.fTextByteIndex, &glyphInfo);
|
2025-10-18 13:32:12 +03:00
|
|
|
if(exists) {
|
2025-10-20 11:41:22 +03:00
|
|
|
bool isLtr = glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr;
|
|
|
|
|
topPoint = (isLtr ? glyphInfo.fBounds.TL() : glyphInfo.fBounds.TR()) + SkPoint{0.0f, pData.heightOffset};
|
2025-10-18 13:32:12 +03:00
|
|
|
height = glyphInfo.fBounds.height();
|
|
|
|
|
}
|
|
|
|
|
else {
|
2025-10-20 11:41:22 +03:00
|
|
|
topPoint = {0.0f, pData.heightOffset};
|
2025-10-18 13:32:12 +03:00
|
|
|
height = 0.0f;
|
|
|
|
|
}
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-20 11:41:22 +03:00
|
|
|
|
|
|
|
|
constexpr float CURSOR_WIDTH = 3.0f;
|
|
|
|
|
|
|
|
|
|
return SkRect::MakeXYWH(topPoint.x() - CURSOR_WIDTH * 0.5f, topPoint.y(), CURSOR_WIDTH, height);
|
2025-10-18 13:32:12 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
void RichTextBox::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);
|
2025-10-18 13:32:12 +03:00
|
|
|
|
2025-10-20 11:41:22 +03:00
|
|
|
constexpr float SELECTION_RECT_EXTRA_AREA = 1.0f;
|
2025-10-18 13:32:12 +03:00
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
TextPosition start = std::min(p1, p2);
|
|
|
|
|
TextPosition end = std::max(p1, p2);
|
2025-10-18 13:32:12 +03:00
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
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++) {
|
2025-10-18 13:32:12 +03:00
|
|
|
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
2025-10-19 15:54:00 +03:00
|
|
|
if(pData.p->getGlyphClusterAt(t, &glyphInfo)) {
|
|
|
|
|
int lineNumber = pData.p->getLineNumberAt(t);
|
2025-10-18 13:32:12 +03:00
|
|
|
if(lineNumber == -1)
|
|
|
|
|
break;
|
|
|
|
|
skia::textlayout::LineMetrics lineMetrics;
|
2025-10-19 15:54:00 +03:00
|
|
|
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);
|
2025-10-18 13:32:12 +03:00
|
|
|
f(r);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 21:22:08 +03:00
|
|
|
void RichTextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) {
|
|
|
|
|
rebuild();
|
|
|
|
|
|
|
|
|
|
if(paintOpts.cursor.has_value()) {
|
|
|
|
|
auto& cur = paintOpts.cursor.value();
|
2025-10-18 13:32:12 +03:00
|
|
|
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();
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
2025-10-19 15:54:00 +03:00
|
|
|
canvas->drawRect(get_cursor_rect(cur.pos), p);
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 15:54:00 +03:00
|
|
|
for(auto& pData : paragraphs)
|
|
|
|
|
pData.p->paint(canvas, 0.0f, pData.heightOffset);
|
2025-10-16 21:22:08 +03:00
|
|
|
}
|