From e507473e3a5ae48800b35edb13674c7e6d2b6ed1 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Thu, 16 Oct 2025 21:22:08 +0300 Subject: [PATCH] Started working on RichTextBox (Broken commit) --- CMakeLists.txt | 1 + src/InputManager.cpp | 59 ++++++++- src/InputManager.hpp | 16 +++ src/MainProgram.cpp | 143 +++++++++++++-------- src/RichTextBox/RichTextBox.cpp | 215 ++++++++++++++++++++++++++++++++ src/RichTextBox/RichTextBox.hpp | 69 ++++++++++ src/main.cpp | 1 + 7 files changed, 454 insertions(+), 50 deletions(-) create mode 100644 src/RichTextBox/RichTextBox.cpp create mode 100644 src/RichTextBox/RichTextBox.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fd646e..409e0c6 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ set(sources "src/main.cpp" "src/Server/ServerData.cpp" "src/CollabTextBox/CollabTextBox.cpp" "src/CollabTextBox/CollabTextBoxShaper.cpp" + "src/RichTextBox/RichTextBox.cpp" "src/DrawingProgram/DrawingProgramCache.cpp" "src/DrawingProgram/DrawingProgram.cpp" "src/DrawingProgram/BrushTool.cpp" diff --git a/src/InputManager.cpp b/src/InputManager.cpp index 42c4fb1..38625bf 100644 --- a/src/InputManager.cpp +++ b/src/InputManager.cpp @@ -77,6 +77,17 @@ void InputManager::text_input_silence_everything() { text.newInput.clear(); } +void InputManager::Text::set_rich_text_box_input(const std::shared_ptr& nTextBox, const std::shared_ptr& nCursor) { + set_accepting_input(); + newTextBox = nTextBox; + newCursor = nCursor; +} + +void InputManager::Text::add_text_to_textbox(const std::string& inputText) { + if(textBox) + textBox->process_text_input(*cursor, inputText); +} + void InputManager::Text::set_accepting_input() { acceptingInputNew = true; } @@ -206,24 +217,43 @@ void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) { switch(kPress) { case SDLK_UP: set_key_down(e, KEY_TEXT_UP); + if(text.textBox && key(KEY_TEXT_UP).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::UP, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_DOWN: set_key_down(e, KEY_TEXT_DOWN); + if(text.textBox && key(KEY_TEXT_DOWN).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::DOWN, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_LEFT: set_key_down(e, KEY_TEXT_LEFT); + if(text.textBox && key(KEY_TEXT_LEFT).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::LEFT, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_RIGHT: set_key_down(e, KEY_TEXT_RIGHT); + if(text.textBox && key(KEY_TEXT_RIGHT).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::RIGHT, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_BACKSPACE: set_key_down(e, KEY_TEXT_BACKSPACE); + if(text.textBox && key(KEY_TEXT_BACKSPACE).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::BACKSPACE, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_DELETE: set_key_down(e, KEY_TEXT_DELETE); + if(text.textBox && key(KEY_TEXT_DELETE).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::DELETE, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_HOME: set_key_down(e, KEY_TEXT_HOME); + if(text.textBox && key(KEY_TEXT_HOME).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::HOME, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); + break; + case SDLK_END: + set_key_down(e, KEY_TEXT_END); + if(text.textBox && key(KEY_TEXT_END).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::END, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; case SDLK_LSHIFT: set_key_down(e, KEY_TEXT_SHIFT); @@ -236,26 +266,41 @@ void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) { set_key_down(e, KEY_TEXT_CTRL); set_key_down(e, KEY_GENERIC_LCTRL); break; + case SDLK_LMETA: + set_key_down(e, KEY_TEXT_META); + set_key_down(e, KEY_GENERIC_LMETA); + break; case SDLK_C: // Use either Ctrl or Meta (command for Mac) keys. We do either instead of checking, since checking can get complicated on Emscripten 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)); 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)); 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()); break; case SDLK_A: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_SELECTALL); + if(text.textBox && key(KEY_TEXT_SELECTALL).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::SELECT_ALL, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; - case SDLK_RETURN: + case SDLK_RETURN: { set_key_down(e, KEY_TEXT_ENTER); + if(text.textBox && key(KEY_TEXT_ENTER).repeat) + text.textBox->process_key_input(*text.cursor, RichTextBox::InputKey::ENTER, ctrl_or_meta_held(), key(KEY_GENERIC_LSHIFT).held); break; + } case SDLK_ESCAPE: set_key_down(e, KEY_GENERIC_ESCAPE); break; @@ -271,6 +316,10 @@ void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) { set_key_down(e, f->second); } +bool InputManager::ctrl_or_meta_held() { + return key(KEY_GENERIC_LCTRL).held || key(KEY_GENERIC_LMETA).held; +} + void InputManager::backend_key_up_update(const SDL_KeyboardEvent& e) { auto kPress = e.key; //auto kMod = e.mod; @@ -297,6 +346,9 @@ void InputManager::backend_key_up_update(const SDL_KeyboardEvent& e) { case SDLK_HOME: set_key_up(e, KEY_TEXT_HOME); break; + case SDLK_END: + set_key_up(e, KEY_TEXT_END); + break; case SDLK_LSHIFT: set_key_up(e, KEY_TEXT_SHIFT); set_key_up(e, KEY_GENERIC_LSHIFT); @@ -354,6 +406,11 @@ void InputManager::frame_reset(const Vector2i& windowSize) { mouse.middleClicks = 0; text.acceptingInput = text.acceptingInputNew; text.acceptingInputNew = false; + text.cursor = text.newCursor; + text.textBox = text.newTextBox; + text.newCursor = nullptr; + text.newTextBox = nullptr; + text.newInput.clear(); droppedItems.clear(); diff --git a/src/InputManager.hpp b/src/InputManager.hpp index 6686850..5213246 100644 --- a/src/InputManager.hpp +++ b/src/InputManager.hpp @@ -10,6 +10,7 @@ #include #include #include +#include "RichTextBox/RichTextBox.hpp" using namespace Eigen; @@ -85,9 +86,20 @@ struct InputManager { void set_accepting_input(); bool get_accepting_input(); + + void set_rich_text_box_input(const std::shared_ptr& nTextBox, const std::shared_ptr& nCursor); + void add_text_to_textbox(const std::string& inputText); + private: + std::shared_ptr textBox; + std::shared_ptr cursor; + + std::shared_ptr newTextBox; + std::shared_ptr newCursor; + bool acceptingInput = false; bool acceptingInputNew = false; + friend struct InputManager; } text; @@ -147,22 +159,26 @@ struct InputManager { KEY_TEXT_BACKSPACE, KEY_TEXT_DELETE, KEY_TEXT_HOME, + KEY_TEXT_END, KEY_TEXT_SHIFT, KEY_TEXT_COPY, KEY_TEXT_CUT, KEY_TEXT_PASTE, KEY_TEXT_ENTER, KEY_TEXT_CTRL, + KEY_TEXT_META, KEY_TEXT_SELECTALL, KEY_GENERIC_ESCAPE, KEY_GENERIC_LSHIFT, KEY_GENERIC_LALT, KEY_GENERIC_LCTRL, + KEY_GENERIC_LMETA, KEY_COUNT }; typedef unsigned KeyCode; + bool ctrl_or_meta_held(); void stop_key_input(); void set_pen_button_up(const SDL_PenButtonEvent& e); void set_pen_button_down(const SDL_PenButtonEvent& e); diff --git a/src/MainProgram.cpp b/src/MainProgram.cpp index 6159fab..8353f6f 100755 --- a/src/MainProgram.cpp +++ b/src/MainProgram.cpp @@ -1,4 +1,6 @@ #include "MainProgram.hpp" +#include "CollabTextBox/CollabTextBoxCursor.hpp" +#include "DrawingProgram/DrawingProgramToolBase.hpp" #include "cereal/archives/portable_binary.hpp" #include "cereal/details/helpers.hpp" #include @@ -35,6 +37,12 @@ #include #include +#include "RichTextBox/RichTextBox.hpp" + +std::shared_ptr textBox; +std::shared_ptr textBoxCursor; +RichTextBox::PaintOpts paintOpts; + MainProgram::MainProgram(): toolbar(*this) { @@ -60,67 +68,97 @@ MainProgram::MainProgram(): *logFile << "[CHAT] " << text << std::endl; std::cout << "[CHAT] " << text << std::endl; }); + + textBox = std::make_shared(); + textBoxCursor = std::make_shared(); + textBox->set_width(1000.0f); + textBox->set_font_collection(fonts.collection); + textBox->insert(0, R"( +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + )"); + + paintOpts.cursor = RichTextBox::Cursor{}; + paintOpts.cursorColor = {1.0f, 0.0f, 0.0f, 0.7f}; } void MainProgram::update() { - while(std::chrono::duration_cast>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit); - lastFrameTime = std::chrono::steady_clock::now(); + input.text.set_rich_text_box_input(textBox, textBoxCursor); - std::erase_if(input.droppedItems, [&](auto& droppedItem) { - if(droppedItem.isFile && std::filesystem::is_regular_file(droppedItem.droppedData)) { - std::filesystem::path droppedFilePath(droppedItem.droppedData); - if(droppedFilePath.has_extension() && droppedFilePath.extension().string() == std::string("." + World::FILE_EXTENSION)) { - new_tab({ - .conType = World::CONNECTIONTYPE_LOCAL, - .fileSource = droppedFilePath.string() - }, true); - return true; - } - } - return false; - }); + //while(std::chrono::duration_cast>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit); + //lastFrameTime = std::chrono::steady_clock::now(); - if(setTabToClose) { - worlds.erase(worlds.begin() + setTabToClose.value()); - setTabToClose.reset(); - } + //if(input.key(InputManager::KEY_DRAW_TOOL_BRUSH).repeat) + // paintOpts.cursor.value().pos = textBox.move(paintOpts.cursor.value().pos, RichTextBox::Movement::kRight); + //else if(input.key(InputManager::KEY_DRAW_TOOL_ERASER).repeat) + // paintOpts.cursor.value().pos = textBox.move(paintOpts.cursor.value().pos, RichTextBox::Movement::kLeft); + //else if(input.key(InputManager::KEY_DRAW_TOOL_TEXTBOX).repeat) + // paintOpts.cursor.value().pos = textBox.move(paintOpts.cursor.value().pos, RichTextBox::Movement::kUp); + //else if(input.key(InputManager::KEY_DRAW_TOOL_ELLIPSE).repeat) + // paintOpts.cursor.value().pos = textBox.move(paintOpts.cursor.value().pos, RichTextBox::Movement::kDown); - if(worldIndex >= worlds.size()) - worldIndex = worlds.size() - 1; - if(worlds.size() == 0) - new_tab({ - .conType = World::CONNECTIONTYPE_LOCAL - }, true); + //std::erase_if(input.droppedItems, [&](auto& droppedItem) { + // if(droppedItem.isFile && std::filesystem::is_regular_file(droppedItem.droppedData)) { + // std::filesystem::path droppedFilePath(droppedItem.droppedData); + // if(droppedFilePath.has_extension() && droppedFilePath.extension().string() == std::string("." + World::FILE_EXTENSION)) { + // new_tab({ + // .conType = World::CONNECTIONTYPE_LOCAL, + // .fileSource = droppedFilePath.string() + // }, true); + // return true; + // } + // } + // return false; + //}); - std::shared_ptr oldWorld = world; - world = worlds[worldIndex]; + //if(setTabToClose) { + // worlds.erase(worlds.begin() + setTabToClose.value()); + // setTabToClose.reset(); + //} - deltaTime.update_time_since(); - deltaTime.update_time_point(); + //if(worldIndex >= worlds.size()) + // worldIndex = worlds.size() - 1; + //if(worlds.size() == 0) + // new_tab({ + // .conType = World::CONNECTIONTYPE_LOCAL + // }, true); - toolbar.initialize_io_before_update(); + //std::shared_ptr oldWorld = world; + //world = worlds[worldIndex]; - for(auto& w : worlds) { - if(w == world) - w->focus_update(); - else - w->unfocus_update(); - } + //deltaTime.update_time_since(); + //deltaTime.update_time_point(); - toolbar.update(); // GUI should be setup after the world data has been fully updated for this frame, so that the GUI reflects the current state of the world data + //toolbar.initialize_io_before_update(); - if(input.key(InputManager::KEY_NOGUI).pressed) - drawGui = !drawGui; + //for(auto& w : worlds) { + // if(w == world) + // w->focus_update(); + // else + // w->unfocus_update(); + //} - if(input.key(InputManager::KEY_FULLSCREEN).pressed) - input.toggleFullscreen = true; + //toolbar.update(); // GUI should be setup after the world data has been fully updated for this frame, so that the GUI reflects the current state of the world data - NetLibrary::update(); + //if(input.key(InputManager::KEY_NOGUI).pressed) + // drawGui = !drawGui; - if(tabSetToOpen) { - tabSetToOpen = false; - new_tab_open(); - } + //if(input.key(InputManager::KEY_FULLSCREEN).pressed) + // input.toggleFullscreen = true; + + //NetLibrary::update(); + + //if(tabSetToOpen) { + // tabSetToOpen = false; + // new_tab_open(); + //} return; } @@ -229,9 +267,16 @@ void MainProgram::load_config() { } void MainProgram::draw(SkCanvas* canvas) { - canvas->clear(transparentBackground ? SkColor4f{0.0f, 0.0f, 0.0f, 0.0f} : world->canvasTheme.backColor); - world->draw(canvas); - toolbar.draw(canvas); + //canvas->clear(transparentBackground ? SkColor4f{0.0f, 0.0f, 0.0f, 0.0f} : world->canvasTheme.backColor); + //world->draw(canvas); + //toolbar.draw(canvas); + + canvas->clear(SkColor4f{0.0f, 0.0f, 0.0f, 1.0f}); + canvas->save(); + canvas->translate(200.0f, 200.0f); + paintOpts.cursor = *textBoxCursor; + textBox->paint(canvas, paintOpts); + canvas->restore(); } bool MainProgram::network_being_used() { diff --git a/src/RichTextBox/RichTextBox.cpp b/src/RichTextBox/RichTextBox.cpp new file mode 100644 index 0000000..601aa66 --- /dev/null +++ b/src/RichTextBox/RichTextBox.cpp @@ -0,0 +1,215 @@ +#include "RichTextBox.hpp" +#include +#include +#include +#include + +RichTextBox::RichTextBox() { +} + +void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift) { + bool moved = false; + + switch(in) { + case InputKey::LEFT: + cur.pos = cur.selectionBeginPos = move(Movement::LEFT, cur.pos); + moved = true; + break; + case InputKey::RIGHT: + cur.pos = cur.selectionBeginPos = move(Movement::RIGHT, cur.pos); + moved = true; + break; + case InputKey::UP: + cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos); + moved = true; + break; + case InputKey::DOWN: + cur.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos); + moved = true; + break; + case InputKey::HOME: + cur.pos = cur.selectionBeginPos = move(Movement::HOME, cur.pos); + moved = true; + break; + case InputKey::END: + cur.pos = cur.selectionBeginPos = move(Movement::END, cur.pos); + moved = true; + break; + case InputKey::BACKSPACE: + if(cur.selectionBeginPos != cur.selectionEndPos) + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); + else + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.pos, move(Movement::LEFT, cur.pos)); + break; + case InputKey::DELETE: + if(cur.selectionBeginPos != cur.selectionEndPos) + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); + else + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.pos, move(Movement::RIGHT, cur.pos)); + break; + case InputKey::ENTER: + if(newlinesAllowed) + process_text_input(cur, "\n"); + break; + case InputKey::SELECT_ALL: + cur.selectionBeginPos = 0; + cur.pos = cur.selectionEndPos = text.size(); + break; + } + + if(moved && !shift) + cur.selectionEndPos = cur.selectionBeginPos; +} + +std::string RichTextBox::process_copy(Cursor& cur) { + uint64_t trueBegin = std::min(cur.selectionBeginPos, cur.selectionEndPos); + uint64_t trueEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos); + return text.substr(trueBegin, trueEnd - trueBegin); +} + +std::string RichTextBox::process_cut(Cursor& cur) { + std::string toRet = process_copy(cur); + if(cur.selectionBeginPos != cur.selectionEndPos) + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); + return toRet; +} + +void RichTextBox::process_text_input(Cursor& cur, const std::string& in) { + if(!in.empty()) { + if(cur.selectionBeginPos != cur.selectionEndPos) + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); + cur.selectionEndPos = cur.selectionBeginPos = cur.pos = insert(cur.pos, in); + } +} + +uint64_t RichTextBox::move(Movement movement, uint64_t pos) { + rebuild(); + + if(pos >= text.size()) + pos = text.size(); + + switch (movement) { + case Movement::LEFT: + if(pos != 0) + pos--; + break; + case Movement::RIGHT: + if(pos < text.size()) + pos++; + break; + case Movement::UP: { + SkRect cursorRect = get_cursor_rect(pos); + Vector2f pointToCheck{cursorRect.x(), cursorRect.top() - 0.1f}; + pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position; + break; + } + case Movement::DOWN: { + SkRect cursorRect = get_cursor_rect(pos); + Vector2f pointToCheck{cursorRect.x(), cursorRect.bottom() + 0.1f}; + pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position; + break; + } + case Movement::HOME: + pos = 0; + break; + case Movement::END: + pos = text.size(); + break; + } + return pos; + +} + +void RichTextBox::set_width(float newWidth) { + width = newWidth; + needsRebuild = true; +} + +void RichTextBox::set_allow_newlines(bool allow) { + newlinesAllowed = true; +} + +void RichTextBox::set_font_collection(const sk_sp& fC) { + fontCollection = fC; + needsRebuild = true; +} + +void RichTextBox::rebuild() { + if(needsRebuild) { + skia::textlayout::ParagraphStyle pStyle; + pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft); + skia::textlayout::TextStyle tStyle; + tStyle.setFontSize(20.0f); + tStyle.setFontFamilies({SkString{"Roboto"}}); + pStyle.setTextStyle(tStyle); + + skia::textlayout::ParagraphBuilderImpl a(pStyle, fontCollection, SkUnicodes::ICU::Make()); + a.addText(text.c_str(), text.length()); + paragraph = a.Build(); + paragraph->layout(width); + } +} + +uint64_t RichTextBox::insert(uint64_t pos, const std::string& textToInsert) { + text.insert(pos, textToInsert); + needsRebuild = true; + return pos + textToInsert.size(); +} + +uint64_t RichTextBox::remove(uint64_t start, uint64_t end) { + if(start > end) + std::swap(start, end); + + text.erase(start, end - start); + needsRebuild = true; + return start; +} + +SkRect RichTextBox::get_cursor_rect(uint64_t pos) { + SkPoint topLeft; + float height; + + if(text.empty()) { + topLeft = {0.0f, 0.0f}; + height = paragraph->getHeight(); + } + else if(pos == text.size()) { + std::vector posBoxes = paragraph->getRectsForRange(pos - 1, pos, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax); + if(text.back() == '\n') { + topLeft = {0.0f, posBoxes.back().rect.bottom()}; + height = std::abs(paragraph->getHeight() - topLeft.y()); + } + else { + topLeft = posBoxes.front().rect.TR(); + height = posBoxes.front().rect.height(); + } + } + else { + std::vector posBoxes = paragraph->getRectsForRange(pos, pos + 1, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax); + topLeft = posBoxes.front().rect.TL(); + height = posBoxes.front().rect.height(); + } + return SkRect::MakeXYWH(topLeft.x(), topLeft.y(), 3.0f, height); +} + +void RichTextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) { + rebuild(); + + if(paintOpts.cursor.has_value()) { + auto& cur = paintOpts.cursor.value(); + SkPaint selection{color_mul_alpha(paintOpts.cursorColor, 0.6f)}; + uint64_t trueSelectionStart = std::min(cur.selectionBeginPos, cur.selectionEndPos); + uint64_t trueSelectionEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos); + std::vector selectionBoxes = paragraph->getRectsForRange(trueSelectionStart, trueSelectionEnd, skia::textlayout::RectHeightStyle::kTight, skia::textlayout::RectWidthStyle::kTight); + for(auto& selectionBox : selectionBoxes) { + SkRect rectFixSmallOverlap = SkRect::MakeXYWH(selectionBox.rect.x(), selectionBox.rect.y(), selectionBox.rect.width(), std::max(selectionBox.rect.height() - 0.25f, 0.0f)); + canvas->drawRect(rectFixSmallOverlap, selection); + } + + if(cur.pos <= text.size()) { + canvas->drawRect(get_cursor_rect(cur.pos), SkPaint{paintOpts.cursorColor}); + } + } + + paragraph->paint(canvas, 0.0f, 0.0f); +} diff --git a/src/RichTextBox/RichTextBox.hpp b/src/RichTextBox/RichTextBox.hpp new file mode 100644 index 0000000..385d753 --- /dev/null +++ b/src/RichTextBox/RichTextBox.hpp @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include +#include +#include "../SharedTypes.hpp" + +class RichTextBox { + public: + RichTextBox(); + + enum class Movement { + LEFT, + RIGHT, + UP, + DOWN, + HOME, + END + }; + + struct Cursor { + uint64_t pos = 0; + uint64_t selectionBeginPos = 0; + uint64_t selectionEndPos = 0; + }; + + struct PaintOpts { + SkColor4f cursorColor = {1, 0, 0, 1}; + std::optional cursor; + }; + + uint64_t move(Movement movement, uint64_t pos); + void set_width(float newWidth); + uint64_t insert(uint64_t pos, const std::string& textToInsert); + uint64_t remove(uint64_t start, uint64_t end); + void set_font_collection(const sk_sp& fC); + void paint(SkCanvas* canvas, const PaintOpts& paintOpts); + void set_allow_newlines(bool allow); + + enum class InputKey { + LEFT, + RIGHT, + UP, + DOWN, + HOME, + END, + BACKSPACE, + DELETE, + ENTER, + SELECT_ALL + }; + + void process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift); + std::string process_copy(Cursor& cur); + std::string process_cut(Cursor& cur); + void process_text_input(Cursor& cur, const std::string& in); + private: + SkRect get_cursor_rect(uint64_t pos); + + void rebuild(); + + bool newlinesAllowed = true; + float width = 100.0f; + float fontSize = 0.0f; + bool needsRebuild = true; + sk_sp fontCollection; + std::unique_ptr paragraph; + std::string text; +}; diff --git a/src/main.cpp b/src/main.cpp index 30a2ecb..59cd2e5 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -696,6 +696,7 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { break; case SDL_EVENT_TEXT_INPUT: mS.m->input.text.newInput += event->text.text; + mS.m->input.text.add_text_to_textbox(event->text.text); break; case SDL_EVENT_DROP_FILE: mS.m->window.scale = SDL_GetWindowPixelDensity(mS.window);