diff --git a/include/eepp/graphics/fontstyleconfig.hpp b/include/eepp/graphics/fontstyleconfig.hpp index 18178e596..7d26b7954 100644 --- a/include/eepp/graphics/fontstyleconfig.hpp +++ b/include/eepp/graphics/fontstyleconfig.hpp @@ -38,6 +38,8 @@ class FontStyleConfig { OutlineColor == other.OutlineColor; } + bool operator!=( const FontStyleConfig& other ) { return !( *this == other ); } + virtual void updateFontStyleConfig( const FontStyleConfig& fontStyleConfig ) { Font = fontStyleConfig.Font; Style = fontStyleConfig.Style; diff --git a/include/eepp/ui/linewrapping.hpp b/include/eepp/ui/linewrapping.hpp index 5495eabcc..58e2fdd52 100644 --- a/include/eepp/ui/linewrapping.hpp +++ b/include/eepp/ui/linewrapping.hpp @@ -10,16 +10,27 @@ using namespace EE::UI::Doc; namespace EE { namespace UI { -enum class LineWrapMode { Letter, Word }; +enum class LineWrapMode { NoWrap, Letter, Word }; struct LineWrapInfo { - std::vector wraps; - Float paddingStart; - bool hasWraps() const { return !wraps.empty(); } + std::vector wraps; + Float paddingStart{ 0.f }; }; -class LineWrapping { +class EE_API LineWrapping { public: + struct Config { + LineWrapMode mode{ LineWrapMode::NoWrap }; + bool keepIndentation{ true }; + Uint32 tabWidth{ 4 }; + std::optional maxCharactersWidth; + bool operator==( const Config& other ) { + return mode == other.mode && keepIndentation == other.keepIndentation && + tabWidth == other.tabWidth && maxCharactersWidth == other.maxCharactersWidth; + } + bool operator!=( const Config& other ) { return !(*this == other); } + }; + static LineWrapInfo computeLineBreaks( const String& string, const FontStyleConfig& fontStyle, Float maxWidth, LineWrapMode mode, bool keepIndentation, Uint32 tabWidth = 4 ); @@ -29,10 +40,32 @@ class LineWrapping { LineWrapMode mode, bool keepIndentation, Uint32 tabWidth = 4 ); + LineWrapping( std::shared_ptr doc, FontStyleConfig fontStyle, Config config ); + + size_t getTotalLines() const; + + const Config& config() const { return mConfig; } + + void reconstructBreaks(); + + void updateBreaks( Int64 fromLine, Int64 toLine, Int64 numLines ); + + void setConfig( Config config ); + + void setMaxWidth( Float maxWidth ); + + void setFontStyle( FontStyleConfig fontStyle ); + protected: + std::shared_ptr mDoc; + FontStyleConfig mFontStyle; + Config mConfig; Float mMaxWidth{ 0 }; - std::unordered_map mWrappedLines; - std::unordered_map mWrappedLineToIndex; + std::vector mWrappedLines; + std::vector mWrappedLinesOffset; + std::vector mWrappedLineToIndex; + + Int64 toWrappedIndex( Int64 docIdx, bool retLast ); }; }} // namespace EE::UI diff --git a/src/eepp/ui/linewrapping.cpp b/src/eepp/ui/linewrapping.cpp index e59d628fd..8ab6d1438 100644 --- a/src/eepp/ui/linewrapping.cpp +++ b/src/eepp/ui/linewrapping.cpp @@ -10,7 +10,8 @@ LineWrapInfo LineWrapping::computeLineBreaks( const String& string, LineWrapMode mode, bool keepIndentation, Uint32 tabWidth ) { LineWrapInfo info; - if ( string.empty() || nullptr == fontStyle.Font ) + info.wraps.push_back( 0 ); + if ( string.empty() || nullptr == fontStyle.Font || mode == LineWrapMode::NoWrap ) return info; if ( keepIndentation ) { @@ -49,7 +50,7 @@ LineWrapInfo LineWrapping::computeLineBreaks( const String& string, else if ( ( curChar ) == '\r' ) w = 0; - if ( curChar != '\r' && !isMonospace ) { + if ( !isMonospace && curChar != '\r' ) { w += fontStyle.Font->getKerning( prevChar, curChar, fontStyle.CharacterSize, bold, italic, outlineThickness ); prevChar = curChar; @@ -82,13 +83,15 @@ LineWrapInfo LineWrapping::computeLineBreaks( const TextDocument& doc, size_t li const FontStyleConfig& fontStyle, Float maxWidth, LineWrapMode mode, bool keepIndentation, Uint32 tabWidth ) { - if ( nullptr == fontStyle.Font || fontStyle.Font->isMonospace() || - nullptr == doc.getHighlighter() || doc.getSyntaxDefinition().getPatterns().empty() ) { + if ( nullptr == fontStyle.Font || mode == LineWrapMode::NoWrap || + fontStyle.Font->isMonospace() || nullptr == doc.getHighlighter() || + doc.getSyntaxDefinition().getPatterns().empty() ) { return computeLineBreaks( doc.line( line ).getText(), fontStyle, maxWidth, mode, keepIndentation, tabWidth ); } LineWrapInfo info; + info.wraps.push_back( 0 ); const auto& string = doc.line( line ).getText(); const auto& tokens = doc.getHighlighter()->getLine( line ); @@ -167,4 +170,128 @@ LineWrapInfo LineWrapping::computeLineBreaks( const TextDocument& doc, size_t li return info; } +LineWrapping::LineWrapping( std::shared_ptr doc, FontStyleConfig fontStyle, + Config config ) : + mDoc( std::move( doc ) ), + mFontStyle( std::move( fontStyle ) ), + mConfig( std::move( config ) ) {} + +void LineWrapping::setMaxWidth( Float maxWidth ) { + if ( maxWidth != mMaxWidth ) { + mMaxWidth = maxWidth; + reconstructBreaks(); + } +} + +void LineWrapping::setFontStyle( FontStyleConfig fontStyle ) { + if ( fontStyle != mFontStyle ) { + mFontStyle = std::move( fontStyle ); + reconstructBreaks(); + } +} + +void LineWrapping::setConfig( Config config ) { + if ( config != mConfig ){ + mConfig = std::move( config ); + reconstructBreaks(); + } +} + +void LineWrapping::reconstructBreaks() { + if ( 0 == mMaxWidth ) + return; + + mWrappedLines.clear(); + mWrappedLineToIndex.clear(); + mWrappedLinesOffset.clear(); + + size_t linesCount = mDoc->linesCount(); + mWrappedLines.reserve( linesCount * 2 ); + mWrappedLinesOffset.reserve( linesCount ); + + for ( auto i = 0; i < linesCount; i++ ) { + auto lb = computeLineBreaks( *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, + mConfig.keepIndentation, mConfig.tabWidth ); + mWrappedLinesOffset.emplace_back( lb.paddingStart ); + for ( const auto& col : lb.wraps ) + mWrappedLines.emplace_back( i, col ); + } + + std::optional lastWrap; + size_t i = 0; + mWrappedLineToIndex.reserve( linesCount ); + + for ( const auto& wl : mWrappedLines ) { + if ( !lastWrap || *lastWrap != wl.line() ) { + mWrappedLineToIndex.emplace_back( i ); + lastWrap = wl.line(); + } + i++; + } +} + +Int64 LineWrapping::toWrappedIndex( Int64 docIdx, bool retLast ) { + auto idx = mWrappedLineToIndex[docIdx < 0 ? 0 + : ( docIdx >= mWrappedLineToIndex.size() + ? mWrappedLineToIndex.size() - 1 + : docIdx )]; + if ( retLast ) { + Int64 lastOfLine = mWrappedLines[idx].line(); + for ( auto i = idx + 1; i < mWrappedLines.size(); i++ ) { + if ( mWrappedLines[i].line() == lastOfLine ) + idx = i; + else + break; + } + } + return idx; +} + +void LineWrapping::updateBreaks( Int64 fromLine, Int64 toLine, Int64 numLines ) { + // Get affected wrapped range + Int64 oldIdxFrom = toWrappedIndex( fromLine, false ); + Int64 oldIdxTo = toWrappedIndex( toLine, true ); + + // Remove old wrapped lines + mWrappedLines.erase( mWrappedLines.begin() + oldIdxFrom, mWrappedLines.begin() + oldIdxTo ); + + // Remove old offsets + mWrappedLinesOffset.erase( mWrappedLinesOffset.begin() + fromLine, + mWrappedLinesOffset.begin() + toLine ); + + // Shift the line numbers + if ( numLines != 0 ) { + Int64 wrappedLines = mWrappedLines.size(); + for ( Int64 i = oldIdxFrom; i < wrappedLines; i++ ) + mWrappedLines[i].setLine( mWrappedLines[i].line() + numLines ); + } + + // Recompute line breaks + for ( auto i = fromLine; i <= toLine; i++ ) { + auto lb = computeLineBreaks( *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, + mConfig.keepIndentation, mConfig.tabWidth ); + mWrappedLinesOffset.insert( mWrappedLinesOffset.begin() + i, lb.paddingStart ); + Int64 loffset = 0; + for ( const auto& col : lb.wraps ) { + mWrappedLines.insert( mWrappedLines.begin() + i + loffset, { i, col } ); + loffset++; + } + } + + // Recompute wrapped line to index + Int64 line = fromLine; + Int64 wrappedLines = mWrappedLines.size(); + for ( Int64 wrappedIdx = oldIdxFrom; wrappedIdx < wrappedLines; wrappedIdx++ ) { + if ( mWrappedLines[wrappedIdx].column() == 0 ) { + mWrappedLineToIndex[line] = wrappedIdx; + line++; + } + } + mWrappedLineToIndex.resize( mDoc->linesCount() ); +} + +size_t LineWrapping::getTotalLines() const { + return mConfig.mode == LineWrapMode::NoWrap ? mDoc->linesCount() : mWrappedLines.size(); +} + }} // namespace EE::UI