diff --git a/.github/workflows/ecode-nightly.yml b/.github/workflows/ecode-nightly.yml index b76b6743e..cdc3bb5fa 100644 --- a/.github/workflows/ecode-nightly.yml +++ b/.github/workflows/ecode-nightly.yml @@ -443,40 +443,40 @@ jobs: files: | projects/freebsd/ecode/ecode-freebsd-${{ env.INSTALL_REF }}-x86_64.tar.gz - # build_haiku_x86_64: - # name: Haiku x86_64 Nightly - # needs: release - # runs-on: ubuntu-latest - # steps: - # - name: Checkout Code - # uses: actions/checkout@v6 - # with: { fetch-depth: 0, submodules: 'recursive' } - # - name: Set Environment Variables - # run: | - # echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - # echo "RARCH=$(uname -m)" >> "$GITHUB_ENV" - # - uses: vmactions/haiku-vm@v1 - # env: - # INSTALL_REF: ${{ needs.release.outputs.version }} - # with: - # envs: 'INSTALL_REF' - # usesh: true - # mem: 8192 - # disable-cache: true - # prepare: | - # pkgman install -y libsdl2 libsdl2_devel git gcc premake5 - # run: | - # git config --global --add safe.directory "$GITHUB_WORKSPACE" - # bash projects/scripts/patch_commit_number.sh - # sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }} - # - name: Upload Files - # uses: softprops/action-gh-release@v3 - # with: - # tag_name: ${{ needs.release.outputs.version }} - # draft: false - # prerelease: true - # files: | - # projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz + build_haiku_x86_64: + name: Haiku x86_64 Nightly + needs: release + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + with: { fetch-depth: 0, submodules: 'recursive' } + - name: Set Environment Variables + run: | + echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" + echo "RARCH=$(uname -m)" >> "$GITHUB_ENV" + - uses: vmactions/haiku-vm@v1 + env: + INSTALL_REF: ${{ needs.release.outputs.version }} + with: + envs: 'INSTALL_REF' + usesh: true + mem: 8192 + disable-cache: true + prepare: | + pkgman install -y libsdl2 libsdl2_devel git gcc premake5 + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + bash projects/scripts/patch_commit_number.sh + sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }} + - name: Upload Files + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ needs.release.outputs.version }} + draft: false + prerelease: true + files: | + projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz build_windows_msvc: name: Windows x86_64 MSVC Nightly diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index dc27b276c..ca9fe702c 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -853,6 +853,8 @@ class EE_API TextDocument { void notifyLineChanged( const Int64& lineIndex ); + void notifyLinesChanged( const Int64& firstLine, const Int64& lastLine ); + void notifyUndoRedo( const UndoRedo& eventType ); void notifyDirtyOnFileSystem(); diff --git a/include/eepp/ui/doc/textdocumentline.hpp b/include/eepp/ui/doc/textdocumentline.hpp index 540d39be1..00ce6fbbe 100644 --- a/include/eepp/ui/doc/textdocumentline.hpp +++ b/include/eepp/ui/doc/textdocumentline.hpp @@ -17,6 +17,23 @@ class EE_API TextDocumentLine { updateState(); } + TextDocumentLine( String&& text, std::shared_ptr docMutex ) : + mText( std::move( text ) ), mDocMutex( std::move( docMutex ) ) { + updateState(); + } + + TextDocumentLine( const TextDocumentLine& ) = default; + + TextDocumentLine( TextDocumentLine&& other ) noexcept : mDocMutex( other.mDocMutex ) { + ConditionalLock lock( mDocMutex != nullptr, mDocMutex.get() ); + mText = std::move( other.mText ); + mHash = other.mHash; + mFlags = other.mFlags; + other.mDocMutex.reset(); + } + + TextDocumentLine& operator=( const TextDocumentLine& ) = default; + ~TextDocumentLine() { if ( mDocMutex ) { // Wait for any readers to finish before destruction diff --git a/include/eepp/window/inputmethod.hpp b/include/eepp/window/inputmethod.hpp index 8f0ec4231..33bd2083c 100644 --- a/include/eepp/window/inputmethod.hpp +++ b/include/eepp/window/inputmethod.hpp @@ -23,6 +23,12 @@ class EE_API InputMethod { Int32 length{ 0 }; }; + /** + * Queues the text-input candidate location. Backend updates are coalesced globally and limited + * to one every 50 ms by default. Set EEPP_IME_LOCATION_UPDATE_INTERVAL_MS to a non-negative + * integer to override the interval (values are capped at 65535 ms); 0 applies the latest + * location once per rendered frame. + */ void setLocation( Rect rect ); bool isEditing() const; @@ -51,11 +57,15 @@ class EE_API InputMethod { EE::Window::Window* mWindow{ nullptr }; InputMethod::State mState; bool mEditing{ false }; + bool mLocationDirty{ false }; + Uint16 mLocationUpdateCooldown{ 0 }; Rect mLastLocation; Uint32 mLastCb{ 0 }; std::map mEditingCbs; void sendTextEditing( const String& txt, Int32 start, Int32 length ); + + void updateLocation(); }; }} // namespace EE::Window diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 5179a8bd7..03244cf49 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -16,6 +16,8 @@ #include #include +#include +#include #include using namespace std::literals; @@ -1570,30 +1572,47 @@ TextPosition TextDocument::insert( const size_t& cursorIdx, TextPosition positio position = sanitizePosition( position ); size_t lineCount = linesCount(); Int64 linesAdd = 0; + bool multiline = text.find( '\n' ) != String::InvalidPos; { Lock l( mLinesMutex ); - if ( text.find( '\n' ) == String::InvalidPos ) { + if ( !multiline ) { mLines[position.line()].insert( position.column(), text ); notifyLineChanged( position.line() ); } else { - String before = mLines[position.line()].substr( 0, position.column() ); - String after = mLines[position.line()].substr( position.column() ); - std::vector lines = text.split( '\n', true ); - linesAdd = eemax( 0, static_cast( lines.size() ) - 1 ); - for ( auto i = 0; i < linesAdd; i++ ) - lines[i] = lines[i] + "\n"; - lines[0] = before + lines[0]; - lines[lines.size() - 1] = lines[lines.size() - 1] + after; + { + Lock l2( *mDocumentMutex ); + String before = mLines[position.line()].substr( 0, position.column() ); + String after = mLines[position.line()].substr( position.column() ); + String::View textView = text.view(); + linesAdd = + static_cast( std::count( textView.begin(), textView.end(), '\n' ) ); + // Build the new document lines once and insert the complete range. Inserting each + // line separately repeatedly grows the vector and shifts its trailing elements. + SmallVector lines; + lines.reserve( static_cast( linesAdd ) ); - mLines[position.line()] = TextDocumentLine( lines[0], mDocumentMutex ); - notifyLineChanged( position.line() ); + size_t lineStart = 0; + for ( Int64 i = 0; i <= linesAdd; ++i ) { + size_t newLine = textView.find( '\n', lineStart ); + size_t lineEnd = newLine == String::InvalidPos ? textView.size() : newLine + 1; + String line( textView.substr( lineStart, lineEnd - lineStart ) ); + if ( i == 0 ) + line.insert( 0, before ); + if ( i == linesAdd ) + line.append( after ); + if ( i == 0 ) + mLines[position.line()].setText( std::move( line ) ); + else + lines.emplace_back( std::move( line ), mDocumentMutex ); + lineStart = lineEnd; + } - for ( Int64 i = 1; i < (Int64)lines.size(); i++ ) { - mLines.insert( mLines.begin() + position.line() + i, - TextDocumentLine( lines[i], mDocumentMutex ) ); - notifyLineChanged( position.line() + i ); + mLines.insert( mLines.begin() + position.line() + 1, + std::make_move_iterator( lines.begin() ), + std::make_move_iterator( lines.end() ) ); } + notifyLinesChanged( position.line(), position.line() + linesAdd ); } } @@ -4398,6 +4417,14 @@ void TextDocument::notifyLineChanged( const Int64& lineIndex ) { } } +void TextDocument::notifyLinesChanged( const Int64& firstLine, const Int64& lastLine ) { + Lock l( mClientsMutex ); + for ( Int64 lineIndex = firstLine; lineIndex <= lastLine; ++lineIndex ) { + for ( auto& client : mClients ) + client->onDocumentLineChanged( lineIndex ); + } +} + void TextDocument::notifyUndoRedo( const TextDocument::UndoRedo& eventType ) { Lock l( mClientsMutex ); for ( auto& client : mClients ) { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 528729c5d..f16fbca86 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1156,8 +1156,8 @@ Uint32 UICodeEditor::onTextInput( const TextInputEvent& event ) { } void UICodeEditor::updateIMELocation() { - if ( mDoc->isLoading() || mDoc->getActiveClient() != this || !Engine::isMainThread() || - mDocView.isFolded( mDoc->getSelection( true ).start().line() ) ) + if ( !hasFocus() || mDoc->isLoading() || mDoc->getActiveClient() != this || + !Engine::isMainThread() || mDocView.isFolded( mDoc->getSelection( true ).start().line() ) ) return; Rectf r( getScreenPosition( mDoc->getSelection( true ).start() ) ); getUISceneNode()->getWindow()->getIME().setLocation( r.asInt() ); diff --git a/src/eepp/ui/uiconsole.cpp b/src/eepp/ui/uiconsole.cpp index 162b4b8d0..ce7067d1c 100644 --- a/src/eepp/ui/uiconsole.cpp +++ b/src/eepp/ui/uiconsole.cpp @@ -1051,7 +1051,7 @@ Uint32 UIConsole::onTextEditing( const TextEditingEvent& event ) { } void UIConsole::updateIMELocation( const Rectf& loc ) { - if ( mDoc.getActiveClient() != this ) + if ( !hasFocus() || mDoc.getActiveClient() != this ) return; getUISceneNode()->getWindow()->getIME().setLocation( loc.asInt() ); } diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index 56afdadcb..be377e607 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -941,7 +941,7 @@ Uint32 UITextInput::onTextInput( const TextInputEvent& event ) { } void UITextInput::updateIMELocation() { - if ( mDoc.getActiveClient() != this || !Engine::isMainThread() ) + if ( !hasFocus() || mDoc.getActiveClient() != this || !Engine::isMainThread() ) return; Vector2f cursor( eefloor( mScreenPos.x + mRealAlignOffset.x + mCurPos.x + mPaddingPx.Left ), mScreenPos.y + mRealAlignOffset.y + mCurPos.y + mPaddingPx.Top ); diff --git a/src/eepp/window/inputmethod.cpp b/src/eepp/window/inputmethod.cpp index 067cf2a58..8da2962dc 100644 --- a/src/eepp/window/inputmethod.cpp +++ b/src/eepp/window/inputmethod.cpp @@ -1,10 +1,28 @@ #include #include +#include #include #include +#include +#include + namespace EE { namespace Window { +static constexpr Uint32 DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 50; + +static Uint16 getLocationUpdateInterval() { + static Uint16 interval = [] { + std::string value = Sys::getEnv( "EEPP_IME_LOCATION_UPDATE_INTERVAL_MS" ); + Uint32 parsedValue; + if ( value.empty() || !String::fromString( parsedValue, value ) ) + return static_cast( DEFAULT_LOCATION_UPDATE_INTERVAL_MS ); + return static_cast( + std::min( parsedValue, std::numeric_limits::max() ) ); + }(); + return interval; +} + InputMethod::InputMethod( EE::Window::Window* window ) : mWindow( window ) {} void InputMethod::sendTextEditing( const String& txt, Int32 start, Int32 length ) { @@ -17,11 +35,32 @@ void InputMethod::setLocation( Rect rect ) { rect = PixelDensity::pxToDpI( rect ); #endif if ( rect != mLastLocation ) { - mWindow->setTextInputRect( rect ); mLastLocation = std::move( rect ); + mLocationDirty = true; } } +void InputMethod::updateLocation() { + double elapsed = mWindow->getElapsed().asMilliseconds(); + if ( elapsed >= mLocationUpdateCooldown ) + mLocationUpdateCooldown = 0; + else + mLocationUpdateCooldown -= static_cast( elapsed ); + + if ( !mLocationDirty ) + return; + + if ( mLocationUpdateCooldown > 0 ) + return; + + // SDL3 can synchronously notify the platform IME (including a DBus round-trip on Linux). + // Apply only the latest requested location at a bounded rate and keep the backend call on the + // main thread, as required by SDL. + mLocationDirty = false; + mLocationUpdateCooldown = getLocationUpdateInterval(); + mWindow->setTextInputRect( mLastLocation ); +} + bool InputMethod::isEditing() const { return mEditing; } diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index cd2067817..5fb91abad 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -463,6 +463,7 @@ void Window::display( bool clear ) { GlobalBatchRenderer::instance()->draw(); if ( TextureFactory* textureFactory = TextureFactory::existsSingleton() ) textureFactory->collectReleasedTextures(); + mIME.updateLocation(); swapBuffers(); diff --git a/src/tests/unit_tests/textdocument_tests.cpp b/src/tests/unit_tests/textdocument_tests.cpp index 06b36a855..6e00ed5ad 100644 --- a/src/tests/unit_tests/textdocument_tests.cpp +++ b/src/tests/unit_tests/textdocument_tests.cpp @@ -127,6 +127,27 @@ UTEST( TextDocument, insertIntoExistingLines ) { EXPECT_STRINGEQ( "Yond\n", doc.line( 3 ).getText() ); } +UTEST( TextDocument, insertLargeMultilineBlock ) { + constexpr size_t insertedLineCount = 8192; + String text; + text.reserve( insertedLineCount * 4 + 4 ); + for ( size_t i = 0; i < insertedLineCount; ++i ) + text.append( "row\n" ); + text.append( "last" ); + + TextDocument doc; + doc.insert( 0, { 0, 0 }, "head\nmiddle\ntail" ); + TextPosition cursor = doc.insert( 0, { 1, 3 }, text ); + + EXPECT_EQ( insertedLineCount + 1, static_cast( cursor.line() ) ); + EXPECT_EQ( 4, cursor.column() ); + EXPECT_EQ( insertedLineCount + 3, doc.linesCount() ); + EXPECT_STRINGEQ( "midrow\n", doc.line( 1 ).getText() ); + EXPECT_STRINGEQ( "row\n", doc.line( insertedLineCount ).getText() ); + EXPECT_STRINGEQ( "lastdle\n", doc.line( insertedLineCount + 1 ).getText() ); + EXPECT_STRINGEQ( "tail\n", doc.line( insertedLineCount + 2 ).getText() ); +} + UTEST( TextDocument, insertEmptyTextDoesNothing ) { TextDocument doc; doc.insert( 0, { 0, 0 }, "content" );