Added UI to change paragraph style

This commit is contained in:
Yousef Khadadeh 2025-11-10 16:49:02 +03:00
parent 435d9346a9
commit def4ccb984
12 changed files with 117 additions and 43 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z"></path></svg>

After

Width:  |  Height:  |  Size: 167 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z"></path></svg>

After

Width:  |  Height:  |  Size: 167 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4H21V6H3V4ZM3 19H17V21H3V19ZM3 14H21V16H3V14ZM3 9H17V11H3V9Z"></path></svg>

After

Width:  |  Height:  |  Size: 167 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z"></path></svg>

After

Width:  |  Height:  |  Size: 167 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 5V15H9V11C6.79086 11 5 9.20914 5 7C5 4.79086 6.79086 3 9 3H17V5H15V15H13V5H11ZM9 5C7.89543 5 7 5.89543 7 7C7 8.10457 7.89543 9 9 9V5ZM17 17V14.5L21 18L17 21.5V19H5V17H17Z"></path></svg>

After

Width:  |  Height:  |  Size: 278 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 5V15H9V11C6.79086 11 5 9.20914 5 7C5 4.79086 6.79086 3 9 3H17V5H15V15H13V5H11ZM9 5C7.89543 5 7 5.89543 7 7C7 8.10457 7.89543 9 9 9V5ZM7 17H19V19H7V21.5L3 18L7 14.5V17Z"></path></svg>

After

Width:  |  Height:  |  Size: 275 B

View file

@ -68,6 +68,21 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr<DrawComponent>& 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<uint32_t>("Font Size Slider", "Font Size", &newFontSize, 3, 100)) {
currentMods[TextStyleModifier::ModifierType::SIZE] = std::make_shared<SizeTextStyleModifier>(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<DrawComponent>& comp) {
t.gui.pop_id();
if(a->textBox->inputChangedTextBox) {
set_mods_at_selection(a);
newFontName = std::static_pointer_cast<FontFamiliesTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::FONT_FAMILIES])->get_families().back().c_str();
newFontSize = std::static_pointer_cast<SizeTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::SIZE])->get_size();
newTextColor = std::static_pointer_cast<ColorTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::COLOR])->get_color();
newIsBold = std::static_pointer_cast<WeightTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::WEIGHT])->get_weight() == SkFontStyle::Weight::kBold_Weight;
newIsItalic = std::static_pointer_cast<SlantTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::SLANT])->get_slant() == SkFontStyle::Slant::kItalic_Slant;
newIsUnderlined = std::static_pointer_cast<DecorationTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kUnderline;
newIsLinethrough = std::static_pointer_cast<DecorationTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kLineThrough;
newIsOverline = std::static_pointer_cast<DecorationTextStyleModifier>(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<DrawTextBox>& a) {
void TextBoxEditTool::set_styles_at_selection(const std::shared_ptr<DrawTextBox>& 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<FontFamiliesTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::FONT_FAMILIES])->get_families().back().c_str();
newFontSize = std::static_pointer_cast<SizeTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::SIZE])->get_size();
newTextColor = std::static_pointer_cast<ColorTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::COLOR])->get_color();
newIsBold = std::static_pointer_cast<WeightTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::WEIGHT])->get_weight() == SkFontStyle::Weight::kBold_Weight;
newIsItalic = std::static_pointer_cast<SlantTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::SLANT])->get_slant() == SkFontStyle::Slant::kItalic_Slant;
newIsUnderlined = std::static_pointer_cast<DecorationTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kUnderline;
newIsLinethrough = std::static_pointer_cast<DecorationTextStyleModifier>(currentMods[TextStyleModifier::ModifierType::DECORATION])->get_decoration_value() & skia::textlayout::TextDecoration::kLineThrough;
newIsOverline = std::static_pointer_cast<DecorationTextStyleModifier>(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<DrawComponent>& comp, std::any& prevData) {
@ -176,7 +193,7 @@ void TextBoxEditTool::edit_start(EditTool& editTool, const std::shared_ptr<DrawC
a->commit_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});

View file

@ -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<DrawTextBox>& a);
void set_styles_at_selection(const std::shared_ptr<DrawTextBox>& a);
RichText::TextStyleModifier::ModifierMap currentMods;
std::vector<std::string> sortedFontList;

View file

@ -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;
}

View file

@ -1,6 +1,18 @@
#pragma once
#include <modules/skparagraph/include/ParagraphStyle.h>
struct ParagraphStyleData {
skia::textlayout::TextAlign textAlignment = skia::textlayout::TextAlign::kLeft;
skia::textlayout::TextDirection textDirection = skia::textlayout::TextDirection::kLtr;
template <typename Archive> void save(Archive& a) const {
a(static_cast<uint8_t>(textAlignment), static_cast<uint8_t>(textDirection));
}
template <typename Archive> void load(Archive& a) {
uint8_t alignRead, directionRead;
a(alignRead, directionRead);
textAlignment = static_cast<skia::textlayout::TextAlign>(alignRead);
textDirection = static_cast<skia::textlayout::TextDirection>(directionRead);
}
skia::textlayout::ParagraphStyle get_paragraph_style() const;
};

View file

@ -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<TextStyleModifier>& 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<TextStyleModifier>& 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<TextStyleModifier>& modifier) {
size_t indexToPlaceAt = 0;
bool finishedInsertingStyle = false;
@ -445,7 +460,7 @@ TextPosition TextBox::move(Movement movement, TextPosition pos, std::optional<fl
if(movement != Movement::NOWHERE && movement != Movement::HOME && movement != Movement::END) {
rebuild();
if(flipDependingOnTextDirection && paragraphs[pos.fParagraphIndex].pStyle.getTextDirection() == skia::textlayout::TextDirection::kRtl) {
if(flipDependingOnTextDirection && paragraphs[pos.fParagraphIndex].pStyleData.textDirection == skia::textlayout::TextDirection::kRtl) {
if(movement == Movement::LEFT)
movement = Movement::RIGHT;
else if(movement == Movement::RIGHT)
@ -640,11 +655,10 @@ void TextBox::rebuild() {
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);
skia::textlayout::ParagraphStyle pStyle = pData.pStyleData.get_paragraph_style();
pStyle.setTextStyle(tStyle);
size_t tIndex = 0;
skia::textlayout::ParagraphBuilderImpl a(pData.pStyle, fontCollection, SkUnicodes::ICU::Make());
skia::textlayout::ParagraphBuilderImpl a(pStyle, fontCollection, SkUnicodes::ICU::Make());
for(;;) {
if(nextTStyleModIt == tStyleMods.end() || nextTStyleModIt->pos.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<void(const SkRect& r)> 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<TextPosition, TextPosition> 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<size_t, size_t> 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();

View file

@ -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<PositionedTextStyleMod> TextStyleModContainer;
struct TextData {
struct Paragraph {
std::string text;
ParagraphStyleData pStyleData;
template<typename Archive> 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<TextStyleModifier>& modifier);
void set_text_style_modifier_between(TextPosition p1, TextPosition p2, const std::shared_ptr<TextStyleModifier>& 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<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
private:
std::pair<TextPosition, TextPosition> get_start_end_text_pos(TextPosition p1, TextPosition p2);
std::pair<size_t, size_t> 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<skia::textlayout::Paragraph> p;
float heightOffset;
skia::textlayout::ParagraphStyle pStyle;
ParagraphStyleData pStyleData;
};
skia::textlayout::TextStyle initialTStyle;