Textbox mostly works with text other than ASCII
This commit is contained in:
parent
e507473e3a
commit
295529586c
3 changed files with 383 additions and 83 deletions
|
|
@ -71,26 +71,16 @@ MainProgram::MainProgram():
|
|||
|
||||
textBox = std::make_shared<RichTextBox>();
|
||||
textBoxCursor = std::make_shared<RichTextBox::Cursor>();
|
||||
textBox->set_width(1000.0f);
|
||||
textBox->set_font_collection(fonts.collection);
|
||||
textBox->insert(0, R"(
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
)");
|
||||
|
||||
textBox->insert({0, 0}, R"(Hello world)");
|
||||
paintOpts.cursor = RichTextBox::Cursor{};
|
||||
paintOpts.cursorColor = {1.0f, 0.0f, 0.0f, 0.7f};
|
||||
paintOpts.cursorColor = {1.0f, 0.0f, 0.0f};
|
||||
}
|
||||
|
||||
void MainProgram::update() {
|
||||
input.text.set_rich_text_box_input(textBox, textBoxCursor);
|
||||
textBox->process_mouse_left_button(*textBoxCursor, input.mouse.pos - Vector2f{10.0f, 10.0f}, input.mouse.leftClicks, input.mouse.leftDown, input.key(InputManager::KEY_GENERIC_LSHIFT).held);
|
||||
textBox->set_width(window.size.x() - 20.0f);
|
||||
|
||||
//while(std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit);
|
||||
//lastFrameTime = std::chrono::steady_clock::now();
|
||||
|
|
@ -273,7 +263,7 @@ void MainProgram::draw(SkCanvas* canvas) {
|
|||
|
||||
canvas->clear(SkColor4f{0.0f, 0.0f, 0.0f, 1.0f});
|
||||
canvas->save();
|
||||
canvas->translate(200.0f, 200.0f);
|
||||
canvas->translate(10.0f, 10.0f);
|
||||
paintOpts.cursor = *textBoxCursor;
|
||||
textBox->paint(canvas, paintOpts);
|
||||
canvas->restore();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
#include "RichTextBox.hpp"
|
||||
#include <limits>
|
||||
#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>
|
||||
|
||||
bool RichTextBox::TextPosition::operator<(const RichTextBox::TextPosition& o) const {
|
||||
return (this->fParagraphIndex < o.fParagraphIndex) || (this->fParagraphIndex == o.fParagraphIndex && this->fTextByteIndex < o.fTextByteIndex);
|
||||
}
|
||||
|
||||
RichTextBox::RichTextBox() {
|
||||
}
|
||||
|
|
@ -12,19 +21,19 @@ void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool sh
|
|||
|
||||
switch(in) {
|
||||
case InputKey::LEFT:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::LEFT, cur.pos);
|
||||
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::LEFT_WORD : Movement::LEFT, cur.pos);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::RIGHT:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::RIGHT, cur.pos);
|
||||
cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::RIGHT_WORD : Movement::RIGHT, cur.pos);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::UP:
|
||||
cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos);
|
||||
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.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos, &cur.previousX);
|
||||
moved = true;
|
||||
break;
|
||||
case InputKey::HOME:
|
||||
|
|
@ -52,25 +61,45 @@ void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool sh
|
|||
process_text_input(cur, "\n");
|
||||
break;
|
||||
case InputKey::SELECT_ALL:
|
||||
cur.selectionBeginPos = 0;
|
||||
cur.pos = cur.selectionEndPos = text.size();
|
||||
cur.selectionBeginPos = {0, 0};
|
||||
cur.pos = cur.selectionEndPos = {text.size(), 0};
|
||||
break;
|
||||
}
|
||||
|
||||
if(moved && !shift)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
|
||||
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();
|
||||
|
||||
std::optional<TextPosition> newPos = get_text_pos_closest_to_point(pos);
|
||||
if(newPos.has_value()) {
|
||||
cur.pos = newPos.value();
|
||||
cur.selectionBeginPos = cur.pos;
|
||||
if(clicked && !shift)
|
||||
cur.selectionEndPos = cur.selectionBeginPos;
|
||||
cur.previousX = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string RichTextBox::process_copy(Cursor& cur) {
|
||||
uint64_t trueBegin = std::min(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
uint64_t trueEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
return text.substr(trueBegin, trueEnd - trueBegin);
|
||||
TextPosition trueBegin = std::min(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
TextPosition trueEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
return text.substr(trueBegin.fTextByteIndex, trueEnd.fTextByteIndex - trueBegin.fTextByteIndex);
|
||||
}
|
||||
|
||||
std::string RichTextBox::process_cut(Cursor& cur) {
|
||||
std::string toRet = process_copy(cur);
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos)
|
||||
if(cur.selectionBeginPos != cur.selectionEndPos) {
|
||||
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
cur.previousX = std::nullopt;
|
||||
}
|
||||
return toRet;
|
||||
}
|
||||
|
||||
|
|
@ -79,41 +108,175 @@ void RichTextBox::process_text_input(Cursor& cur, const std::string& in) {
|
|||
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);
|
||||
cur.previousX = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t RichTextBox::move(Movement movement, uint64_t pos) {
|
||||
size_t RichTextBox::count_grapheme() {
|
||||
rebuild();
|
||||
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 RichTextBox::next_grapheme(size_t textBytePos) {
|
||||
rebuild();
|
||||
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 RichTextBox::prev_grapheme(size_t textBytePos) {
|
||||
rebuild();
|
||||
if(textBytePos == 0)
|
||||
return 0;
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool glyphClusterFound = paragraph->getGlyphClusterAt(textBytePos - 1, &glyphInfo);
|
||||
if(glyphClusterFound)
|
||||
return glyphInfo.fClusterTextRange.start;
|
||||
return std::numeric_limits<size_t>::max();
|
||||
}
|
||||
|
||||
std::optional<RichTextBox::TextPosition> RichTextBox::get_text_pos_closest_to_point(const Vector2f& point) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool foundGlyph = paragraph->getClosestGlyphClusterAt(point.x(), point.y(), &glyphInfo);
|
||||
if(foundGlyph) {
|
||||
TextPosition toRet = {0, 0};
|
||||
toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start;
|
||||
if(std::abs(glyphInfo.fBounds.right() - point.x()) < std::abs(glyphInfo.fBounds.left() - point.x()))
|
||||
toRet.fTextByteIndex = next_grapheme(toRet.fTextByteIndex);
|
||||
return toRet;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, std::optional<float>* previousX) {
|
||||
rebuild();
|
||||
|
||||
if(pos >= text.size())
|
||||
pos = text.size();
|
||||
if(pos.fTextByteIndex >= text.size())
|
||||
pos.fTextByteIndex = text.size();
|
||||
|
||||
switch (movement) {
|
||||
case Movement::LEFT:
|
||||
if(pos != 0)
|
||||
pos--;
|
||||
case Movement::LEFT: {
|
||||
pos.fTextByteIndex = prev_grapheme(pos.fTextByteIndex);
|
||||
break;
|
||||
case Movement::RIGHT:
|
||||
if(pos < text.size())
|
||||
pos++;
|
||||
}
|
||||
case Movement::RIGHT: {
|
||||
pos.fTextByteIndex = next_grapheme(pos.fTextByteIndex);
|
||||
break;
|
||||
}
|
||||
case Movement::UP: {
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
Vector2f pointToCheck{cursorRect.x(), cursorRect.top() - 0.1f};
|
||||
pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position;
|
||||
assert(previousX != nullptr);
|
||||
if(text.size() == 0)
|
||||
break;
|
||||
if(pos.fTextByteIndex == 0)
|
||||
break;
|
||||
int lineNumber = paragraph->getLineNumberAt(prev_grapheme(pos.fTextByteIndex));
|
||||
if(lineNumber == -1)
|
||||
break;
|
||||
if(lineNumber == 0) {
|
||||
pos.fTextByteIndex = 0;
|
||||
if(previousX != nullptr)
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
}
|
||||
else {
|
||||
skia::textlayout::LineMetrics prevLineMetrics;
|
||||
paragraph->getLineMetricsAt(lineNumber - 1, &prevLineMetrics);
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
Vector2f pointToCheck = {(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), prevLineMetrics.fBaseline};
|
||||
|
||||
std::optional<TextPosition> newPos = get_text_pos_closest_to_point(pointToCheck);
|
||||
if(newPos.has_value())
|
||||
pos = newPos.value();
|
||||
if(previousX != nullptr && !previousX->has_value()) {
|
||||
*previousX = pointToCheck.x();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::DOWN: {
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
Vector2f pointToCheck{cursorRect.x(), cursorRect.bottom() + 0.1f};
|
||||
pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position;
|
||||
assert(previousX != nullptr);
|
||||
if(text.size() == 0)
|
||||
break;
|
||||
if(pos.fTextByteIndex == text.size())
|
||||
break;
|
||||
int lineNumber = paragraph->getLineNumberAt(pos.fTextByteIndex);
|
||||
if(lineNumber == -1)
|
||||
break;
|
||||
if(lineNumber == static_cast<int>(paragraph->lineNumber() - 1)) {
|
||||
pos.fTextByteIndex = text.size();
|
||||
if(previousX != nullptr)
|
||||
*previousX = get_cursor_rect(pos).centerX();
|
||||
}
|
||||
else {
|
||||
skia::textlayout::LineMetrics nextLineMetrics;
|
||||
paragraph->getLineMetricsAt(lineNumber + 1, &nextLineMetrics);
|
||||
SkRect cursorRect = get_cursor_rect(pos);
|
||||
Vector2f pointToCheck{(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), nextLineMetrics.fBaseline};
|
||||
|
||||
std::optional<TextPosition> newPos = get_text_pos_closest_to_point(pointToCheck);
|
||||
if(newPos.has_value())
|
||||
pos = newPos.value();
|
||||
if(previousX != nullptr && !previousX->has_value()) {
|
||||
*previousX = pointToCheck.x();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::LEFT_WORD: {
|
||||
if(pos.fTextByteIndex == 0)
|
||||
break;
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
|
||||
size_t byteIndexToRet = 0;
|
||||
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
||||
breakIterator->setText(text.c_str(), text.size());
|
||||
|
||||
while(!breakIterator->isDone()) {
|
||||
size_t nextByteIndex = breakIterator->next();
|
||||
if(nextByteIndex >= pos.fTextByteIndex)
|
||||
return {byteIndexToRet, 0};
|
||||
|
||||
const char* tPtr = text.c_str() + nextByteIndex;
|
||||
SkUnichar u = SkUTF::NextUTF8(&tPtr, text.c_str() + text.size());
|
||||
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
||||
byteIndexToRet = nextByteIndex;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::RIGHT_WORD: {
|
||||
if(pos.fTextByteIndex == text.size())
|
||||
return pos;
|
||||
auto u = SkUnicodes::ICU::Make();
|
||||
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords);
|
||||
breakIterator->setText(text.c_str() + pos.fTextByteIndex, text.size() - pos.fTextByteIndex);
|
||||
while(!breakIterator->isDone()) {
|
||||
TextPosition p = {breakIterator->next() + pos.fTextByteIndex, 0};
|
||||
if(p.fTextByteIndex == text.size())
|
||||
return p;
|
||||
const char* tPtr = text.c_str() + p.fTextByteIndex;
|
||||
SkUnichar u = SkUTF::NextUTF8(&tPtr, text.c_str() + text.size());
|
||||
if(!SkUnicodes::ICU::Make()->isWhitespace(u))
|
||||
return p;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Movement::HOME:
|
||||
pos = 0;
|
||||
pos.fTextByteIndex = 0;
|
||||
break;
|
||||
case Movement::END:
|
||||
pos = text.size();
|
||||
pos.fTextByteIndex = text.size();
|
||||
break;
|
||||
}
|
||||
return pos;
|
||||
|
|
@ -121,8 +284,10 @@ uint64_t RichTextBox::move(Movement movement, uint64_t pos) {
|
|||
}
|
||||
|
||||
void RichTextBox::set_width(float newWidth) {
|
||||
width = newWidth;
|
||||
needsRebuild = true;
|
||||
if(width != newWidth) {
|
||||
width = newWidth;
|
||||
needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextBox::set_allow_newlines(bool allow) {
|
||||
|
|
@ -138,6 +303,7 @@ void RichTextBox::rebuild() {
|
|||
if(needsRebuild) {
|
||||
skia::textlayout::ParagraphStyle pStyle;
|
||||
pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft);
|
||||
pStyle.setTextDirection(skia::textlayout::TextDirection::kLtr);
|
||||
skia::textlayout::TextStyle tStyle;
|
||||
tStyle.setFontSize(20.0f);
|
||||
tStyle.setFontFamilies({SkString{"Roboto"}});
|
||||
|
|
@ -147,25 +313,26 @@ void RichTextBox::rebuild() {
|
|||
a.addText(text.c_str(), text.length());
|
||||
paragraph = a.Build();
|
||||
paragraph->layout(width);
|
||||
needsRebuild = false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t RichTextBox::insert(uint64_t pos, const std::string& textToInsert) {
|
||||
text.insert(pos, textToInsert);
|
||||
RichTextBox::TextPosition RichTextBox::insert(TextPosition pos, const std::string& textToInsert) {
|
||||
text.insert(pos.fTextByteIndex, textToInsert);
|
||||
needsRebuild = true;
|
||||
return pos + textToInsert.size();
|
||||
return TextPosition{pos.fTextByteIndex + textToInsert.size()};
|
||||
}
|
||||
|
||||
uint64_t RichTextBox::remove(uint64_t start, uint64_t end) {
|
||||
if(start > end)
|
||||
std::swap(start, end);
|
||||
RichTextBox::TextPosition RichTextBox::remove(TextPosition start, TextPosition end) {
|
||||
TextPosition trueBegin = std::min(start, end);
|
||||
TextPosition trueEnd = std::max(start, end);
|
||||
|
||||
text.erase(start, end - start);
|
||||
text.erase(trueBegin.fTextByteIndex, trueEnd.fTextByteIndex - trueBegin.fTextByteIndex);
|
||||
needsRebuild = true;
|
||||
return start;
|
||||
return trueBegin;
|
||||
}
|
||||
|
||||
SkRect RichTextBox::get_cursor_rect(uint64_t pos) {
|
||||
SkRect RichTextBox::get_cursor_rect(TextPosition pos) {
|
||||
SkPoint topLeft;
|
||||
float height;
|
||||
|
||||
|
|
@ -173,41 +340,158 @@ SkRect RichTextBox::get_cursor_rect(uint64_t pos) {
|
|||
topLeft = {0.0f, 0.0f};
|
||||
height = paragraph->getHeight();
|
||||
}
|
||||
else if(pos == text.size()) {
|
||||
std::vector<skia::textlayout::TextBox> posBoxes = paragraph->getRectsForRange(pos - 1, pos, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax);
|
||||
if(text.back() == '\n') {
|
||||
topLeft = {0.0f, posBoxes.back().rect.bottom()};
|
||||
height = std::abs(paragraph->getHeight() - topLeft.y());
|
||||
else {
|
||||
if(pos.fTextByteIndex == text.size()) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool exists = paragraph->getGlyphClusterAt(prev_grapheme(pos.fTextByteIndex), &glyphInfo);
|
||||
if(exists) {
|
||||
topLeft = glyphInfo.fBounds.TR();
|
||||
height = glyphInfo.fBounds.height();
|
||||
}
|
||||
else {
|
||||
topLeft = {0.0f, 0.0f};
|
||||
height = 0.0f;
|
||||
}
|
||||
}
|
||||
else {
|
||||
topLeft = posBoxes.front().rect.TR();
|
||||
height = posBoxes.front().rect.height();
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
bool exists = paragraph->getGlyphClusterAt(pos.fTextByteIndex, &glyphInfo);
|
||||
if(exists) {
|
||||
topLeft = glyphInfo.fBounds.TL();
|
||||
height = glyphInfo.fBounds.height();
|
||||
}
|
||||
else {
|
||||
topLeft = {0.0f, 0.0f};
|
||||
height = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::vector<skia::textlayout::TextBox> posBoxes = paragraph->getRectsForRange(pos, pos + 1, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax);
|
||||
topLeft = posBoxes.front().rect.TL();
|
||||
height = posBoxes.front().rect.height();
|
||||
}
|
||||
return SkRect::MakeXYWH(topLeft.x(), topLeft.y(), 3.0f, height);
|
||||
}
|
||||
|
||||
std::vector<skia::textlayout::TextBox> RichTextBox::rects_between_text_positions(TextPosition start, TextPosition end) {
|
||||
TextPosition trueStart = std::min(start, end);
|
||||
TextPosition trueEnd = std::max(start, end);
|
||||
|
||||
unsigned glyphID1 = 0, glyphID2 = 0;
|
||||
|
||||
// Commenting this so that illegal instructions become more apparent (they're there even if its uncommented, but can't be caught as easily)
|
||||
//if(trueStart == trueEnd)
|
||||
// return {};
|
||||
|
||||
if(text.empty())
|
||||
return {};
|
||||
else {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
|
||||
if(trueStart.fTextByteIndex == text.size()) {
|
||||
if(paragraph->getGlyphClusterAt(prev_grapheme(trueStart.fTextByteIndex), &glyphInfo)) {
|
||||
glyphID1 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position + 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(paragraph->getGlyphClusterAt(trueStart.fTextByteIndex, &glyphInfo))
|
||||
glyphID1 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position;
|
||||
}
|
||||
|
||||
if(trueEnd.fTextByteIndex == text.size()) {
|
||||
if(paragraph->getGlyphClusterAt(prev_grapheme(trueEnd.fTextByteIndex), &glyphInfo))
|
||||
glyphID2 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position + 1;
|
||||
}
|
||||
else {
|
||||
if(paragraph->getGlyphClusterAt(trueEnd.fTextByteIndex, &glyphInfo))
|
||||
glyphID2 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position;
|
||||
}
|
||||
}
|
||||
|
||||
if(glyphID1 == glyphID2)
|
||||
return {};
|
||||
|
||||
return paragraph->getRectsForRange(glyphID1, glyphID2, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax);
|
||||
}
|
||||
|
||||
constexpr float SELECTION_RECT_EXTRA_AREA = 2.0f;
|
||||
|
||||
std::vector<SkRect> RichTextBox::rects_between_text_positions_2(TextPosition start, TextPosition end) {
|
||||
TextPosition trueStart = std::min(start, end);
|
||||
TextPosition trueEnd = std::max(start, end);
|
||||
|
||||
std::vector<std::pair<int, SkRect>> lineRects;
|
||||
|
||||
if(text.empty())
|
||||
return {};
|
||||
else {
|
||||
for(size_t i = trueStart.fTextByteIndex; i < trueEnd.fTextByteIndex; i++) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
if(paragraph->getGlyphClusterAt(i, &glyphInfo)) {
|
||||
int lineNumber = paragraph->getLineNumberAt(i);
|
||||
if(lineNumber == -1)
|
||||
break;
|
||||
skia::textlayout::LineMetrics lineMetrics;
|
||||
if(paragraph->getLineMetricsAt(lineNumber, &lineMetrics)) {
|
||||
SkRect r = SkRect::MakeLTRB(glyphInfo.fBounds.left(), lineMetrics.fBaseline - lineMetrics.fAscent, glyphInfo.fBounds.right(), lineMetrics.fBaseline + lineMetrics.fDescent);
|
||||
auto it = std::find_if(lineRects.begin(), lineRects.end(), [&](const auto& p) {
|
||||
SkRect checkCollideWith = p.second;
|
||||
checkCollideWith.fLeft = checkCollideWith.fLeft - SELECTION_RECT_EXTRA_AREA;
|
||||
checkCollideWith.fRight = checkCollideWith.fRight + SELECTION_RECT_EXTRA_AREA;
|
||||
return p.first == lineNumber && checkCollideWith.intersects(r);
|
||||
});
|
||||
if(it != lineRects.end())
|
||||
it->second.join(r);
|
||||
else
|
||||
lineRects.emplace_back(lineNumber, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<SkRect> toRet;
|
||||
for(auto& l : lineRects)
|
||||
toRet.emplace_back(l.second);
|
||||
|
||||
return toRet;
|
||||
}
|
||||
|
||||
void RichTextBox::rects_between_text_positions_func(TextPosition start, TextPosition end, std::function<void(const SkRect& r)> f) {
|
||||
TextPosition trueStart = std::min(start, end);
|
||||
TextPosition trueEnd = std::max(start, end);
|
||||
|
||||
if(text.empty())
|
||||
return;
|
||||
else {
|
||||
for(size_t i = trueStart.fTextByteIndex; i < trueEnd.fTextByteIndex; i++) {
|
||||
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
|
||||
if(paragraph->getGlyphClusterAt(i, &glyphInfo)) {
|
||||
int lineNumber = paragraph->getLineNumberAt(i);
|
||||
if(lineNumber == -1)
|
||||
break;
|
||||
skia::textlayout::LineMetrics lineMetrics;
|
||||
if(paragraph->getLineMetricsAt(lineNumber, &lineMetrics)) {
|
||||
SkRect r = SkRect::MakeLTRB(glyphInfo.fBounds.left() - SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline - lineMetrics.fAscent - SELECTION_RECT_EXTRA_AREA, glyphInfo.fBounds.right() + SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline + lineMetrics.fDescent + SELECTION_RECT_EXTRA_AREA);
|
||||
f(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RichTextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) {
|
||||
rebuild();
|
||||
|
||||
if(paintOpts.cursor.has_value()) {
|
||||
auto& cur = paintOpts.cursor.value();
|
||||
SkPaint selection{color_mul_alpha(paintOpts.cursorColor, 0.6f)};
|
||||
uint64_t trueSelectionStart = std::min(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
uint64_t trueSelectionEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos);
|
||||
std::vector<skia::textlayout::TextBox> selectionBoxes = paragraph->getRectsForRange(trueSelectionStart, trueSelectionEnd, skia::textlayout::RectHeightStyle::kTight, skia::textlayout::RectWidthStyle::kTight);
|
||||
for(auto& selectionBox : selectionBoxes) {
|
||||
SkRect rectFixSmallOverlap = SkRect::MakeXYWH(selectionBox.rect.x(), selectionBox.rect.y(), selectionBox.rect.width(), std::max(selectionBox.rect.height() - 0.25f, 0.0f));
|
||||
canvas->drawRect(rectFixSmallOverlap, selection);
|
||||
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();
|
||||
}
|
||||
|
||||
if(cur.pos <= text.size()) {
|
||||
canvas->drawRect(get_cursor_rect(cur.pos), SkPaint{paintOpts.cursorColor});
|
||||
if(cur.pos.fTextByteIndex <= text.size()) {
|
||||
canvas->drawRect(get_cursor_rect(cur.pos), p);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,25 +14,39 @@ class RichTextBox {
|
|||
RIGHT,
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT_WORD,
|
||||
RIGHT_WORD,
|
||||
HOME,
|
||||
END
|
||||
};
|
||||
|
||||
struct TextPosition {
|
||||
// Set to uint32_t values because theyll be saved and sent over networks. Size has to be consistent
|
||||
size_t fTextByteIndex = std::numeric_limits<uint32_t>::max();
|
||||
size_t fParagraphIndex = std::numeric_limits<uint32_t>::max();
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a((uint32_t)fTextByteIndex, (uint32_t)fParagraphIndex);
|
||||
}
|
||||
bool operator==(const TextPosition& o) const = default;
|
||||
bool operator<(const TextPosition& o) const;
|
||||
};
|
||||
|
||||
struct Cursor {
|
||||
uint64_t pos = 0;
|
||||
uint64_t selectionBeginPos = 0;
|
||||
uint64_t selectionEndPos = 0;
|
||||
TextPosition pos = {0, 0};
|
||||
TextPosition selectionBeginPos = {0, 0};
|
||||
TextPosition selectionEndPos = {0, 0};
|
||||
std::optional<float> previousX;
|
||||
};
|
||||
|
||||
struct PaintOpts {
|
||||
SkColor4f cursorColor = {1, 0, 0, 1};
|
||||
Vector3f cursorColor = {1, 0, 0};
|
||||
std::optional<Cursor> cursor;
|
||||
};
|
||||
|
||||
uint64_t move(Movement movement, uint64_t pos);
|
||||
TextPosition move(Movement movement, TextPosition pos, std::optional<float>* previousX = nullptr);
|
||||
void set_width(float newWidth);
|
||||
uint64_t insert(uint64_t pos, const std::string& textToInsert);
|
||||
uint64_t remove(uint64_t start, uint64_t end);
|
||||
TextPosition insert(TextPosition pos, const std::string& textToInsert);
|
||||
TextPosition remove(TextPosition start, TextPosition end);
|
||||
void set_font_collection(const sk_sp<skia::textlayout::FontCollection>& fC);
|
||||
void paint(SkCanvas* canvas, const PaintOpts& paintOpts);
|
||||
void set_allow_newlines(bool allow);
|
||||
|
|
@ -50,17 +64,29 @@ class RichTextBox {
|
|||
SELECT_ALL
|
||||
};
|
||||
|
||||
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);
|
||||
std::string process_copy(Cursor& cur);
|
||||
std::string process_cut(Cursor& cur);
|
||||
|
||||
void process_text_input(Cursor& cur, const std::string& in);
|
||||
private:
|
||||
SkRect get_cursor_rect(uint64_t pos);
|
||||
size_t count_grapheme();
|
||||
size_t next_grapheme(size_t textBytePos);
|
||||
size_t prev_grapheme(size_t textBytePos);
|
||||
|
||||
std::vector<skia::textlayout::TextBox> rects_between_text_positions(TextPosition start, TextPosition end);
|
||||
std::vector<SkRect> rects_between_text_positions_2(TextPosition start, TextPosition end);
|
||||
void rects_between_text_positions_func(TextPosition start, TextPosition end, std::function<void(const SkRect& r)> f);
|
||||
|
||||
SkRect get_cursor_rect(TextPosition pos);
|
||||
|
||||
std::optional<TextPosition> get_text_pos_closest_to_point(const Vector2f& point);
|
||||
|
||||
void rebuild();
|
||||
|
||||
bool newlinesAllowed = true;
|
||||
float width = 100.0f;
|
||||
float width = 0.0f;
|
||||
float fontSize = 0.0f;
|
||||
bool needsRebuild = true;
|
||||
sk_sp<skia::textlayout::FontCollection> fontCollection;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue