From def4ccb984e89bb199bf7019e9253f9d5e0620e4 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Mon, 10 Nov 2025 16:49:02 +0300 Subject: [PATCH] Added UI to change paragraph style --- data/icons/RemixIcon/align-center.svg | 1 + data/icons/RemixIcon/align-justify.svg | 1 + data/icons/RemixIcon/align-left.svg | 1 + data/icons/RemixIcon/align-right.svg | 1 + data/icons/RemixIcon/text-direction-l.svg | 1 + data/icons/RemixIcon/text-direction-r.svg | 1 + src/DrawingProgram/TextBoxEditTool.cpp | 43 ++++++++++---- src/DrawingProgram/TextBoxEditTool.hpp | 3 +- src/RichText/ParagraphStyleData.cpp | 8 +++ src/RichText/ParagraphStyleData.hpp | 14 ++++- src/RichText/TextBox.cpp | 72 +++++++++++++++-------- src/RichText/TextBox.hpp | 14 ++++- 12 files changed, 117 insertions(+), 43 deletions(-) create mode 100644 data/icons/RemixIcon/align-center.svg create mode 100644 data/icons/RemixIcon/align-justify.svg create mode 100644 data/icons/RemixIcon/align-left.svg create mode 100644 data/icons/RemixIcon/align-right.svg create mode 100644 data/icons/RemixIcon/text-direction-l.svg create mode 100644 data/icons/RemixIcon/text-direction-r.svg diff --git a/data/icons/RemixIcon/align-center.svg b/data/icons/RemixIcon/align-center.svg new file mode 100644 index 0000000..45d0d29 --- /dev/null +++ b/data/icons/RemixIcon/align-center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/RemixIcon/align-justify.svg b/data/icons/RemixIcon/align-justify.svg new file mode 100644 index 0000000..d5e3f05 --- /dev/null +++ b/data/icons/RemixIcon/align-justify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/RemixIcon/align-left.svg b/data/icons/RemixIcon/align-left.svg new file mode 100644 index 0000000..14ab85e --- /dev/null +++ b/data/icons/RemixIcon/align-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/RemixIcon/align-right.svg b/data/icons/RemixIcon/align-right.svg new file mode 100644 index 0000000..ef8aaa8 --- /dev/null +++ b/data/icons/RemixIcon/align-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/RemixIcon/text-direction-l.svg b/data/icons/RemixIcon/text-direction-l.svg new file mode 100644 index 0000000..c1d8cbf --- /dev/null +++ b/data/icons/RemixIcon/text-direction-l.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/RemixIcon/text-direction-r.svg b/data/icons/RemixIcon/text-direction-r.svg new file mode 100644 index 0000000..a5a734d --- /dev/null +++ b/data/icons/RemixIcon/text-direction-r.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/DrawingProgram/TextBoxEditTool.cpp b/src/DrawingProgram/TextBoxEditTool.cpp index 51c97f9..8230d1b 100644 --- a/src/DrawingProgram/TextBoxEditTool.cpp +++ b/src/DrawingProgram/TextBoxEditTool.cpp @@ -68,6 +68,21 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr& comp) { } }); + t.gui.left_to_right_line_centered_layout([&]() { + if(t.gui.svg_icon_button("Align left button", "data/icons/RemixIcon/align-left.svg", currentPStyle.textAlignment == skia::textlayout::TextAlign::kLeft)) + a->textBox->set_text_alignment_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextAlign::kLeft); + if(t.gui.svg_icon_button("Align center button", "data/icons/RemixIcon/align-center.svg", currentPStyle.textAlignment == skia::textlayout::TextAlign::kCenter)) + a->textBox->set_text_alignment_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextAlign::kCenter); + if(t.gui.svg_icon_button("Align right button", "data/icons/RemixIcon/align-right.svg", currentPStyle.textAlignment == skia::textlayout::TextAlign::kRight)) + a->textBox->set_text_alignment_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextAlign::kRight); + if(t.gui.svg_icon_button("Align justify button", "data/icons/RemixIcon/align-justify.svg", currentPStyle.textAlignment == skia::textlayout::TextAlign::kJustify)) + a->textBox->set_text_alignment_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextAlign::kJustify); + if(t.gui.svg_icon_button("Text direction left", "data/icons/RemixIcon/text-direction-l.svg", currentPStyle.textDirection == skia::textlayout::TextDirection::kLtr)) + a->textBox->set_text_direction_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextDirection::kLtr); + if(t.gui.svg_icon_button("Text direction right", "data/icons/RemixIcon/text-direction-r.svg", currentPStyle.textDirection == skia::textlayout::TextDirection::kRtl)) + a->textBox->set_text_direction_between(a->cursor->selectionBeginPos.fParagraphIndex, a->cursor->selectionEndPos.fParagraphIndex, skia::textlayout::TextDirection::kRtl); + }); + if(t.gui.slider_scalar_field("Font Size Slider", "Font Size", &newFontSize, 3, 100)) { currentMods[TextStyleModifier::ModifierType::SIZE] = std::make_shared(newFontSize); a->textBox->set_text_style_modifier_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos, currentMods[TextStyleModifier::ModifierType::SIZE]); @@ -90,17 +105,8 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr& comp) { t.gui.pop_id(); - if(a->textBox->inputChangedTextBox) { - set_mods_at_selection(a); - newFontName = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::FONT_FAMILIES])->get_families().back().c_str(); - newFontSize = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::SIZE])->get_size(); - newTextColor = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::COLOR])->get_color(); - newIsBold = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::WEIGHT])->get_weight() == SkFontStyle::Weight::kBold_Weight; - newIsItalic = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::SLANT])->get_slant() == SkFontStyle::Slant::kItalic_Slant; - newIsUnderlined = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kUnderline; - newIsLinethrough = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kLineThrough; - newIsOverline = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kOverline; - } + if(a->textBox->inputChangedTextBox) + set_styles_at_selection(a); // NOTE: There should be a system that periodically sends updates even if no changes are made, since unreliable channels can drop update data bool oldInputChangedTextBox = a->textBox->inputChangedTextBox; @@ -119,13 +125,24 @@ uint8_t TextBoxEditTool::get_new_decoration_value() { return toRet; } -void TextBoxEditTool::set_mods_at_selection(const std::shared_ptr& a) { +void TextBoxEditTool::set_styles_at_selection(const std::shared_ptr& a) { TextPosition start = std::min(a->cursor->selectionBeginPos, a->cursor->selectionEndPos); TextPosition end = std::max(a->cursor->selectionBeginPos, a->cursor->selectionEndPos); if(start == end) currentMods = a->textBox->get_mods_used_at_pos(a->textBox->move(TextBox::Movement::LEFT, start)); else currentMods = a->textBox->get_mods_used_at_pos(start); + + newFontName = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::FONT_FAMILIES])->get_families().back().c_str(); + newFontSize = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::SIZE])->get_size(); + newTextColor = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::COLOR])->get_color(); + newIsBold = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::WEIGHT])->get_weight() == SkFontStyle::Weight::kBold_Weight; + newIsItalic = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::SLANT])->get_slant() == SkFontStyle::Slant::kItalic_Slant; + newIsUnderlined = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kUnderline; + newIsLinethrough = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kLineThrough; + newIsOverline = std::static_pointer_cast(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kOverline; + + currentPStyle = a->textBox->get_paragraph_style_data_at(start.fParagraphIndex); } void TextBoxEditTool::commit_edit_updates(const std::shared_ptr& comp, std::any& prevData) { @@ -176,7 +193,7 @@ void TextBoxEditTool::edit_start(EditTool& editTool, const std::shared_ptrcommit_update(drawP); a->client_send_update(drawP, false); - set_mods_at_selection(a); + set_styles_at_selection(a); editTool.add_point_handle({&a->d.p1, nullptr, &a->d.p2}); editTool.add_point_handle({&a->d.p2, &a->d.p1, nullptr}); diff --git a/src/DrawingProgram/TextBoxEditTool.hpp b/src/DrawingProgram/TextBoxEditTool.hpp index 37cf2ae..d114692 100644 --- a/src/DrawingProgram/TextBoxEditTool.hpp +++ b/src/DrawingProgram/TextBoxEditTool.hpp @@ -28,9 +28,10 @@ class TextBoxEditTool : public DrawingProgramEditToolBase { bool newIsLinethrough = false; bool newIsOverline = false; std::string newFontName; + ParagraphStyleData currentPStyle; uint8_t get_new_decoration_value(); - void set_mods_at_selection(const std::shared_ptr& a); + void set_styles_at_selection(const std::shared_ptr& a); RichText::TextStyleModifier::ModifierMap currentMods; std::vector sortedFontList; diff --git a/src/RichText/ParagraphStyleData.cpp b/src/RichText/ParagraphStyleData.cpp index e69de29..a3fc29b 100644 --- a/src/RichText/ParagraphStyleData.cpp +++ b/src/RichText/ParagraphStyleData.cpp @@ -0,0 +1,8 @@ +#include "ParagraphStyleData.hpp" + +skia::textlayout::ParagraphStyle ParagraphStyleData::get_paragraph_style() const { + skia::textlayout::ParagraphStyle pStyle; + pStyle.setTextAlign(textAlignment); + pStyle.setTextDirection(textDirection); + return pStyle; +} diff --git a/src/RichText/ParagraphStyleData.hpp b/src/RichText/ParagraphStyleData.hpp index 4e67884..e1c7ecf 100644 --- a/src/RichText/ParagraphStyleData.hpp +++ b/src/RichText/ParagraphStyleData.hpp @@ -1,6 +1,18 @@ #pragma once +#include struct ParagraphStyleData { - + skia::textlayout::TextAlign textAlignment = skia::textlayout::TextAlign::kLeft; + skia::textlayout::TextDirection textDirection = skia::textlayout::TextDirection::kLtr; + template void save(Archive& a) const { + a(static_cast(textAlignment), static_cast(textDirection)); + } + template void load(Archive& a) { + uint8_t alignRead, directionRead; + a(alignRead, directionRead); + textAlignment = static_cast(alignRead); + textDirection = static_cast(directionRead); + } + skia::textlayout::ParagraphStyle get_paragraph_style() const; }; diff --git a/src/RichText/TextBox.cpp b/src/RichText/TextBox.cpp index 6808db5..4e14f76 100644 --- a/src/RichText/TextBox.cpp +++ b/src/RichText/TextBox.cpp @@ -274,6 +274,7 @@ TextData TextBox::get_rich_text_data() { for(auto& p : paragraphs) { toRet.paragraphs.emplace_back(); toRet.paragraphs.back().text = p.text; + toRet.paragraphs.back().pStyleData = p.pStyleData; } toRet.tStyleMods = tStyleMods; return toRet; @@ -295,6 +296,7 @@ void TextBox::set_rich_text_data(const TextData& richText) { for(auto& p : richText.paragraphs) { paragraphs.emplace_back(); paragraphs.back().text = p.text; + paragraphs.back().pStyleData = p.pStyleData; } if(richText.paragraphs.empty()) paragraphs.emplace_back(); @@ -308,15 +310,11 @@ std::string TextBox::get_string() { } std::string TextBox::get_text_between(TextPosition p1, TextPosition p2) { - p1 = move(Movement::NOWHERE, p1); - p2 = move(Movement::NOWHERE, p2); + auto [start, end] = get_start_end_text_pos(p1, p2); - if(p1 == p2) + if(start == end) return ""; - TextPosition start = std::min(p1, p2); - TextPosition end = std::max(p1, p2); - std::string toRet; for(size_t pIndex = start.fParagraphIndex; pIndex <= end.fParagraphIndex; pIndex++) { @@ -395,11 +393,8 @@ void TextBox::erase_if_over_all_styles_until_pos(TextPosition pos, const std::fu } void TextBox::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) { - TextPosition start = std::min(p1, p2); - TextPosition end = std::max(p1, p2); + auto [start, end] = get_start_end_text_pos(p1, p2); + if(start != end) { 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) { @@ -413,6 +408,26 @@ void TextBox::set_text_style_modifier_between(TextPosition p1, TextPosition p2, } } +void TextBox::set_text_alignment_between(size_t paragraphIndex1, size_t paragraphIndex2, skia::textlayout::TextAlign newAlignment) { + auto [start, end] = get_start_end_paragraph_pos(paragraphIndex1, paragraphIndex2); + for(size_t i = start; i <= end; i++) + paragraphs[i].pStyleData.textAlignment = newAlignment; + inputChangedTextBox = true; + needsRebuild = true; +} + +void TextBox::set_text_direction_between(size_t paragraphIndex1, size_t paragraphIndex2, skia::textlayout::TextDirection newDirection) { + auto [start, end] = get_start_end_paragraph_pos(paragraphIndex1, paragraphIndex2); + for(size_t i = start; i <= end; i++) + paragraphs[i].pStyleData.textDirection = newDirection; + inputChangedTextBox = true; + needsRebuild = true; +} + +ParagraphStyleData TextBox::get_paragraph_style_data_at(size_t paragraphIndex) { + return paragraphs[std::min(paragraphIndex, paragraphs.size() - 1)].pStyleData; +} + void TextBox::insert_style_at_pos(TextPosition pos, const std::shared_ptr& modifier) { size_t indexToPlaceAt = 0; bool finishedInsertingStyle = false; @@ -445,7 +460,7 @@ TextPosition TextBox::move(Movement movement, TextPosition pos, std::optionalpos.fParagraphIndex != pIndex) { @@ -721,6 +735,7 @@ TextPosition TextBox::insert(TextPosition pos, std::string_view textToInsert, co } } paragraphs.insert(paragraphs.begin() + pos.fParagraphIndex + 1, ParagraphData{}); + paragraphs[pos.fParagraphIndex + 1].pStyleData = paragraphs[pos.fParagraphIndex].pStyleData; 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); pos.fParagraphIndex++; @@ -759,11 +774,8 @@ TextPosition TextBox::insert(TextPosition pos, std::string_view textToInsert, co } TextPosition TextBox::remove(TextPosition p1, TextPosition p2) { - p1 = move(Movement::NOWHERE, p1); - p2 = move(Movement::NOWHERE, p2); + auto [start, end] = get_start_end_text_pos(p1, p2); - TextPosition start = std::min(p1, p2); - TextPosition end = std::max(p1, p2); if(start == end || start.fParagraphIndex == paragraphs.size()) return start; if(start.fParagraphIndex == end.fParagraphIndex) { @@ -817,7 +829,7 @@ SkRect TextBox::get_cursor_rect(TextPosition pos) { const ParagraphData& pData = paragraphs[pos.fParagraphIndex]; if(pData.text.empty()) { - bool isAlignLeft = (pData.pStyle.getTextAlign() == skia::textlayout::TextAlign::kLeft) || (pData.pStyle.getTextAlign() == skia::textlayout::TextAlign::kJustify && pData.pStyle.getTextDirection() == skia::textlayout::TextDirection::kLtr); + bool isAlignLeft = (pData.pStyleData.textAlignment == skia::textlayout::TextAlign::kLeft) || (pData.pStyleData.textAlignment == skia::textlayout::TextAlign::kJustify && pData.pStyleData.textDirection == skia::textlayout::TextDirection::kLtr); topPoint = {isAlignLeft ? 0.0f : pData.p->getMaxWidth(), pData.heightOffset}; height = pData.p->getHeight(); } @@ -856,14 +868,10 @@ SkRect TextBox::get_cursor_rect(TextPosition pos) { } void TextBox::rects_between_text_positions_func(TextPosition p1, TextPosition p2, std::function f) { - p1 = move(Movement::NOWHERE, p1); - p2 = move(Movement::NOWHERE, p2); + auto [start, end] = get_start_end_text_pos(p1, p2); constexpr float SELECTION_RECT_EXTRA_AREA = 1.0f; - TextPosition start = std::min(p1, p2); - TextPosition end = std::max(p1, p2); - 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(); @@ -884,6 +892,18 @@ void TextBox::rects_between_text_positions_func(TextPosition p1, TextPosition p2 } } +std::pair TextBox::get_start_end_text_pos(TextPosition p1, TextPosition p2) { + p1 = move(Movement::NOWHERE, p1); + p2 = move(Movement::NOWHERE, p2); + return {std::min(p1, p2), std::max(p1, p2)}; +} + +std::pair TextBox::get_start_end_paragraph_pos(size_t p1, size_t p2) { + p1 = std::min(p1, paragraphs.size() - 1); + p2 = std::min(p2, paragraphs.size() - 1); + return {std::min(p1, p2), std::max(p1, p2)}; +} + void TextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) { rebuild(); diff --git a/src/RichText/TextBox.hpp b/src/RichText/TextBox.hpp index 1ae3a12..70cd1fb 100644 --- a/src/RichText/TextBox.hpp +++ b/src/RichText/TextBox.hpp @@ -8,6 +8,7 @@ #include "../SharedTypes.hpp" #include "TextStyleModifier.hpp" #include "cereal/archives/portable_binary.hpp" +#include "ParagraphStyleData.hpp" namespace RichText { @@ -35,8 +36,9 @@ typedef std::vector TextStyleModContainer; struct TextData { struct Paragraph { std::string text; + ParagraphStyleData pStyleData; template void serialize(Archive& a) { - a(text); + a(text, pStyleData); } }; void save(cereal::PortableBinaryOutputArchive& a) const; @@ -105,6 +107,11 @@ class TextBox { 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); + + void set_text_alignment_between(size_t paragraphIndex1, size_t paragraphIndex2, skia::textlayout::TextAlign newAlignment); + void set_text_direction_between(size_t paragraphIndex1, size_t paragraphIndex2, skia::textlayout::TextDirection newDirection); + ParagraphStyleData get_paragraph_style_data_at(size_t paragraphIndex); + TextStyleModifier::ModifierMap get_mods_used_at_pos(TextPosition p); TextData get_rich_text_data(); @@ -122,6 +129,9 @@ class TextBox { void process_text_input(Cursor& cur, const std::string& in, const std::optional& inputModMap = std::nullopt); private: + std::pair get_start_end_text_pos(TextPosition p1, TextPosition p2); + std::pair get_start_end_paragraph_pos(size_t p1, size_t p2); + static size_t count_grapheme(const std::string& text); static size_t next_grapheme(const std::string& text, size_t textBytePos); static size_t prev_grapheme(const std::string& text, size_t textBytePos); @@ -152,7 +162,7 @@ class TextBox { std::string text; std::unique_ptr p; float heightOffset; - skia::textlayout::ParagraphStyle pStyle; + ParagraphStyleData pStyleData; }; skia::textlayout::TextStyle initialTStyle;