diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index aa462f250..020fbe14b 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -1890,6 +1890,28 @@ class EE_API Node : public Transformable { */ bool hasEventsOfType( const Uint32& eventType ) const; + /** + * @brief Enables clipping for the node's bounds. + * + * Convenience overload that automatically determines if clipping planes + * are needed based on transforms. + * + * @param x Left edge. + * @param y Top edge. + * @param Width Width. + * @param Height Height. + */ + void clipSmartEnable( const Int32& x, const Int32& y, const Uint32& Width, + const Uint32& Height ); + + /** + * @brief Disables clipping. + * + * Convenience overload that automatically determines if clipping planes + * are active. + */ + void clipSmartDisable(); + protected: /** @brief Map of event type to callback ID to callback function. */ typedef UnorderedMap> EventsMap; @@ -2465,28 +2487,6 @@ class EE_API Node : public Transformable { */ void clipSmartDisable( bool needsClipPlanes ); - /** - * @brief Enables clipping for the node's bounds. - * - * Convenience overload that automatically determines if clipping planes - * are needed based on transforms. - * - * @param x Left edge. - * @param y Top edge. - * @param Width Width. - * @param Height Height. - */ - void clipSmartEnable( const Int32& x, const Int32& y, const Uint32& Width, - const Uint32& Height ); - - /** - * @brief Disables clipping. - * - * Convenience overload that automatically determines if clipping planes - * are active. - */ - void clipSmartDisable(); - /** * @brief Finds the nearest draw invalidator in the parent chain. * diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp index 1c79bf9ae..1f4e233af 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.cpp @@ -171,6 +171,17 @@ void AutoCompletePlugin::load( PluginManager* pluginManager ) { config["max_label_characters"] = mMaxLabelCharacters; updateConfigFile = true; } + + if ( config.contains( "max_suggestion_documentation_width" ) ) + mMaxSuggestionDocumentationWidth = + config.value( "max_suggestion_documentation_width", "100%" ); + else { + config["max_suggestion_documentation_width"] = "100%"; + updateConfigFile = true; + } + + if ( mMaxSuggestionDocumentationWidth.empty() ) + mMaxSuggestionDocumentationWidth = "100%"; } if ( mKeyBindings.empty() ) { @@ -1209,33 +1220,33 @@ void AutoCompletePlugin::postDraw( UICodeEditor* editor, const Vector2f& startSc } if ( mSuggestionIndex == (int)i && !suggestion.documentation.value.empty() ) { - mSuggestionDoc.setFont( editor->getFont() ); - mSuggestionDoc.setFontSize( editor->getFontSize() ); mSuggestionDoc.setFillColor( normalStyle.color ); mSuggestionDoc.setStyle( normalStyle.style ); - bool changed = mSuggestionDoc.setString( suggestion.documentation.value ); + mSuggestionDoc.setFont( editor->getFont() ); + mSuggestionDoc.setFontSize( editor->getFontSize() ); + mSuggestionDoc.setLineWrapMode( LineWrapMode::Word ); + mSuggestionDoc.setLineWrapKeepIndentation( true ); - Vector2f boxPos = { cursorPos.x + mBoxRect.getWidth(), - cursorPos.y + mRowHeight * count }; - Sizef boxSize = { mSuggestionDoc.getTextWidth() + mBoxPadding.Left + mBoxPadding.Right, - mSuggestionDoc.getTextHeight() + mBoxPadding.Top + - mBoxPadding.Bottom }; - primitives.setColor( - Color( selectedStyle.background ).blendAlpha( editor->getAlpha() ) ); - primitives.drawRoundedRectangle( { boxPos, boxSize }, 0.f, Vector2f::One, 6 ); + Rectf docRect = + findBestDocumentationPlacement( editor, suggestion, boxRect, + { { cursorPos.x, cursorPos.y + mRowHeight * count }, + { mBoxRect.getWidth(), mRowHeight } }, + drawUp, lineHeight ); - if ( changed ) { - bool forceHTML = String::startsWith( suggestion.detail, "Emmet" ); - if ( suggestion.documentation.kind == LSPMarkupKind::MarkDown || forceHTML ) { - const auto& syntaxDef = - forceHTML ? SyntaxDefinitionManager::instance()->getByLSPName( "html" ) - : SyntaxDefinitionManager::instance()->getByLSPName( "markdown" ); - SyntaxTokenizer::tokenizeText( syntaxDef, editor->getColorScheme(), - &mSuggestionDoc, 0, 0xFFFFFFFF, true, "\n\t " ); - } + if ( docRect.getSize().getWidth() > 0 && docRect.getSize().getHeight() > 0 ) { + primitives.setColor( + Color( selectedStyle.background ).blendAlpha( editor->getAlpha() ) ); + + editor->clipSmartEnable( docRect.Left, docRect.Top, docRect.getWidth(), + docRect.getHeight() ); + + primitives.drawRoundedRectangle( docRect, 0.f, Vector2f::One, 6 ); + + mSuggestionDoc.draw( docRect.Left + mBoxPadding.Left, + docRect.Top + mBoxPadding.Top ); + + editor->clipSmartDisable(); } - - mSuggestionDoc.draw( boxPos.x + mBoxPadding.Left, boxPos.y + mBoxPadding.Top ); } count++; visibleStrIndex++; @@ -1259,6 +1270,137 @@ void AutoCompletePlugin::postDraw( UICodeEditor* editor, const Vector2f& startSc (int)eefloor( bar.getWidth() * 0.5f ) ); } +Rectf AutoCompletePlugin::findBestDocumentationPlacement( UICodeEditor* editor, + const Suggestion& suggestion, + const Rectf& anchorBox, + const Rectf& rowRect, bool drawUp, + Float lineHeight ) { + const Rectf& areaRect = editor->getScreenRect(); + Float userMaxWidth = editor->convertLength( + StyleSheetLength( mMaxSuggestionDocumentationWidth ), editor->getPixelsSize().getWidth() ); + const Float minSideWidth = PixelDensity::dpToPx( 200.f ); + const Float margin = PixelDensity::dpToPx( 2.f ); // Visual breathing room + + struct PlacementCandidate { + enum Type { Right, Left, Bottom, Top } type; + Float availableWidth; + Float availableHeight; + Float score; + }; + + // Calculate available space, strictly removing the cursor's line so we don't cover what the + // user is typing. + Float topAvail = std::max( 0.f, ( drawUp ? ( anchorBox.Top - areaRect.Top - lineHeight ) + : ( anchorBox.Top - areaRect.Top ) ) - + margin ); + Float bottomAvail = + std::max( 0.f, ( drawUp ? ( areaRect.Bottom - anchorBox.Bottom ) + : ( areaRect.Bottom - anchorBox.Bottom - lineHeight ) ) - + margin ); + Float rightAvail = std::max( 0.f, areaRect.Right - anchorBox.Right - margin ); + Float leftAvail = std::max( 0.f, anchorBox.Left - areaRect.Left - margin ); + + SmallVector candidates = { + { PlacementCandidate::Right, rightAvail, std::max( 0.f, areaRect.getHeight() - margin * 2 ), + 0 }, + { PlacementCandidate::Left, leftAvail, std::max( 0.f, areaRect.getHeight() - margin * 2 ), + 0 }, + { PlacementCandidate::Bottom, std::max( 0.f, areaRect.getWidth() - margin * 2 ), + bottomAvail, 0 }, + { PlacementCandidate::Top, std::max( 0.f, areaRect.getWidth() - margin * 2 ), topAvail, + 0 } }; + + for ( auto& c : candidates ) { + Float maxW = + std::min( userMaxWidth, c.availableWidth - mBoxPadding.Left - mBoxPadding.Right ); + if ( maxW < 50 || c.availableHeight < mRowHeight * 2 ) { + c.score = 0; + continue; + } + c.score = maxW * std::min( c.availableHeight, mRowHeight * 10 ); + if ( ( c.type == PlacementCandidate::Right || c.type == PlacementCandidate::Left ) && + c.availableWidth >= minSideWidth ) { + c.score += 1000000; // Prefer side placement if it has enough width + } + } + + std::sort( candidates.begin(), candidates.end(), + []( const auto& a, const auto& b ) { return a.score > b.score; } ); + + const auto& best = candidates.front(); + + // Edge case: No space anywhere on screen (zero score) + if ( best.score == 0 ) { + return Rectf(); // Return empty rect, `postDraw` will skip drawing + } + + Float maxWidth = std::max( + 0.f, std::min( userMaxWidth, best.availableWidth - mBoxPadding.Left - mBoxPadding.Right ) ); + + mSuggestionDoc.setMaxWrapWidth( maxWidth ); + bool changed = mSuggestionDoc.setString( suggestion.documentation.value ); + + if ( changed ) { + bool forceHTML = String::startsWith( suggestion.detail, "Emmet" ); + if ( suggestion.documentation.kind == LSPMarkupKind::MarkDown || forceHTML ) { + const auto& syntaxDef = + forceHTML ? SyntaxDefinitionManager::instance()->getByLSPName( "html" ) + : SyntaxDefinitionManager::instance()->getByLSPName( "markdown" ); + SyntaxTokenizer::tokenizeText( syntaxDef, editor->getColorScheme(), &mSuggestionDoc, 0, + 0xFFFFFFFF, true, "\n\t " ); + } + } + + Sizef boxSize = { mSuggestionDoc.getTextWidth() + mBoxPadding.Left + mBoxPadding.Right, + mSuggestionDoc.getTextHeight() + mBoxPadding.Top + mBoxPadding.Bottom }; + + // Height Clamping: Prevent background box from bleeding off-screen. + // Text that overflows this will simply get clipped or run over gracefully. + boxSize.setHeight( std::min( boxSize.getHeight(), best.availableHeight ) ); + + Vector2f pos; + if ( best.type == PlacementCandidate::Right ) { + pos.x = anchorBox.Right + margin; + pos.y = rowRect.Top; + } else if ( best.type == PlacementCandidate::Left ) { + pos.x = anchorBox.Left - boxSize.getWidth() - margin; + pos.y = rowRect.Top; + } else if ( best.type == PlacementCandidate::Bottom ) { + pos.x = anchorBox.Left; + pos.y = anchorBox.Bottom + margin + ( !drawUp ? lineHeight : 0 ); + } else { // Top + pos.x = anchorBox.Left; + pos.y = anchorBox.Top - boxSize.getHeight() - margin - ( drawUp ? lineHeight : 0 ); + } + + // Final Clamping: Kept firmly inside `areaRect` but enforcing boundaries to prevent overlap + if ( best.type == PlacementCandidate::Right ) { + pos.x = std::min( pos.x, areaRect.Right - boxSize.getWidth() ); + pos.x = std::max( pos.x, anchorBox.Right + margin ); + pos.y = std::max( areaRect.Top + margin, + std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin ) ); + } else if ( best.type == PlacementCandidate::Left ) { + pos.x = std::max( pos.x, areaRect.Left + margin ); + pos.x = std::min( pos.x, anchorBox.Left - boxSize.getWidth() - margin ); + pos.y = std::max( areaRect.Top + margin, + std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin ) ); + } else if ( best.type == PlacementCandidate::Bottom ) { + pos.y = std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin ); + Float minBottom = anchorBox.Bottom + margin + ( !drawUp ? lineHeight : 0 ); + pos.y = std::max( pos.y, minBottom ); + pos.x = std::max( areaRect.Left + margin, + std::min( pos.x, areaRect.Right - boxSize.getWidth() - margin ) ); + } else { // Top + pos.y = std::max( pos.y, areaRect.Top + margin ); + Float maxTop = anchorBox.Top - boxSize.getHeight() - margin - ( drawUp ? lineHeight : 0 ); + pos.y = std::min( pos.y, maxTop ); + pos.x = std::max( areaRect.Left + margin, + std::min( pos.x, areaRect.Right - boxSize.getWidth() - margin ) ); + } + + return Rectf( pos, boxSize ).round(); +} + bool AutoCompletePlugin::onMouseDown( UICodeEditor* editor, const Vector2i& position, const Uint32& flags ) { if ( mSuggestions.empty() || !mSuggestionsEditor || mSuggestionsEditor != editor || diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp index 0a2631cd7..e33512279 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp @@ -45,7 +45,7 @@ class AutoCompletePlugin : public Plugin { sortText( sortText.empty() ? std::string{ this->text } : std::move( sortText ) ), range( range ), insertText( std::move( insertText ) ), - documentation( doc ){}; + documentation( doc ) {}; bool operator<( const Suggestion& other ) const { return getCmpStr() < other.getCmpStr(); } @@ -173,6 +173,7 @@ class AutoCompletePlugin : public Plugin { String::HashType mConfigHash{ 0 }; std::unordered_map mKeyBindings; std::unordered_map mShortcuts; + std::string mMaxSuggestionDocumentationWidth; Float mRowHeight{ 0 }; Rectf mBoxRect; @@ -220,6 +221,10 @@ class AutoCompletePlugin : public Plugin { void tryStartSnippetNav( const Suggestion& suggestion, UICodeEditor* editor, const TextRanges& prevSels ); + Rectf findBestDocumentationPlacement( UICodeEditor* editor, const Suggestion& suggestion, + const Rectf& anchorBox, const Rectf& rowRect, bool drawUp, + Float lineHeight ); + void updateShortcuts(); };