diff --git a/src/DrawComponents/DrawTextBox.cpp b/src/DrawComponents/DrawTextBox.cpp index 77538ea..4f6b586 100644 --- a/src/DrawComponents/DrawTextBox.cpp +++ b/src/DrawComponents/DrawTextBox.cpp @@ -54,6 +54,10 @@ void DrawTextBox::update_from_delayed_ptr(const std::shared_ptr& void DrawTextBox::init_text_box(DrawingProgram& drawP) { textBox->set_font_collection(drawP.world.main.fonts.collection); // Getting a segfault relating to the paragraph cache means that the font collection hasn't been set yet textBox->set_width(d.p2.x() - d.p1.x()); + skia::textlayout::TextStyle tStyle; + tStyle.setFontFamilies({SkString{"Roboto"}}); + tStyle.setFontSize(16.0f); + textBox->set_initial_text_style(tStyle); } void DrawTextBox::draw(SkCanvas* canvas, const DrawData& drawData) { diff --git a/src/DrawingProgram/TextBoxEditTool.cpp b/src/DrawingProgram/TextBoxEditTool.cpp index a7c1895..42160a2 100644 --- a/src/DrawingProgram/TextBoxEditTool.cpp +++ b/src/DrawingProgram/TextBoxEditTool.cpp @@ -4,6 +4,7 @@ #include "../DrawData.hpp" #include "Helpers/SCollision.hpp" #include +#include #include #include "EditTool.hpp" @@ -16,7 +17,21 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr& comp) { Toolbar& t = drawP.world.main.toolbar; t.gui.push_id("edit tool text"); t.gui.text_label_centered("Edit Text"); - //t.gui.slider_scalar_field("Text Relative Size", "Text Size", &a->d.textSize, 5.0f, 100.0f); + if(t.gui.text_button_wide("Bold button", "Bold")) { + auto boldMod = std::make_shared(); + boldMod->set_weight(SkFontStyle::kBold_Weight); + a->textBox->set_text_style_modifier_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos, boldMod); + } + if(t.gui.text_button_wide("Italic button", "Italic")) { + auto italicMod = std::make_shared(); + italicMod->set_slant(SkFontStyle::Slant::kItalic_Slant); + a->textBox->set_text_style_modifier_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos, italicMod); + } + if(t.gui.text_button_wide("Red button", "Red")) { + auto redMod = std::make_shared(); + redMod->color = {1.0f, 0.0f, 0.0f, 1.0f}; + a->textBox->set_text_style_modifier_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos, redMod); + } //t.gui.left_to_right_line_layout([&]() { // CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(40)}}}) { // if(t.gui.color_button("Text Color", &a->d.textColor, &a->d.textColor == t.colorRight)) diff --git a/src/GUIStuff/Elements/TextBox.hpp b/src/GUIStuff/Elements/TextBox.hpp index 98f2dd5..37226eb 100644 --- a/src/GUIStuff/Elements/TextBox.hpp +++ b/src/GUIStuff/Elements/TextBox.hpp @@ -2,6 +2,7 @@ #include "Element.hpp" #include "../../RichTextBox/RichTextBox.hpp" #include +#include namespace GUIStuff { @@ -66,15 +67,13 @@ template class TextBox : public Element { paintOpts.cursor = *cur; paintOpts.cursorColor = {io.theme->fillColor1.fR, io.theme->fillColor1.fG, io.theme->fillColor1.fB}; - auto fontFamiliesMod = std::make_shared(); - fontFamiliesMod->families.emplace_back("Roboto"); - textbox->set_initial_text_style_modifier(fontFamiliesMod); - auto fontSizeMod = std::make_shared(); - fontSizeMod->size = io.fontSize; - textbox->set_initial_text_style_modifier(fontSizeMod); - auto fontColorMod = std::make_shared(); - fontColorMod->color = convert_vec4(io.theme->frontColor1); - textbox->set_initial_text_style_modifier(fontColorMod); + skia::textlayout::TextStyle tStyle; + std::vector families; + families.emplace_back("Roboto"); + tStyle.setFontFamilies(families); + tStyle.setFontSize(io.fontSize); + tStyle.setForegroundPaint(SkPaint{io.theme->frontColor1}); + textbox->set_initial_text_style(tStyle); textbox->paint(canvas, paintOpts); diff --git a/src/RichTextBox/RichTextBox.cpp b/src/RichTextBox/RichTextBox.cpp index e0c6231..3869c7c 100644 --- a/src/RichTextBox/RichTextBox.cpp +++ b/src/RichTextBox/RichTextBox.cpp @@ -12,40 +12,31 @@ #include #include -void RichTextBox::RichTextData::Paragraph::save(cereal::PortableBinaryOutputArchive& a) const { - a(text); - a(static_cast(tStyles.size())); - for(auto& [pos, styles] : tStyles) { - a(pos, static_cast(styles.size())); - for(auto& [styleType, style] : styles) - a(static_cast(styleType), *style); - } +void RichTextBox::TextStyleRangeModifier::save(cereal::PortableBinaryOutputArchive& a) const { + a(start, end, modifier->get_type(), *modifier); } -void RichTextBox::RichTextData::Paragraph::load(cereal::PortableBinaryInputArchive& a) { - a(text); - uint32_t styleCount; - a(styleCount); - tStyles.clear(); - for(uint32_t i = 0; i < styleCount; i++) { - uint32_t pos; - uint16_t styleCountAtPos; - a(pos, styleCountAtPos); - TextModAtPosContainer& mods = tStyles[pos]; - for(uint16_t j = 0; j < styleCountAtPos; j++) { - uint16_t styleType; - a(styleType); - std::shared_ptr modifier = TextStyleModifier::allocate_modifier(static_cast(styleType)); - a(*modifier); - mods.emplace(static_cast(styleType), modifier); - } - } +void RichTextBox::TextStyleRangeModifier::load(cereal::PortableBinaryInputArchive& a) { + a(start, end); + TextStyleModifier::ModifierType mType; + a(mType); + modifier = TextStyleModifier::allocate_modifier(mType); + a(*modifier); +} + +void RichTextBox::TextStyleRangeModifier::apply_to_start_and_end(const std::function& f) { + f(start); + f(end); } bool RichTextBox::TextPosition::operator<(const RichTextBox::TextPosition& o) const { return (this->fParagraphIndex < o.fParagraphIndex) || (this->fParagraphIndex == o.fParagraphIndex && this->fTextByteIndex < o.fTextByteIndex); } +bool RichTextBox::TextPosition::operator>(const RichTextBox::TextPosition& o) const { + return !(*this < o) && !(*this == o); +} + RichTextBox::RichTextBox() { paragraphs.emplace_back(); } @@ -97,8 +88,8 @@ void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool sh process_text_input(cur, "\t"); break; case InputKey::SELECT_ALL: - cur.selectionBeginPos = {0, 0}; - cur.pos = cur.selectionEndPos = {paragraphs.back().text.size(), paragraphs.size() - 1}; + cur.selectionBeginPos = move(Movement::HOME, cur.pos); + cur.pos = cur.selectionEndPos = move(Movement::END, cur.pos); break; } @@ -216,7 +207,7 @@ RichTextBox::TextPosition RichTextBox::get_text_pos_closest_to_point(Vector2f po bool foundGlyph = pData.p->getClosestGlyphClusterAt(point.x(), point.y(), &glyphInfo); if(foundGlyph) { - TextPosition toRet = {0, pIndex}; + TextPosition toRet = {pIndex, 0}; toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start; if(glyphInfo.fGlyphClusterPosition == skia::textlayout::TextDirection::kLtr) { @@ -245,7 +236,7 @@ RichTextBox::TextPosition RichTextBox::get_text_pos_closest_to_point(Vector2f po return toRet; } else - return TextPosition{0, pIndex}; + return TextPosition{pIndex, 0}; } RichTextBox::RichTextData RichTextBox::get_rich_text_data() { @@ -253,13 +244,14 @@ RichTextBox::RichTextData RichTextBox::get_rich_text_data() { for(auto& p : paragraphs) { toRet.paragraphs.emplace_back(); toRet.paragraphs.back().text = p.text; - toRet.paragraphs.back().tStyles = p.tStyles; } + toRet.tStyleModifiers = tStyleModifiers; return toRet; } void RichTextBox::clear_text() { paragraphs.clear(); + tStyleModifiers.clear(); paragraphs.emplace_back(); } @@ -273,10 +265,11 @@ void RichTextBox::set_rich_text_data(const RichTextData& richText) { for(auto& p : richText.paragraphs) { paragraphs.emplace_back(); paragraphs.back().text = p.text; - paragraphs.back().tStyles = p.tStyles; } if(richText.paragraphs.empty()) paragraphs.emplace_back(); + tStyleModifiers = richText.tStyleModifiers; + fit_text_style_ranges_in_text(); needsRebuild = true; } @@ -307,14 +300,26 @@ std::string RichTextBox::get_text_between(TextPosition p1, TextPosition p2) { return toRet; } -void RichTextBox::set_initial_text_style_modifier(const std::shared_ptr& modifier) { - std::shared_ptr& modToSet = paragraphs.back().tStyles[0][modifier->get_type()]; - needsRebuild |= ((modToSet == nullptr) || (!modifier->equivalent(*modToSet))); - modToSet = modifier; + +void RichTextBox::set_initial_text_style(const skia::textlayout::TextStyle& tStyle) { + if(!tStyle.equals(initialTStyle)) { + initialTStyle = tStyle; + needsRebuild = true; + } } void RichTextBox::set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr& modifier) { - needsRebuild = true; + p1 = move(Movement::NOWHERE, p1); + p2 = move(Movement::NOWHERE, p2); + if(p1 != p2) { + fit_text_style_ranges_in_text(); + TextStyleRangeModifier toPlace; + toPlace.start = std::min(p1, p2); + toPlace.end = std::max(p1, p2); + toPlace.modifier = modifier; + tStyleModifiers.emplace_back(toPlace); + needsRebuild = true; + } } RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, std::optional* previousX, bool flipDependingOnTextDirection) { @@ -378,7 +383,7 @@ RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, 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}; + pos = TextPosition{pos.fParagraphIndex - 1, 0}; } else { skia::textlayout::LineMetrics prevLineMetrics; @@ -410,7 +415,7 @@ RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, if((lineNumber == -1 || lineNumber == static_cast(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}; + pos = TextPosition{pos.fParagraphIndex + 1, 0}; } else { skia::textlayout::LineMetrics nextLineMetrics; @@ -511,35 +516,71 @@ void RichTextBox::set_font_collection(const sk_sp>> modifierStackCommands; - for(ParagraphData& pData : paragraphs) { + for(TextStyleRangeModifier& modifierRange : tStyleModifiers) { + modifierStackCommands[modifierRange.start].emplace_back(modifierRange.modifier); + modifierStackCommands[modifierRange.end].emplace_back(modifierRange.modifier); + } + + auto nextStackCommandIt = modifierStackCommands.begin(); + std::unordered_map>> modifierStacks; + + 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()); - size_t startPos = 0; - for(auto& [pos, tStylesAtPos] : pData.tStyles) { - if(pos > pData.text.size()) - break; - - for(auto& [styleType, styleMod] : tStylesAtPos) - styleMod->modify_text_style(tStyle); - if(startPos != pos) { + for(;;) { + if(nextStackCommandIt == modifierStackCommands.end() || nextStackCommandIt->first.fParagraphIndex != pIndex) { a.pushStyle(tStyle); - a.addText(pData.text.c_str() + startPos, pos - startPos); + a.addText(pData.text.c_str() + tIndex, pData.text.length() - tIndex); a.pop(); + break; } - startPos = pos; - } + else { + auto& nextStackCommand = *nextStackCommandIt; - if(startPos < pData.text.size()) { - a.pushStyle(tStyle); - a.addText(pData.text.c_str() + startPos, pData.text.size() - startPos); - a.pop(); + // Print with previous style + a.pushStyle(tStyle); + a.addText(pData.text.c_str() + tIndex, nextStackCommand.first.fTextByteIndex - tIndex); + a.pop(); + tIndex = nextStackCommand.first.fTextByteIndex; + + // Modify stack with new data + for(const std::shared_ptr& modifier : nextStackCommand.second) { + auto& modStack = modifierStacks[modifier->get_type()]; + if(modStack.empty() || modStack.top() != modifier) + modStack.emplace(modifier); + else + modStack.pop(); + } + + // Modify style with new stacks + tStyle = initialTStyle; + for(auto& [modType, modifier] : modifierStacks) { + if(!modifier.empty()) + modifier.top()->modify_text_style(tStyle); + } + + ++nextStackCommandIt; + } } pData.p = a.Build(); @@ -547,6 +588,7 @@ void RichTextBox::rebuild() { pData.heightOffset = heightOffset; heightOffset += pData.p->getHeight(); } + needsRebuild = false; } } @@ -561,6 +603,16 @@ RichTextBox::TextPosition RichTextBox::insert(TextPosition pos, std::string_view for(char c : textToInsert) { if(c == '\n') { if(newlinesAllowed) { + for(TextStyleRangeModifier& tStyle : tStyleModifiers) { + tStyle.apply_to_start_and_end([&pos](TextPosition& p){ + if(p.fParagraphIndex > pos.fParagraphIndex) + p.fParagraphIndex++; + else if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex) { + 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); @@ -569,15 +621,28 @@ RichTextBox::TextPosition RichTextBox::insert(TextPosition pos, std::string_view } } else if(c == '\t') { + for(TextStyleRangeModifier& tStyle : tStyleModifiers) { + tStyle.apply_to_start_and_end([&pos, tabWidth = tabWidth](TextPosition& p) { + if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex) + p.fTextByteIndex += tabWidth; + }); + } paragraphs[pos.fParagraphIndex].text.insert(pos.fTextByteIndex, std::string(tabWidth, ' ')); pos.fTextByteIndex += tabWidth; } else { + for(TextStyleRangeModifier& tStyle : tStyleModifiers) { + tStyle.apply_to_start_and_end([&pos](TextPosition& p) { + if(p.fParagraphIndex == pos.fParagraphIndex && p.fTextByteIndex >= pos.fTextByteIndex) + p.fTextByteIndex++; + }); + } paragraphs[pos.fParagraphIndex].text.insert(paragraphs[pos.fParagraphIndex].text.begin() + pos.fTextByteIndex, c); pos.fTextByteIndex++; } } + fit_text_style_ranges_in_text(); needsRebuild = true; return pos; } @@ -601,6 +666,25 @@ RichTextBox::TextPosition RichTextBox::remove(TextPosition p1, TextPosition p2) 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(TextStyleRangeModifier& tStyle : tStyleModifiers) { + tStyle.apply_to_start_and_end([&start, &end](TextPosition& p) { + 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); + } + }); + } + + fit_text_style_ranges_in_text(); needsRebuild = true; return start; } diff --git a/src/RichTextBox/RichTextBox.hpp b/src/RichTextBox/RichTextBox.hpp index d68ef2a..2248b82 100644 --- a/src/RichTextBox/RichTextBox.hpp +++ b/src/RichTextBox/RichTextBox.hpp @@ -3,16 +3,13 @@ #include #include #include -#include +#include #include "../SharedTypes.hpp" #include "TextStyleModifier.hpp" #include "cereal/archives/portable_binary.hpp" class RichTextBox { public: - typedef std::unordered_map> TextModAtPosContainer; - typedef std::map TextModContainer; - RichTextBox(); enum class Movement { @@ -29,13 +26,23 @@ class RichTextBox { 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::max(); size_t fParagraphIndex = std::numeric_limits::max(); + size_t fTextByteIndex = std::numeric_limits::max(); template void serialize(Archive& a) { - a((uint32_t)fTextByteIndex, (uint32_t)fParagraphIndex); + 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; + }; + + struct TextStyleRangeModifier { + TextPosition start; + TextPosition end; + std::shared_ptr modifier; + void apply_to_start_and_end(const std::function& f); + void save(cereal::PortableBinaryOutputArchive& a) const; + void load(cereal::PortableBinaryInputArchive& a); }; struct Cursor { @@ -56,14 +63,15 @@ class RichTextBox { struct RichTextData { struct Paragraph { std::string text; - TextModContainer tStyles; - void save(cereal::PortableBinaryOutputArchive& a) const; - void load(cereal::PortableBinaryInputArchive& a); + template void serialize(Archive& a) { + a(text); + } }; template void serialize(Archive& a) { - a(paragraphs); + a(paragraphs, tStyleModifiers); } std::vector paragraphs; + std::vector tStyleModifiers; }; TextPosition move(Movement movement, TextPosition pos, std::optional* previousX = nullptr, bool flipDependingOnTextDirection = false); @@ -88,7 +96,7 @@ class RichTextBox { SELECT_ALL }; - void set_initial_text_style_modifier(const std::shared_ptr& modifier); + void set_initial_text_style(const skia::textlayout::TextStyle& tStyle); void set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr& modifier); RichTextData get_rich_text_data(); @@ -115,6 +123,8 @@ class RichTextBox { void rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function f); + void fit_text_style_ranges_in_text(); + SkRect get_cursor_rect(TextPosition pos); TextPosition get_text_pos_closest_to_point(Vector2f point); @@ -132,8 +142,9 @@ class RichTextBox { std::unique_ptr p; float heightOffset; skia::textlayout::ParagraphStyle pStyle; - TextModContainer tStyles; }; + skia::textlayout::TextStyle initialTStyle; std::vector paragraphs; + std::vector tStyleModifiers; };