diff --git a/src/DrawComponents/DrawTextBox.cpp b/src/DrawComponents/DrawTextBox.cpp index 4f6b586..6f71369 100644 --- a/src/DrawComponents/DrawTextBox.cpp +++ b/src/DrawComponents/DrawTextBox.cpp @@ -58,6 +58,16 @@ void DrawTextBox::init_text_box(DrawingProgram& drawP) { tStyle.setFontFamilies({SkString{"Roboto"}}); tStyle.setFontSize(16.0f); textBox->set_initial_text_style(tStyle); + + auto boldMod = std::make_shared(); + boldMod->set_weight(SkFontStyle::kNormal_Weight); + textBox->set_initial_text_style_modifier(boldMod); + auto italicMod = std::make_shared(); + italicMod->set_slant(SkFontStyle::Slant::kUpright_Slant); + textBox->set_initial_text_style_modifier(italicMod); + auto colorMod = std::make_shared(); + colorMod->color = {1.0f, 1.0f, 1.0f, 1.0f}; + textBox->set_initial_text_style_modifier(colorMod); } void DrawTextBox::draw(SkCanvas* canvas, const DrawData& drawData) { diff --git a/src/RichTextBox/RichTextBox.cpp b/src/RichTextBox/RichTextBox.cpp index 9f9af0b..ba57407 100644 --- a/src/RichTextBox/RichTextBox.cpp +++ b/src/RichTextBox/RichTextBox.cpp @@ -12,21 +12,33 @@ #include #include -void RichTextBox::TextStyleRangeModifier::save(cereal::PortableBinaryOutputArchive& a) const { - a(start, end, modifier->get_type(), *modifier); +void RichTextBox::RichTextData::save(cereal::PortableBinaryOutputArchive& a) const { + a(paragraphs); + a(static_cast(tStyleMods.size())); + for(auto& [pos, modsInPos] : tStyleMods) { + a(pos, static_cast(modsInPos.size())); + for(auto& [modType, modifier] : modsInPos) + a(modType, *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); +void RichTextBox::RichTextData::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 RichTextBox::TextPosition::operator<(const RichTextBox::TextPosition& o) const { @@ -253,13 +265,13 @@ RichTextBox::RichTextData RichTextBox::get_rich_text_data() { toRet.paragraphs.emplace_back(); toRet.paragraphs.back().text = p.text; } - toRet.tStyleModifiers = tStyleModifiers; + toRet.tStyleMods = tStyleMods; return toRet; } void RichTextBox::clear_text() { paragraphs.clear(); - tStyleModifiers.clear(); + tStyleMods.clear(); paragraphs.emplace_back(); } @@ -276,8 +288,7 @@ void RichTextBox::set_rich_text_data(const RichTextData& richText) { } if(richText.paragraphs.empty()) paragraphs.emplace_back(); - tStyleModifiers = richText.tStyleModifiers; - fit_text_style_ranges_in_text(); + tStyleMods = richText.tStyleMods; needsRebuild = true; } @@ -316,46 +327,73 @@ void RichTextBox::set_initial_text_style(const skia::textlayout::TextStyle& tSty } } +void RichTextBox::set_initial_text_style_modifier(const std::shared_ptr& 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; +} + +std::shared_ptr RichTextBox::get_last_text_style_mod_before_pos(TextPosition pos, TextStyleModifier::ModifierType modType) { + std::shared_ptr lastModOfThisTypeBeforeEnd; + 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 RichTextBox::erase_if_over_all_styles_until_pos(TextPosition pos, const std::function&)>& func) { + for(auto& [tPos, tStyleModsInPos] : tStyleMods) { + if(tPos <= pos) { + std::erase_if(tStyleModsInPos, [&](auto& item) { + return func(tPos, item.second); + }); + } + else + break; + } +} + void RichTextBox::set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr& modifier) { 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; - std::erase_if(tStyleModifiers, [&toPlace](TextStyleRangeModifier& tStyleMod) { - if(tStyleMod.modifier->equivalent(*toPlace.modifier)) { - // The equivalent modifiers collide, so merge them into one modifier - if((tStyleMod.start >= toPlace.start && tStyleMod.start <= toPlace.end) || (tStyleMod.end >= toPlace.start && tStyleMod.end <= toPlace.end) || - (toPlace.start >= tStyleMod.start && toPlace.start <= tStyleMod.end) || (toPlace.end >= tStyleMod.start && toPlace.end <= tStyleMod.end)) { - toPlace.start = std::min(toPlace.start, tStyleMod.start); - toPlace.end = std::max(toPlace.end, tStyleMod.end); - return true; - } - } - return false; + 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& modToCheck) { + return p >= start && modifier->get_type() == modToCheck->get_type(); }); - std::erase_if(tStyleModifiers, [&toPlace](TextStyleRangeModifier& tStyleMod) { - if(tStyleMod.modifier->get_type() == toPlace.modifier->get_type()) { - bool tStyleModStartInToPlace = (tStyleMod.start >= toPlace.start && tStyleMod.start <= toPlace.end); - bool tStyleModEndInToPlace = (tStyleMod.end >= toPlace.start && tStyleMod.end <= toPlace.end); - if(tStyleModStartInToPlace && tStyleModEndInToPlace) - return true; - else if(tStyleModStartInToPlace) - tStyleMod.start = toPlace.end; - else if(tStyleModEndInToPlace) - tStyleMod.end = toPlace.start; - } - return false; - }); - tStyleModifiers.emplace_back(toPlace); - fit_text_style_ranges_in_text(); + insert_style_at_pos(start, modifier); + insert_style_at_pos(end, lastModOfThisTypeBeforeEnd); needsRebuild = true; } } +void RichTextBox::insert_style_at_pos(TextPosition pos, const std::shared_ptr& modifier) { + size_t indexToPlaceAt = 0; + for(auto& [tPos, tStyleModsInPos] : tStyleMods) { + if(tPos > pos) + break; + else if(tPos == pos) + tStyleModsInPos[modifier->get_type()] = modifier; + indexToPlaceAt++; + } + auto it = tStyleMods.insert(tStyleMods.begin() + indexToPlaceAt, PositionedTextStyleMod{pos}); + it->mods[modifier->get_type()] = modifier; +} + RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, std::optional* previousX, bool flipDependingOnTextDirection) { if(pos.fParagraphIndex >= paragraphs.size()) { pos.fParagraphIndex = paragraphs.size() - 1; @@ -550,26 +588,11 @@ void RichTextBox::set_font_collection(const sk_sp>> modifierStackCommands; - 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; // Not really a stack, but we use the last element as the modifier + auto nextTStyleModIt = tStyleMods.begin(); skia::textlayout::TextStyle tStyle = initialTStyle; @@ -582,39 +605,26 @@ void RichTextBox::rebuild() { skia::textlayout::ParagraphBuilderImpl a(pData.pStyle, fontCollection, SkUnicodes::ICU::Make()); for(;;) { - if(nextStackCommandIt == modifierStackCommands.end() || nextStackCommandIt->first.fParagraphIndex != pIndex) { + 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& nextStackCommand = *nextStackCommandIt; + auto& nextTStyleMod = *nextTStyleModIt; // Print with previous style a.pushStyle(tStyle); - a.addText(pData.text.c_str() + tIndex, nextStackCommand.first.fTextByteIndex - tIndex); + a.addText(pData.text.c_str() + tIndex, nextTStyleMod.pos.fTextByteIndex - tIndex); a.pop(); - tIndex = nextStackCommand.first.fTextByteIndex; + tIndex = nextTStyleMod.pos.fTextByteIndex; - // Modify stack with new data - for(const std::shared_ptr& modifier : nextStackCommand.second) { - auto& modStack = modifierStacks[modifier->get_type()]; - auto foundInStackIt = std::find(modStack.begin(), modStack.end(), modifier); - if(foundInStackIt == modStack.end()) - modStack.emplace_back(modifier); - else - modStack.erase(foundInStackIt); - } + // Modify tStyle with new data + for(auto& [modType, modifier] : nextTStyleMod.mods) + modifier->modify_text_style(tStyle); - // Modify style with new stacks - tStyle = initialTStyle; - for(auto& [modType, modifier] : modifierStacks) { - if(!modifier.empty()) - modifier.back()->modify_text_style(tStyle); - } - - ++nextStackCommandIt; + ++nextTStyleModIt; } } @@ -638,15 +648,13 @@ 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++; - } - }); + for(auto& [p, tStylesInPos] : tStyleMods) { + 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); @@ -656,28 +664,23 @@ 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; - }); + for(auto& [p, tStylesInPos] : tStyleMods) { + 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++; - }); + for(auto& [p, tStylesInPos] : tStyleMods) { + 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; } @@ -702,24 +705,32 @@ RichTextBox::TextPosition RichTextBox::remove(TextPosition p1, TextPosition p2) 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); + std::optional prevTextStyleModIndex; + + for(int32_t i = 0; i < static_cast(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(prevTextStyleModIndex.has_value() && tStyleMods[prevTextStyleModIndex.value()].pos == p) { + auto& tStyleModsToMergeWith = tStyleMods[prevTextStyleModIndex.value()].mods; + for(auto& [modType, tStyle] : tStyleMod.mods) + tStyleModsToMergeWith[modType] = tStyle; + tStyleMods.erase(tStyleMods.begin() + i); + i--; + } } - fit_text_style_ranges_in_text(); needsRebuild = true; return start; } diff --git a/src/RichTextBox/RichTextBox.hpp b/src/RichTextBox/RichTextBox.hpp index 5963e17..ab6c06e 100644 --- a/src/RichTextBox/RichTextBox.hpp +++ b/src/RichTextBox/RichTextBox.hpp @@ -10,6 +10,7 @@ class RichTextBox { public: + RichTextBox(); enum class Movement { @@ -38,15 +39,13 @@ class RichTextBox { bool operator<=(const RichTextBox::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 PositionedTextStyleMod { + TextPosition pos; + std::unordered_map> mods; }; + typedef std::vector TextStyleModContainer; + struct Cursor { TextPosition pos = {0, 0}; TextPosition selectionBeginPos = {0, 0}; @@ -69,11 +68,10 @@ class RichTextBox { a(text); } }; - template void serialize(Archive& a) { - a(paragraphs, tStyleModifiers); - } + void save(cereal::PortableBinaryOutputArchive& a) const; + void load(cereal::PortableBinaryInputArchive& a); + TextStyleModContainer tStyleMods; std::vector paragraphs; - std::vector tStyleModifiers; }; TextPosition move(Movement movement, TextPosition pos, std::optional* previousX = nullptr, bool flipDependingOnTextDirection = false); @@ -99,6 +97,7 @@ class RichTextBox { }; void set_initial_text_style(const skia::textlayout::TextStyle& tStyle); + void set_initial_text_style_modifier(const std::shared_ptr& modifier); void set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr& modifier); RichTextData get_rich_text_data(); @@ -125,11 +124,12 @@ 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); + std::shared_ptr 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&)>& func); + void insert_style_at_pos(TextPosition pos, const std::shared_ptr& modifier); void rebuild(); @@ -147,6 +147,6 @@ class RichTextBox { }; skia::textlayout::TextStyle initialTStyle; + TextStyleModContainer tStyleMods; std::vector paragraphs; - std::vector tStyleModifiers; };