Added rich text clipboard

This commit is contained in:
Yousef Khadadeh 2025-11-11 17:42:41 +03:00
parent c7cea45e9c
commit afe1623cf8
5 changed files with 139 additions and 142 deletions

View file

@ -101,11 +101,6 @@ bool TextBoxEditTool::edit_gui(const std::shared_ptr<DrawComponent>& comp) {
a->textBox->set_text_style_modifier_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos, currentMods[TextStyleModifier::ModifierType::COLOR]);
}
if(t.gui.text_button_wide("print text", "Print HTML Selection")) {
std::cout << "-----HTML-----\n" << a->textBox->get_rich_text_data_between(a->cursor->selectionBeginPos, a->cursor->selectionEndPos).get_html() << std::endl;
}
t.gui.pop_id();
if(a->textBox->inputChangedTextBox)

View file

@ -5,6 +5,8 @@
#include <optional>
#include "Server/CommandList.hpp"
#ifdef __EMSCRIPTEN__
#include <EmscriptenHelpers/emscripten_browser_clipboard.h>
@ -116,6 +118,14 @@ std::string InputManager::get_clipboard_str() {
#endif
}
std::string InputManager::get_clipboard_data_for_mimetype(const std::string& mimeType) {
size_t datSize = 0;
void* dat = SDL_GetClipboardData(mimeType.c_str(), &datSize);
if(!dat)
return "";
return std::string(static_cast<char*>(dat), datSize);
}
void InputManager::set_clipboard_str(std::string_view s) {
#ifdef __EMSCRIPTEN__
emscripten_browser_clipboard::copy(std::string(s));
@ -124,6 +134,40 @@ void InputManager::set_clipboard_str(std::string_view s) {
#endif
}
std::string InputManager::get_infpnt_richtext_mimetype() {
return "infpnt/richtext" + std::string(VERSION_STRING);
}
void InputManager::set_clipboard_plain_and_richtext_pair(const std::pair<std::string, std::string>& plainAndRichtextPair) {
std::unordered_map<std::string, std::string> clipboardData;
clipboardData["text/plain"] = plainAndRichtextPair.first;
clipboardData[get_infpnt_richtext_mimetype()] = plainAndRichtextPair.second;
set_clipboard_data(clipboardData);
}
void InputManager::set_clipboard_data(const std::unordered_map<std::string, std::string>& newClipboardData) {
SDL_ClearClipboardData();
clipboardData = newClipboardData;
std::vector<const char*> mimeTypes;
for(auto& [k, v] : clipboardData)
mimeTypes.emplace_back(k.c_str());
SDL_SetClipboardData(
[](void *userdata, const char *mime_type, size_t *size) {
std::unordered_map<std::string, std::string>& clipboardData = *static_cast<std::unordered_map<std::string, std::string>*>(userdata);
std::string mimeTypeStr(mime_type);
auto it = clipboardData.find(mimeTypeStr);
if(it == clipboardData.end())
return static_cast<const void*>(nullptr);
*size = it->second.length();
return static_cast<const void*>(it->second.c_str());
},
[](void *userdata) {
},
static_cast<void*>(&clipboardData), mimeTypes.data(), mimeTypes.size());
}
std::string InputManager::key_assignment_to_str(const Vector2ui32& k) {
std::string toRet;
if(k.x() & SDL_KMOD_CTRL)
@ -276,19 +320,24 @@ void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) {
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
set_key_down(e, KEY_TEXT_COPY);
if(text.textBox && key(KEY_TEXT_COPY).repeat)
set_clipboard_str(text.textBox->process_copy(*text.cursor));
set_clipboard_plain_and_richtext_pair(text.textBox->process_copy(*text.cursor));
break;
case SDLK_X:
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
set_key_down(e, KEY_TEXT_CUT);
if(text.textBox && key(KEY_TEXT_CUT).repeat)
set_clipboard_str(text.textBox->process_cut(*text.cursor));
set_clipboard_plain_and_richtext_pair(text.textBox->process_cut(*text.cursor));
break;
case SDLK_V:
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))
set_key_down(e, KEY_TEXT_PASTE);
if(text.textBox && key(KEY_TEXT_PASTE).repeat)
text.textBox->process_text_input(*text.cursor, get_clipboard_str(), text.modMap);
if(text.textBox && key(KEY_TEXT_PASTE).repeat) {
std::string richTextClipboard = get_clipboard_data_for_mimetype(get_infpnt_richtext_mimetype());
if(!richTextClipboard.empty())
text.textBox->process_rich_text_input(*text.cursor, richTextClipboard);
else
text.textBox->process_text_input(*text.cursor, get_clipboard_data_for_mimetype("text/plain"), text.modMap);
}
break;
case SDLK_A:
if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL))

View file

@ -195,7 +195,15 @@ struct InputManager {
bool get_clipboard_paste_happened();
std::string get_clipboard_str();
std::string get_clipboard_data_for_mimetype(const std::string& mimeType);
void set_clipboard_str(std::string_view s);
void set_clipboard_plain_and_richtext_pair(const std::pair<std::string, std::string>& plainAndRichtextPair);
void set_clipboard_data(const std::unordered_map<std::string, std::string>& newClipboardData);
std::string get_infpnt_richtext_mimetype();
std::unordered_map<std::string, std::string> clipboardData;
#ifdef __EMSCRIPTEN__
bool clipboardPasteEventHappened = false;

View file

@ -1,5 +1,6 @@
#include "TextBox.hpp"
#include "TextStyleModifier.hpp"
#include "cereal/archives/portable_binary.hpp"
#include <limits>
#include <modules/skparagraph/include/DartTypes.h>
#include <modules/skparagraph/include/Metrics.h>
@ -15,128 +16,13 @@
namespace RichText {
std::string TextData::get_html() {
auto nextTStyleModIt = tStyleMods.begin();
skia::textlayout::TextStyle tStyle;
std::stringstream toRet;
for(size_t pIndex = 0; pIndex < paragraphs.size(); pIndex++) {
Paragraph& pData = paragraphs[pIndex];
size_t tIndex = 0;
toRet << "<p style=\"" << get_css_from_paragraph_style(pData.pStyleData) << "\">";
for(;;) {
if(nextTStyleModIt == tStyleMods.end() || nextTStyleModIt->pos.fParagraphIndex != pIndex) {
if(pData.text.length() != tIndex) {
toRet << "<span style=\"" << get_css_from_text_style(tStyle) << "\">"
<< pData.text.substr(tIndex, pData.text.length() - tIndex)
<< "</span>";
}
break;
}
else {
auto& nextTStyleMod = *nextTStyleModIt;
// Print with previous style
if(nextTStyleMod.pos.fTextByteIndex != tIndex) {
toRet << "<span style=\"" << get_css_from_text_style(tStyle) << "\">"
<< pData.text.substr(tIndex, nextTStyleMod.pos.fTextByteIndex - tIndex)
<< "</span>";
}
tIndex = nextTStyleMod.pos.fTextByteIndex;
// Modify tStyle with new data
for(auto& [modType, modifier] : nextTStyleMod.mods)
modifier->modify_text_style(tStyle);
++nextTStyleModIt;
}
}
toRet << "</p>";
std::string TextData::get_serialized() {
std::stringstream s;
{
cereal::PortableBinaryOutputArchive a(s);
a(*this);
}
return toRet.str();
}
std::string TextData::get_css_from_paragraph_style(const ParagraphStyleData& pStyle) {
std::stringstream toRet;
toRet << "text-align:";
switch(pStyle.textAlignment) {
case skia::textlayout::TextAlign::kLeft:
toRet << "left";
break;
case skia::textlayout::TextAlign::kRight:
toRet << "right";
break;
case skia::textlayout::TextAlign::kCenter:
toRet << "center";
break;
case skia::textlayout::TextAlign::kJustify:
toRet << "justify";
break;
default:
toRet << "left";
break;
}
toRet << ";"
<< "direction:";
switch(pStyle.textDirection) {
case skia::textlayout::TextDirection::kLtr:
toRet << "ltr";
break;
case skia::textlayout::TextDirection::kRtl:
toRet << "rtl";
break;
}
toRet << ";";
return toRet.str();
}
std::string TextData::get_css_from_text_style(const skia::textlayout::TextStyle& tStyle) {
std::stringstream toRet;
toRet << "font: " << static_cast<int>(tStyle.getFontSize()) << "px ";
auto& fontFamilies = tStyle.getFontFamilies();
for(size_t i = 0; i < fontFamilies.size(); i++) {
auto& family = fontFamilies[i];
toRet << std::string(family.c_str(), family.size());
if(i != fontFamilies.size() - 1)
toRet << ",";
}
SkPaint paint = tStyle.getForeground();
toRet << ";"
<< "color: rgb(" << std::fixed << std::setprecision(7) << paint.getColor4f().fR * 255.0f << " " << paint.getColor4f().fG * 255.0f << " " << paint.getColor4f().fB * 255.0f << " / " << paint.getColor4f().fA << ");" << std::defaultfloat
<< "font-weight:" << tStyle.getFontStyle().weight() << "; "
<< "font-style:";
switch(tStyle.getFontStyle().slant()) {
case SkFontStyle::Slant::kUpright_Slant:
toRet << "normal";
break;
case SkFontStyle::Slant::kItalic_Slant:
toRet << "italic";
break;
case SkFontStyle::Slant::kOblique_Slant:
toRet << "oblique";
break;
}
toRet << ";"
<< "text-decoration-line:";
if(tStyle.getDecorationType() == skia::textlayout::TextDecoration::kNoDecoration)
toRet << " none";
else {
if(tStyle.getDecorationType() & skia::textlayout::TextDecoration::kOverline)
toRet << " overline";
if(tStyle.getDecorationType() & skia::textlayout::TextDecoration::kUnderline)
toRet << " underline";
if(tStyle.getDecorationType() & skia::textlayout::TextDecoration::kLineThrough)
toRet << " line-through";
}
toRet << ";";
return toRet.str();
return s.str();
}
void TextData::save(cereal::PortableBinaryOutputArchive& a) const {
@ -265,12 +151,12 @@ void TextBox::process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool c
}
}
std::string TextBox::process_copy(Cursor& cur) {
return get_text_between(cur.selectionBeginPos, cur.selectionEndPos);
std::pair<std::string, std::string> TextBox::process_copy(Cursor& cur) {
return {get_text_between(cur.selectionBeginPos, cur.selectionEndPos), get_rich_text_data_between(cur.selectionBeginPos, cur.selectionEndPos).get_serialized()};
}
std::string TextBox::process_cut(Cursor& cur) {
std::string toRet = process_copy(cur);
std::pair<std::string, std::string> TextBox::process_cut(Cursor& cur) {
auto toRet = process_copy(cur);
if(cur.selectionBeginPos != cur.selectionEndPos) {
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
cur.previousX = std::nullopt;
@ -289,6 +175,22 @@ void TextBox::process_text_input(Cursor& cur, const std::string& in, const std::
}
}
void TextBox::process_rich_text_input(Cursor& cur, const std::string& serializedRichText) {
if(!serializedRichText.empty()) {
if(cur.selectionBeginPos != cur.selectionEndPos)
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos);
RichText::TextData textData;
std::stringstream ss(serializedRichText);
{
cereal::PortableBinaryInputArchive a(ss);
a(textData);
}
cur.selectionEndPos = cur.selectionBeginPos = cur.pos = insert_rich_text(cur.pos, textData);
cur.previousX = std::nullopt;
inputChangedTextBox = true;
}
}
size_t TextBox::count_grapheme(const std::string& text) {
auto u = SkUnicodes::ICU::Make();
auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes);
@ -442,6 +344,44 @@ TextData TextBox::get_rich_text_data_between(TextPosition p1, TextPosition p2) {
return toRet;
}
TextPosition TextBox::insert_rich_text(TextPosition p, const TextData& richText) {
p = move(Movement::NOWHERE, p);
if(richText.paragraphs.empty())
return p;
std::string strToInsert;
for(auto& p : richText.paragraphs) {
strToInsert += p.text;
strToInsert += '\n';
}
strToInsert.pop_back();
TextPosition toRet = insert(p, strToInsert);
TextPosition lastPos;
lastPos.fParagraphIndex = p.fParagraphIndex + richText.paragraphs.size() - 1;
if(richText.paragraphs.size() == 1)
lastPos.fTextByteIndex = p.fTextByteIndex + richText.paragraphs[0].text.size();
else
lastPos.fTextByteIndex = richText.paragraphs.back().text.size();
for(auto& [modPos, modMap] : richText.tStyleMods) {
TextPosition shiftedModPos;
shiftedModPos.fParagraphIndex = modPos.fParagraphIndex + p.fParagraphIndex;
if(shiftedModPos.fParagraphIndex == p.fParagraphIndex)
shiftedModPos.fTextByteIndex = p.fTextByteIndex + modPos.fTextByteIndex;
else
shiftedModPos.fTextByteIndex = modPos.fTextByteIndex;
for(auto& [modType, modifier] : modMap)
set_text_style_modifier_between(shiftedModPos, lastPos, modifier);
}
inputChangedTextBox = true;
needsRebuild = true;
return toRet;
}
void TextBox::clear_text() {
paragraphs.clear();
tStyleMods.clear();

View file

@ -16,8 +16,14 @@ struct TextPosition {
// Set to uint32_t values because theyll be saved and sent over networks. Size has to be consistent
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)fParagraphIndex, (uint32_t)fTextByteIndex);
template <typename Archive> void save(Archive& a) const {
a(static_cast<uint32_t>(fParagraphIndex), static_cast<uint32_t>(fTextByteIndex));
}
template <typename Archive> void load(Archive& a) {
uint32_t p, t;
a(p, t);
fParagraphIndex = p;
fTextByteIndex = t;
}
bool operator==(const TextPosition& o) const = default;
bool operator<(const TextPosition& o) const;
@ -42,14 +48,11 @@ class TextData {
a(text, pStyleData);
}
};
std::string get_html();
std::string get_serialized();
void save(cereal::PortableBinaryOutputArchive& a) const;
void load(cereal::PortableBinaryInputArchive& a);
TextStyleModContainer tStyleMods;
std::vector<Paragraph> paragraphs;
private:
std::string get_css_from_text_style(const skia::textlayout::TextStyle& tStyle);
std::string get_css_from_paragraph_style(const ParagraphStyleData& pStyle);
};
class TextBox {
@ -123,18 +126,20 @@ class TextBox {
void set_rich_text_data(const TextData& richText);
TextData get_rich_text_data_between(TextPosition p1, TextPosition p2);
TextPosition insert_rich_text(TextPosition p, const TextData& richText);
void clear_text();
void set_string(const std::string& str);
void process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift);
void process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift, const std::optional<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
std::string process_copy(Cursor& cur);
std::string process_cut(Cursor& cur);
std::pair<std::string, std::string> process_copy(Cursor& cur);
std::pair<std::string, std::string> process_cut(Cursor& cur);
std::string get_text_between(TextPosition p1, TextPosition p2);
std::string get_string();
void set_tab_space_width(unsigned newTabWidth);
void process_text_input(Cursor& cur, const std::string& in, const std::optional<TextStyleModifier::ModifierMap>& inputModMap = std::nullopt);
void process_rich_text_input(Cursor& cur, const std::string& serializedRichText);
private:
std::pair<TextPosition, TextPosition> get_start_end_text_pos(TextPosition p1, TextPosition p2);