Can now add styles to textbox (stacking styles still has issues)

This commit is contained in:
Yousef Khadadeh 2025-11-04 17:31:23 +03:00
parent b43fedaaac
commit bb388efffc
5 changed files with 191 additions and 78 deletions

View file

@ -54,6 +54,10 @@ void DrawTextBox::update_from_delayed_ptr(const std::shared_ptr<DrawComponent>&
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) {

View file

@ -4,6 +4,7 @@
#include "../DrawData.hpp"
#include "Helpers/SCollision.hpp"
#include <cereal/types/vector.hpp>
#include <include/core/SkFontStyle.h>
#include <memory>
#include "EditTool.hpp"
@ -16,7 +17,21 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr<DrawComponent>& 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<WeightTextStyleModifier>();
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<SlantTextStyleModifier>();
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<ColorTextStyleModifier>();
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))

View file

@ -2,6 +2,7 @@
#include "Element.hpp"
#include "../../RichTextBox/RichTextBox.hpp"
#include <limits>
#include <modules/skparagraph/include/TextStyle.h>
namespace GUIStuff {
@ -66,15 +67,13 @@ template <typename T> 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<FontFamiliesTextStyleModifier>();
fontFamiliesMod->families.emplace_back("Roboto");
textbox->set_initial_text_style_modifier(fontFamiliesMod);
auto fontSizeMod = std::make_shared<SizeTextStyleModifier>();
fontSizeMod->size = io.fontSize;
textbox->set_initial_text_style_modifier(fontSizeMod);
auto fontColorMod = std::make_shared<ColorTextStyleModifier>();
fontColorMod->color = convert_vec4<Vector4f>(io.theme->frontColor1);
textbox->set_initial_text_style_modifier(fontColorMod);
skia::textlayout::TextStyle tStyle;
std::vector<SkString> 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);

View file

@ -12,40 +12,31 @@
#include <src/base/SkUTF.h>
#include <cereal/types/string.hpp>
void RichTextBox::RichTextData::Paragraph::save(cereal::PortableBinaryOutputArchive& a) const {
a(text);
a(static_cast<uint32_t>(tStyles.size()));
for(auto& [pos, styles] : tStyles) {
a(pos, static_cast<uint16_t>(styles.size()));
for(auto& [styleType, style] : styles)
a(static_cast<uint16_t>(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<TextStyleModifier> modifier = TextStyleModifier::allocate_modifier(static_cast<TextStyleModifier::ModifierType>(styleType));
a(*modifier);
mods.emplace(static_cast<TextStyleModifier::ModifierType>(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<void(TextPosition&)>& 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<TextStyleModifier>& modifier) {
std::shared_ptr<TextStyleModifier>& 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<TextStyleModifier>& 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<float>* 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<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};
pos = TextPosition{pos.fParagraphIndex + 1, 0};
}
else {
skia::textlayout::LineMetrics nextLineMetrics;
@ -511,35 +516,71 @@ void RichTextBox::set_font_collection(const sk_sp<skia::textlayout::FontCollecti
}
}
void RichTextBox::fit_text_style_ranges_in_text() {
std::erase_if(tStyleModifiers, [&](TextStyleRangeModifier& tStyleMod) {
tStyleMod.start = move(Movement::NOWHERE, tStyleMod.start);
tStyleMod.end = move(Movement::NOWHERE, tStyleMod.end);
return tStyleMod.start == tStyleMod.end;
});
}
void RichTextBox::rebuild() {
if(needsRebuild) {
float heightOffset = 0.0f;
skia::textlayout::TextStyle tStyle;
std::map<TextPosition, std::vector<std::shared_ptr<TextStyleModifier>>> 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<TextStyleModifier::ModifierType, std::stack<std::shared_ptr<TextStyleModifier>>> 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<TextStyleModifier>& 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;
}

View file

@ -3,16 +3,13 @@
#include <include/core/SkCanvas.h>
#include <modules/skparagraph/include/FontCollection.h>
#include <modules/skparagraph/include/Paragraph.h>
#include <map>
#include <modules/skparagraph/include/TextStyle.h>
#include "../SharedTypes.hpp"
#include "TextStyleModifier.hpp"
#include "cereal/archives/portable_binary.hpp"
class RichTextBox {
public:
typedef std::unordered_map<TextStyleModifier::ModifierType, std::shared_ptr<TextStyleModifier>> TextModAtPosContainer;
typedef std::map<size_t, TextModAtPosContainer> 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<uint32_t>::max();
size_t fParagraphIndex = std::numeric_limits<uint32_t>::max();
size_t fTextByteIndex = std::numeric_limits<uint32_t>::max();
template <typename Archive> 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<TextStyleModifier> modifier;
void apply_to_start_and_end(const std::function<void(TextPosition&)>& 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<typename Archive> void serialize(Archive& a) {
a(text);
}
};
template<typename Archive> void serialize(Archive& a) {
a(paragraphs);
a(paragraphs, tStyleModifiers);
}
std::vector<Paragraph> paragraphs;
std::vector<TextStyleRangeModifier> tStyleModifiers;
};
TextPosition move(Movement movement, TextPosition pos, std::optional<float>* previousX = nullptr, bool flipDependingOnTextDirection = false);
@ -88,7 +96,7 @@ class RichTextBox {
SELECT_ALL
};
void set_initial_text_style_modifier(const std::shared_ptr<TextStyleModifier>& 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<TextStyleModifier>& modifier);
RichTextData get_rich_text_data();
@ -115,6 +123,8 @@ class RichTextBox {
void rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function<void(const SkRect& r)> 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<skia::textlayout::Paragraph> p;
float heightOffset;
skia::textlayout::ParagraphStyle pStyle;
TextModContainer tStyles;
};
skia::textlayout::TextStyle initialTStyle;
std::vector<ParagraphData> paragraphs;
std::vector<TextStyleRangeModifier> tStyleModifiers;
};