Tabs can be properly rendered in textbox, and don't convert to spaces

This commit is contained in:
Yousef Khadadeh 2025-11-27 13:55:07 +03:00
parent 700238ffee
commit da7f8af582
2 changed files with 96 additions and 13 deletions

View file

@ -2,9 +2,11 @@
#include "Helpers/Networking/ByteStream.hpp"
#include "TextStyleModifier.hpp"
#include "cereal/archives/portable_binary.hpp"
#include <include/core/SkFontMetrics.h>
#include <limits>
#include <modules/skparagraph/include/DartTypes.h>
#include <modules/skparagraph/include/Metrics.h>
#include <modules/skparagraph/include/Paragraph.h>
#include <modules/skparagraph/include/TextStyle.h>
#include <modules/skparagraph/src/TextLine.h>
#include <modules/skunicode/include/SkUnicode.h>
@ -296,6 +298,7 @@ TextPosition TextBox::get_text_pos_closest_to_point(Vector2f point) {
if(foundGlyph) {
TextPosition toRet = {pIndex, 0};
toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start;
toRet = render_text_pos_to_byte_text_pos(toRet);
if(glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr) {
if(std::abs(glyphInfo.fBounds.right() - point.x()) < std::abs(glyphInfo.fBounds.left() - point.x()))
@ -652,7 +655,7 @@ TextPosition TextBox::move(Movement movement, TextPosition pos, std::optional<fl
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);
int lineNumber = get_line_number_at_from_byte_text_pos({pos.fParagraphIndex, (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)
@ -684,7 +687,7 @@ TextPosition TextBox::move(Movement movement, TextPosition pos, std::optional<fl
case Movement::DOWN: {
assert(previousX != nullptr);
int lineNumber = paragraphs[pos.fParagraphIndex].p->getLineNumberAt(pos.fTextByteIndex);
int lineNumber = get_line_number_at_from_byte_text_pos(pos);
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)
@ -777,6 +780,11 @@ TextPosition TextBox::move(Movement movement, TextPosition pos, std::optional<fl
}
int TextBox::get_line_number_at_from_byte_text_pos(TextPosition pos) {
TextPosition renderPos = byte_text_pos_to_render_text_pos(pos);
return paragraphs[pos.fParagraphIndex].p->getLineNumberAt(renderPos.fTextByteIndex);
}
void TextBox::set_width(float newWidth) {
if(width != newWidth) {
width = std::max(newWidth, 4.0f);
@ -832,7 +840,7 @@ void TextBox::rebuild() {
for(;;) {
if(nextTStyleModIt == tStyleMods.end() || nextTStyleModIt->pos.fParagraphIndex != pIndex) {
a.pushStyle(tStyle);
a.addText(pData.text.c_str() + tIndex, pData.text.length() - tIndex);
rebuild_build_run_of_text_with_tabs(std::string_view(pData.text.c_str() + tIndex, pData.text.length() - tIndex), tStyle, a);
a.pop();
break;
}
@ -842,7 +850,7 @@ void TextBox::rebuild() {
// Print with previous style
a.pushStyle(tStyle);
a.addText(pData.text.c_str() + tIndex, nextTStyleMod.pos.fTextByteIndex - tIndex);
rebuild_build_run_of_text_with_tabs(std::string_view(pData.text.c_str() + tIndex, nextTStyleMod.pos.fTextByteIndex - tIndex), tStyle, a);
a.pop();
tIndex = nextTStyleMod.pos.fTextByteIndex;
@ -871,6 +879,37 @@ void TextBox::rebuild() {
}
}
void TextBox::rebuild_build_run_of_text_with_tabs(std::string_view s, const skia::textlayout::TextStyle& tStyle, skia::textlayout::ParagraphBuilder& a) {
if(s.empty())
return;
size_t tabLocation = s.find('\t');
for(;;) {
if(tabLocation == std::string_view::npos) {
a.addText(s.data(), s.size());
break;
}
a.addText(s.data(), tabLocation);
skia::textlayout::PlaceholderStyle tabPlaceholderStyle;
skia::textlayout::ParagraphStyle tabPStyle;
tabPStyle.setTextStyle(tStyle);
skia::textlayout::ParagraphBuilderImpl tabParagraphBuilder(tabPStyle, fontData->collection, SkUnicodes::ICU::Make());
std::string indentStr(tabWidth, ' ');
tabParagraphBuilder.addText(indentStr.c_str(), indentStr.size());
auto tabParagraph = tabParagraphBuilder.Build();
tabParagraph->layout(10000000.0f);
tabPlaceholderStyle.fWidth = tabParagraph->getMaxIntrinsicWidth();
tabPlaceholderStyle.fHeight = tabParagraph->getHeight();
tabPlaceholderStyle.fAlignment = skia::textlayout::PlaceholderAlignment::kMiddle;
a.addPlaceholder(tabPlaceholderStyle);
s = std::string_view(s.data() + tabLocation + 1, s.size() - tabLocation - 1);
tabLocation = s.find('\t');
}
}
void TextBox::remove_duplicate_text_style_mods() {
TextStyleModifier::ModifierMap previouslyAppliedMods = TextStyleModifier::get_default_modifiers();
@ -888,10 +927,6 @@ void TextBox::remove_duplicate_text_style_mods() {
});
}
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);
@ -914,7 +949,7 @@ TextPosition TextBox::insert(TextPosition pos, std::string_view textToInsert, co
pos.fTextByteIndex = 0;
}
}
else if(c == '\t') {
else if(c == '\t' && !newlinesAllowed) {
for(auto& [p, tStylesInPos] : tStyleMods) {
if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex && !(p.fTextByteIndex == pos.fTextByteIndex && p.fTextByteIndex == 0))
p.fTextByteIndex += tabWidth;
@ -993,6 +1028,43 @@ TextPosition TextBox::remove(TextPosition p1, TextPosition p2) {
return start;
}
TextPosition TextBox::byte_text_pos_to_render_text_pos(TextPosition pos) {
pos = move(Movement::NOWHERE, pos);
const ParagraphData& pData = paragraphs[pos.fParagraphIndex];
size_t tabsBeforeTextByteIndex = std::count(pData.text.begin(), pData.text.begin() + pos.fTextByteIndex, '\t');
pos.fTextByteIndex += tabsBeforeTextByteIndex * 2;
return pos;
}
TextPosition TextBox::render_text_pos_to_byte_text_pos(TextPosition pos) {
if(pos.fParagraphIndex >= paragraphs.size()) {
pos.fParagraphIndex = paragraphs.size() - 1;
pos.fTextByteIndex = get_render_text_length(paragraphs[pos.fParagraphIndex].text);
}
else if(pos.fTextByteIndex > get_render_text_length(paragraphs[pos.fParagraphIndex].text))
pos.fTextByteIndex = get_render_text_length(paragraphs[pos.fParagraphIndex].text);
const std::string& text = paragraphs[pos.fParagraphIndex].text;
size_t renderPosToTrack = 0;
size_t i;
for(i = 0; i <= text.size(); i++) {
if(renderPosToTrack == pos.fTextByteIndex)
break;
assert(renderPosToTrack < pos.fTextByteIndex);
if(i != text.size() && text[i] == '\t')
renderPosToTrack += 3;
else
renderPosToTrack++;
}
pos.fTextByteIndex = i;
return pos;
}
size_t TextBox::get_render_text_length(const std::string& str) {
size_t tabCount = std::count(str.begin(), str.end(), '\t');
return str.size() + tabCount * 2;
}
SkRect TextBox::get_cursor_rect(TextPosition pos) {
pos = move(Movement::NOWHERE, pos);
@ -1029,7 +1101,9 @@ SkRect TextBox::get_cursor_rect(TextPosition pos) {
}
else {
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;
bool exists = pData.p->getGlyphClusterAt(prev_grapheme(pData.text, pos.fTextByteIndex), &glyphInfo);
pos.fTextByteIndex = prev_grapheme(pData.text, pos.fTextByteIndex);
pos = byte_text_pos_to_render_text_pos(pos);
bool exists = pData.p->getGlyphClusterAt(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};
@ -1065,12 +1139,14 @@ skia::textlayout::TextDirection TextBox::get_suggested_direction(size_t pIndex)
void TextBox::rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function<void(const SkRect& r)> f) {
auto [start, end] = get_start_end_text_pos(p1, p2);
start = byte_text_pos_to_render_text_pos(start);
end = byte_text_pos_to_render_text_pos(end);
constexpr float SELECTION_RECT_EXTRA_AREA = 1.0f;
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();
size_t tEnd = p == end.fParagraphIndex ? end.fTextByteIndex : get_render_text_length(paragraphs[p].text);
ParagraphData& pData = paragraphs[p];
for(size_t t = tStart; t < tEnd; t++) {
skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo;

View file

@ -9,6 +9,7 @@
#include "TextStyleModifier.hpp"
#include "cereal/archives/portable_binary.hpp"
#include "ParagraphStyleData.hpp"
#include <modules/skparagraph/include/ParagraphBuilder.h>
namespace RichText {
@ -140,7 +141,6 @@ class TextBox {
std::pair<std::string, TextData> 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);
void process_rich_text_input(Cursor& cur, const TextData& richText);
@ -166,14 +166,21 @@ class TextBox {
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);
TextPosition byte_text_pos_to_render_text_pos(TextPosition pos);
TextPosition render_text_pos_to_byte_text_pos(TextPosition pos);
size_t get_render_text_length(const std::string& str);
void remove_duplicate_text_style_mods();
void rebuild();
void rebuild_build_run_of_text_with_tabs(std::string_view s, const skia::textlayout::TextStyle& tStyle, skia::textlayout::ParagraphBuilder& a);
int get_line_number_at_from_byte_text_pos(TextPosition pos);
bool newlinesAllowed = true;
float width = 0.0f;
bool needsRebuild = true;
unsigned tabWidth = 8;
unsigned tabWidth = 4;
std::shared_ptr<FontData> fontData;
struct ParagraphData {