diff --git a/escher/include/escher/text_area.h b/escher/include/escher/text_area.h index ffc65b4bc..e8aa34aa5 100644 --- a/escher/include/escher/text_area.h +++ b/escher/include/escher/text_area.h @@ -138,6 +138,7 @@ private: TextAreaDelegate * m_delegate; // Due to rect size limitation, the editor cannot display more than 1800 lines constexpr static int k_maxLines = 999; + constexpr static int k_maxLineChars = 3000; }; #endif diff --git a/escher/src/text_area.cpp b/escher/src/text_area.cpp index bbddcb700..72a209a46 100644 --- a/escher/src/text_area.cpp +++ b/escher/src/text_area.cpp @@ -47,24 +47,79 @@ bool TextArea::handleEventWithText(const char * text, bool indentation, bool for * indentation, stop here. */ int spacesCount = 0; int totalIndentationSize = 0; - int textLen = strlen(text); + int addedTextLength = strlen(text); + size_t previousTextLength = contentView()->getText()->textLength(); char * insertionPosition = const_cast(cursorLocation()); + const char * textAreaBuffer = contentView()->text(); if (indentation) { // Compute the indentation spacesCount = indentationBeforeCursor(); - const char * textAreaBuffer = contentView()->text(); if (insertionPosition > textAreaBuffer && UTF8Helper::PreviousCodePointIs(textAreaBuffer, insertionPosition, ':')) { spacesCount += k_indentationSpaces; } // Check the text will not overflow the buffer totalIndentationSize = UTF8Helper::CountOccurrences(text, '\n') * spacesCount; - if (contentView()->getText()->textLength() + textLen + totalIndentationSize >= contentView()->getText()->bufferSize()) { + if (previousTextLength + addedTextLength + totalIndentationSize >= contentView()->getText()->bufferSize()) { return false; } } - // Check the text will not overflow the max number of lines - if (contentView()->getText()->textLineTotal() + UTF8Helper::CountOccurrences(text, '\n') >= k_maxLines) { + /* KDCoordinate is a int16. We must limit the number of characters per line, + * and lines per scripts, otherwise the line rects or content rect + * height/width can overflow int16, which results in weird visual effects.*/ + // 1 - Number of Characters per line : + if (previousTextLength + addedTextLength > k_maxLineChars) { + /* Only check for long lines in long scripts. PreviousTextLength and + * addedTextLength being greater than the actual number of glyphs is not an + * issue here. After insertion, text buffer will have this structure : + * ".../n"+"before"+"inserted1"+("/n.../n")?+"inserted2"+"after"+"\n..." + * Lengths : b ib ia a + * As maxBufferSize is lower than k_maxLineChars, there is no need to check + * for inserted lines between "\n...\n" */ + static_assert(TextField::maxBufferSize() < k_maxLineChars, "Pasting text might cause content rect overflow."); + + // Counting line text lengths before and after insertion. + int b = 0; + int a = 0; + UTF8Helper::countGlyphsInLine(textAreaBuffer, &b, &a, insertionPosition); + + if (a + b + addedTextLength > k_maxLineChars) { + /* Overflow expected, depending on '/n' code point presence and position. + * Counting : Glyphs inserted before first '/n' : ib + * Glyphs inserted after last '/n' : ia + * Number of '/n' : n */ + int glyphCount[3] = {0, 0, 0}; + UTF8Helper::PerformAtCodePoints(text, '\n', + [](int, void * intArray, int, int) { + // '\n' found, Increment n + int * n = (int *)intArray + 2; + *n = *n + 1; + // Reset ia + int * ia = (int *)intArray + 1; + *ia = 0; + }, + [](int, void * intArray, int, int) { + if (((int *)intArray)[2] == 0) { + // While no '\n' found, increment ib + int * ib = (int *)intArray; + *ib = *ib + 1; + } else { + // Increment ia + int * ia = (int *)intArray + 1; + *ia = *ia + 1; + } + }, + &glyphCount, 0, 0); + // Insertion is not possible if one of the produced line is too long. + if ((glyphCount[2] == 0 && a + glyphCount[0] + b > k_maxLineChars) || b + glyphCount[0] > k_maxLineChars || a + glyphCount[1] > k_maxLineChars) { + return false; + } + } + } + + // 2 - Total number of line : + if (previousTextLength + addedTextLength > k_maxLines && contentView()->getText()->textLineTotal() + UTF8Helper::CountOccurrences(text, '\n') > k_maxLines) { + // Only check for overflowed lines in long scripts to save computation return false; } @@ -89,9 +144,9 @@ bool TextArea::handleEventWithText(const char * text, bool indentation, bool for UCodePointNull, true, nullptr, - insertionPosition + textLen); + insertionPosition + addedTextLength); } - const char * endOfInsertedText = insertionPosition + textLen + totalIndentationSize; + const char * endOfInsertedText = insertionPosition + addedTextLength + totalIndentationSize; const char * cursorPositionInCommand = TextInputHelpers::CursorPositionInCommand(insertionPosition, endOfInsertedText); // Remove the Empty code points @@ -268,9 +323,16 @@ CodePoint TextArea::Text::removePreviousGlyph(char * * position) { assert(m_buffer <= *position && *position < m_buffer + m_bufferSize); CodePoint removedCodePoint = 0; - int removedSize = UTF8Helper::RemovePreviousGlyph(m_buffer, *position, &removedCodePoint); - assert(removedSize > 0); - + int removedSize = 0; + if (UTF8Helper::PreviousCodePoint(m_buffer, *position) == '\n') { + // See comments in handleEventWithText about max number of glyphs per line + removedCodePoint = '\n'; + // removeText will handle max number of glyphs per line + removedSize = removeText(*position-1, *position); + } else { + removedSize = UTF8Helper::RemovePreviousGlyph(m_buffer, *position, &removedCodePoint); + assert(removedSize > 0); + } // Set the new cursor position *position = *position - removedSize; return removedCodePoint; @@ -288,6 +350,24 @@ size_t TextArea::Text::removeText(const char * start, const char * end) { return 0; } + /* Removing text can increase line length. See comments in handleEventWithText + * about max number of glyphs per line. */ + if (textLength() - delta >= k_maxLineChars) { + /* Only check for line length on long enough scripts. TextLength() and delta + * being greater than the actual number of glyphs is not an issue here. */ + + // Counting text lengths between previous and last '/n' (non removed). + int b = 0; + int a = 0; + UTF8Helper::countGlyphsInLine(text(), &b, &a, start, end); + + if (a + b > k_maxLineChars) { + // Resulting line would exceed limits, no text is removed + // TODO error message: Add Message to explain failure to remove text + return 0; + } + } + for (size_t index = src - m_buffer; index < m_bufferSize; index++) { *dst = *src; if (*src == 0) { @@ -551,6 +631,9 @@ KDRect TextArea::ContentView::glyphFrameAtPosition(const char * text, const char assert(found); (void) found; + // Check for KDCoordinate overflow + assert(x < KDCOORDINATE_MAX - glyphSize.width() && p.line() * glyphSize.height() < KDCOORDINATE_MAX - glyphSize.height()); + return KDRect( x, p.line() * glyphSize.height(), diff --git a/escher/src/text_input.cpp b/escher/src/text_input.cpp index ccf943ddd..da741f62c 100644 --- a/escher/src/text_input.cpp +++ b/escher/src/text_input.cpp @@ -145,7 +145,9 @@ void TextInput::scrollToCursor() { * In order to avoid requiring two layouts, we allow overscrolling in * scrollToContentRect, and the last layout of the scroll view corrects the * size of the scroll view only once. */ - scrollToContentRect(contentView()->cursorRect(), true); + KDRect cursorRect = contentView()->cursorRect(); + assert(cursorRect.top() >= 0 && cursorRect.right() >= 0 && cursorRect.bottom() >= 0 && cursorRect.left() >= 0); + scrollToContentRect(cursorRect, true); } void TextInput::deleteSelection() { @@ -201,7 +203,7 @@ bool TextInput::moveCursorLeft(int step) { } i++; } - // true is returned if there was at least one successful cursor mouvement + // true is returned if there was at least one successful cursor movement return (i > 1 || canMove); } @@ -218,7 +220,7 @@ bool TextInput::moveCursorRight(int step) { } i++; } - // true is returned if there was at least one successful cursor mouvement + // true is returned if there was at least one successful cursor movement return (i > 1 || canMove); } diff --git a/ion/include/ion/unicode/utf8_helper.h b/ion/include/ion/unicode/utf8_helper.h index 17ff9385f..96404b863 100644 --- a/ion/include/ion/unicode/utf8_helper.h +++ b/ion/include/ion/unicode/utf8_helper.h @@ -89,6 +89,9 @@ const char * BeginningOfWord(const char * text, const char * word); // Returns the position of the first following char ' ', '\n' or 0 const char * EndOfWord(const char * word); +// On a line, count number of glyphs before and after locations +void countGlyphsInLine(const char * text, int * before, int * after, const char * beforeLocation, const char *afterLocation = nullptr); + }; #endif diff --git a/ion/src/shared/unicode/utf8_helper.cpp b/ion/src/shared/unicode/utf8_helper.cpp index d15c897c6..e3ae93fba 100644 --- a/ion/src/shared/unicode/utf8_helper.cpp +++ b/ion/src/shared/unicode/utf8_helper.cpp @@ -407,4 +407,18 @@ const char * EndOfWord(const char * word) { return result; } +void countGlyphsInLine(const char * text, int * before, int * after, const char * beforeLocation, const char *afterLocation) { + UTF8Helper::CodePointAction countGlyph = [](int, void * glyphCount, int, int) { + int * castedCount = (int *) glyphCount; + *castedCount = *castedCount + 1; + }; + // Count glyphs before + UTF8Helper::PerformAtCodePoints(text, UCodePointLineFeed, nullptr, countGlyph, before, 0, 0, UCodePointLineFeed, false, beforeLocation); + if (afterLocation == nullptr) { + afterLocation = beforeLocation; + } + // Count glyphs after + UTF8Helper::PerformAtCodePoints(afterLocation, UCodePointLineFeed, nullptr, countGlyph, after, 0, 0, UCodePointLineFeed); +} + } diff --git a/kandinsky/src/context_text.cpp b/kandinsky/src/context_text.cpp index 8f36b6247..c1fd73a6e 100644 --- a/kandinsky/src/context_text.cpp +++ b/kandinsky/src/context_text.cpp @@ -28,6 +28,7 @@ KDPoint KDContext::pushOrPullString(const char * text, KDPoint p, const KDFont * while (codePoint != UCodePointNull && (maxByteLength < 0 || codePointPointer < text + maxByteLength)) { codePointPointer = decoder.stringPosition(); if (codePoint == UCodePointLineFeed) { + assert(position.y() < KDCOORDINATE_MAX - glyphSize.height()); position = KDPoint(0, position.y() + glyphSize.height()); codePoint = decoder.nextCodePoint(); } else if (codePoint == UCodePointTabulation) { diff --git a/kandinsky/src/font.cpp b/kandinsky/src/font.cpp index ce4fb81fd..cae40c789 100644 --- a/kandinsky/src/font.cpp +++ b/kandinsky/src/font.cpp @@ -30,6 +30,7 @@ KDSize KDFont::stringSizeUntil(const char * text, const char * limit) const { currentStringPosition = decoder.stringPosition(); codePoint = decoder.nextCodePoint(); } + assert(stringSize.width() >= 0 && stringSize.height() >= 0); return stringSize; } diff --git a/kandinsky/src/point.cpp b/kandinsky/src/point.cpp index 796232558..f6a096de4 100644 --- a/kandinsky/src/point.cpp +++ b/kandinsky/src/point.cpp @@ -1,6 +1,10 @@ #include +#include KDPoint KDPoint::translatedBy(KDPoint other) const { + assert((other.x() >= 0 && m_x <= KDCOORDINATE_MAX - other.x()) || (other.x() < 0 && m_x >= KDCOORDINATE_MIN - other.x())); + assert((other.y() >= 0 && m_y <= KDCOORDINATE_MAX - other.y()) || (other.y() < 0 && m_y >= KDCOORDINATE_MIN - other.y())); + return KDPoint(m_x+other.x(), m_y+other.y()); }