Files
Upsilon/escher/src/text_area.cpp

690 lines
24 KiB
C++

#include <escher/text_area.h>
#include <escher/clipboard.h>
#include <escher/text_input_helpers.h>
#include <ion/unicode/utf8_decoder.h>
#include <ion/unicode/utf8_helper.h>
#include <poincare/serialization_helper.h>
#include <stddef.h>
#include <assert.h>
#include <limits.h>
#include <algorithm>
/* TextArea */
TextArea::TextArea(Responder * parentResponder, View * contentView, const KDFont * font) :
TextInput(parentResponder, contentView),
InputEventHandler(nullptr),
m_delegate(nullptr)
{
}
static inline void InsertSpacesAtLocation(int spacesCount, char * buffer, int bufferSize) {
assert(buffer != nullptr);
assert((int)(strlen(buffer) + spacesCount) < bufferSize);
size_t sizeToMove = strlen(buffer) + 1;
size_t spaceCharSize = UTF8Decoder::CharSizeOfCodePoint(' ');
size_t spacesLength = spacesCount * spaceCharSize;
memmove(buffer + spacesLength, buffer, sizeToMove);
for (int i = 0; i < spacesCount; i++) {
int spaceOffset = i * spaceCharSize;
UTF8Decoder::CodePointToChars(' ', buffer + spaceOffset, bufferSize - spaceOffset);
}
}
bool TextArea::handleEventWithText(const char * text, bool indentation, bool forceCursorRightOfText) {
if (*text == 0) {
return false;
}
// Delete the selected text if needed
if (!contentView()->selectionIsEmpty()) {
deleteSelection();
}
/* Compute the indentation. If the text cannot be inserted with the
* indentation, stop here. */
int spacesCount = 0;
int totalIndentationSize = 0;
int addedTextLength = strlen(text);
size_t previousTextLength = contentView()->getText()->textLength();
char * insertionPosition = const_cast<char *>(cursorLocation());
const char * textAreaBuffer = contentView()->text();
if (indentation) {
// Compute the indentation
spacesCount = indentationBeforeCursor();
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 (previousTextLength + addedTextLength + totalIndentationSize >= contentView()->getText()->bufferSize()) {
return false;
}
}
/* 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;
}
// Insert the text
if (!insertTextAtLocation(text, insertionPosition)) {
return true;
}
// Insert the indentation
if (indentation) {
UTF8Helper::PerformAtCodePoints(
insertionPosition,
'\n',
[](int codePointOffset, void * text, int indentation, int bufferLength) {
int offset = codePointOffset + UTF8Decoder::CharSizeOfCodePoint('\n');
InsertSpacesAtLocation(indentation, (char *)text + offset, bufferLength);
},
[](int c1, void * c2, int c3, int c4) {},
(void *)insertionPosition,
spacesCount,
contentView()->getText()->bufferSize() - (insertionPosition - contentView()->getText()->text()),
UCodePointNull,
true,
nullptr,
insertionPosition + addedTextLength);
}
const char * endOfInsertedText = insertionPosition + addedTextLength + totalIndentationSize;
const char * cursorPositionInCommand = TextInputHelpers::CursorPositionInCommand(insertionPosition, endOfInsertedText);
// Remove the Empty code points
UTF8Helper::RemoveCodePoint(insertionPosition, UCodePointEmpty, &cursorPositionInCommand, endOfInsertedText);
// Set the cursor location
const char * nextCursorLocation = forceCursorRightOfText ? endOfInsertedText : cursorPositionInCommand;
setCursorLocation(nextCursorLocation);
return true;
}
bool TextArea::handleEvent(Ion::Events::Event event) {
if (m_delegate != nullptr && m_delegate->textAreaDidReceiveEvent(this, event)) {
return true;
}
if (handleBoxEvent(event)) {
return true;
}
int step = Ion::Events::repetitionFactor();
if (event == Ion::Events::ShiftLeft || event == Ion::Events::ShiftRight) {
selectLeftRight(event == Ion::Events::ShiftLeft, false, step);
return true;
}
if (event == Ion::Events::ShiftUp || event == Ion::Events::ShiftDown) {
selectUpDown(event == Ion::Events::ShiftUp, 1);
return true;
} else if (event == Ion::Events::AlphaLeft) {
contentView()->moveCursorGeo(-INT_MAX/2, 0);
TextInput::scrollToCursor();
} else if (event == Ion::Events::AlphaRight) {
contentView()->moveCursorGeo(INT_MAX/2, 0);
TextInput::scrollToCursor();
} else if (event == Ion::Events::AlphaUp) {
contentView()->moveCursorGeo(0, -INT_MAX/2);
TextInput::scrollToCursor();
} else if (event == Ion::Events::AlphaDown) {
contentView()->moveCursorGeo(0, INT_MAX/2);
TextInput::scrollToCursor();
} else if (event == Ion::Events::Left || event == Ion::Events::Right) {
if (contentView()->resetSelection()) {
return true;
}
return (event == Ion::Events::Left) ?
TextInput::moveCursorLeft(step) :
TextInput::moveCursorRight(step);
}
if (event == Ion::Events::Left || event == Ion::Events::Right) {
if (contentView()->resetSelection()) {
return true;
}
return (event == Ion::Events::Left) ?
TextInput::moveCursorLeft(step) :
TextInput::moveCursorRight(step);
}
if (event.hasText()) {
return handleEventWithText(event.text());
}
if (event == Ion::Events::EXE) {
return handleEventWithText("\n");
}
if (event == Ion::Events::Copy || event == Ion::Events::Cut) {
if (contentView()->selectionIsEmpty()) {
return false;
}
const char * start = contentView()->selectionStart();
Clipboard::sharedClipboard()->store(start, contentView()->selectionEnd() - start);
if (event == Ion::Events::Cut) {
deleteSelection();
}
return true;
}
if (event == Ion::Events::Paste) {
return handleEventWithText(Clipboard::sharedClipboard()->storedText(), false, true);
}
// The following events need a scrollToCursor and return true
if (event == Ion::Events::Backspace) {
if (contentView()->selectionIsEmpty()) {
if (!removePreviousGlyph()) {
return false;
}
} else {
deleteSelection();
return true;
}
} else if (event == Ion::Events::Up || event == Ion::Events::Down) {
contentView()->resetSelection();
contentView()->moveCursorGeo(0, event == Ion::Events::Up ? -step : step);
} else if (event == Ion::Events::Clear) {
if (!contentView()->selectionIsEmpty()) {
deleteSelection();
return true;
} else if (!contentView()->removeEndOfLine()) {
contentView()->removeStartOfLine();
}
} else if (event == Ion::Events::Paste) {
return handleEventWithText(Clipboard::sharedClipboard()->storedText());
} else if (event == Ion::Events::Percent) {
return removePreviousGlyph();
} else if (event.hasText()) {
return handleEventWithText(event.text());
} else {
return false;
}
scrollToCursor();
return true;
}
void TextArea::setText(char * textBuffer, size_t textBufferSize) {
contentView()->setText(textBuffer, textBufferSize);
contentView()->moveCursorGeo(0, 0);
}
int TextArea::indentationBeforeCursor() const {
int indentationSize = 0;
/* Compute the number of spaces at the beginning of the line. Increase the
* indentation size when encountering spaces, reset it to 0 when encountering
* another code point, until reaching the beginning of the line. */
UTF8Helper::PerformAtCodePoints(const_cast<TextArea *>(this)->contentView()->text(), ' ',
[](int codePointOffset, void * indentationSize, int context1, int context2){
int * castedSize = (int *) indentationSize;
*castedSize = *castedSize + 1;
},
[](int codePointOffset, void * indentationSize, int context1, int context2){
*((int *) indentationSize) = 0;
},
&indentationSize, 0, -1, '\n', false, cursorLocation());
return indentationSize;
}
/* TextArea::Text */
const char * TextArea::Text::pointerAtPosition(Position p) {
assert(m_buffer != nullptr);
if (p.line() < 0) {
return m_buffer;
}
int y = 0;
for (Line l : *this) {
if (p.line() == y) {
const char * result = UTF8Helper::CodePointAtGlyphOffset(l.text(), p.column());
return std::min(result, l.text() + l.charLength());
}
y++;
}
return m_buffer + strlen(m_buffer);
}
TextArea::Text::Position TextArea::Text::positionAtPointer(const char * p) const {
assert(m_buffer != nullptr);
assert(m_buffer <= p && p < m_buffer + m_bufferSize);
size_t y = 0;
for (Line l : *this) {
if (l.contains(p)) {
size_t x = UTF8Helper::GlyphOffsetAtCodePoint(l.text(), p);
return Position(x, y);
}
y++;
}
assert(false);
return Position(0, 0);
}
void TextArea::Text::insertText(const char * s, int textLength, char * location) {
assert(m_buffer != nullptr);
assert(location >= m_buffer && location < m_buffer + m_bufferSize - 1);
assert(strlen(m_buffer) + textLength < m_bufferSize);
// assert the text to insert does not overlap the location where to insert
assert(s >= location || s + textLength < location);
/* The text to insert might be located after the insertion location, in which
* case we cannot simply do a memmove, as s will be shifted by the copy. */
bool noShift = (s + textLength < location) || (s > m_buffer + m_bufferSize);
size_t sizeToMove = strlen(location) + 1;
assert(location + textLength + sizeToMove <= m_buffer + m_bufferSize);
memmove(location + textLength, location, sizeToMove);
memmove(location, s + (noShift ? 0 : textLength), textLength);
}
void TextArea::Text::insertSpacesAtLocation(int numberOfSpaces, char * location) {
assert(m_buffer != nullptr);
assert(location >= m_buffer && location < m_buffer + m_bufferSize - 1);
assert(strlen(m_buffer) + numberOfSpaces < m_bufferSize);
size_t sizeToMove = strlen(location) + 1;
size_t spaceCharSize = UTF8Decoder::CharSizeOfCodePoint(' ');
size_t spacesSize = numberOfSpaces * spaceCharSize;
assert(location + spacesSize + sizeToMove <= m_buffer + m_bufferSize);
memmove(location + spacesSize, location, sizeToMove);
for (int i = 0; i < numberOfSpaces; i++) {
UTF8Decoder::CodePointToChars(' ', location+i*spaceCharSize, (m_buffer + m_bufferSize) - location);
}
}
CodePoint TextArea::Text::removePreviousGlyph(char * * position) {
assert(m_buffer != nullptr);
assert(m_buffer <= *position && *position < m_buffer + m_bufferSize);
CodePoint removedCodePoint = 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;
}
size_t TextArea::Text::removeText(const char * start, const char * end) {
assert(start <= end);
assert(start >= m_buffer && end <= m_buffer + m_bufferSize);
char * dst = const_cast<char* >(start);
char * src = const_cast<char* >(end);
size_t delta = src - dst;
if (delta == 0) {
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) {
assert(delta > 0);
return delta;
}
dst++;
src++;
}
assert(false);
return 0;
}
size_t TextArea::Text::removeRemainingLine(const char * location, int direction) {
assert(m_buffer != nullptr);
assert(location >= m_buffer && location <= m_buffer + m_bufferSize);
assert(direction > 0 || location > m_buffer);
assert(direction < 0 || location < m_buffer + m_bufferSize);
UTF8Decoder decoder(m_buffer, location);
const char * codePointPosition = decoder.stringPosition();
CodePoint nextCodePoint = direction > 0 ? decoder.nextCodePoint() : decoder.previousCodePoint();
if (direction < 0) {
codePointPosition = decoder.stringPosition();
}
while (nextCodePoint != '\n'
&& ((direction > 0 && nextCodePoint != 0)
|| (direction < 0 && codePointPosition > m_buffer)))
{
if (direction > 0) {
codePointPosition = decoder.stringPosition();
}
nextCodePoint = direction > 0 ? decoder.nextCodePoint() : decoder.previousCodePoint();
if (direction < 0) {
codePointPosition = decoder.stringPosition();
}
}
return removeText(direction > 0 ? location : codePointPosition, direction > 0 ? codePointPosition : location);
}
/* TextArea::Text::Line */
TextArea::Text::Line::Line(const char * text) :
m_text(text),
m_charLength(0)
{
if (m_text != nullptr) {
m_charLength = UTF8Helper::CodePointSearch(text, '\n') - m_text;
}
}
KDCoordinate TextArea::Text::Line::glyphWidth(const KDFont * const font) const {
return font->stringSizeUntil(m_text, m_text + m_charLength).width();
}
bool TextArea::Text::Line::contains(const char * c) const {
return (c >= m_text)
&& ((c < m_text + m_charLength)
|| (c == m_text + m_charLength
&& (UTF8Helper::CodePointIs(c, 0)
|| UTF8Helper::CodePointIs(c, '\n')))) ;
}
/* TextArea::Text::LineIterator */
TextArea::Text::LineIterator & TextArea::Text::LineIterator::operator++() {
const char * last = m_line.text() + m_line.charLength();
assert(UTF8Helper::CodePointIs(last, 0) || UTF8Helper::CodePointIs(last, '\n'));
assert(UTF8Decoder::CharSizeOfCodePoint('\n') == 1);
m_line = Line(UTF8Helper::CodePointIs(last, 0) ? nullptr : last + 1);
return *this;
}
/* TextArea::Text::Position */
KDSize TextArea::Text::span(const KDFont * const font) const {
assert(m_buffer != nullptr);
KDCoordinate width = 0;
int numberOfLines = 0;
for (Line l : *this) {
KDCoordinate lineWidth = l.glyphWidth(font);
if (lineWidth > width) {
width = lineWidth;
}
numberOfLines++;
}
return KDSize(width, numberOfLines * font->glyphSize().height());
}
/* TextArea::ContentView */
void TextArea::ContentView::drawRect(KDContext * ctx, KDRect rect) const {
// TODO: We're clearing areas we'll draw text over. It's not needed.
clearRect(ctx, rect);
KDSize glyphSize = m_font->glyphSize();
// We want to draw even partially visible characters. So we need to round
// down for the top left corner and up for the bottom right one.
Text::Position topLeft(
rect.x()/glyphSize.width(),
rect.y()/glyphSize.height()
);
Text::Position bottomRight(
rect.right()/glyphSize.width() + 1,
rect.bottom()/glyphSize.height() + 1
);
int y = 0;
for (Text::Line line : m_text) {
KDCoordinate width = line.glyphWidth(m_font);
if (y >= topLeft.line() && y <= bottomRight.line() && topLeft.column() < (int)width) {
drawLine(ctx, y, line.text(), line.charLength(), topLeft.column(), bottomRight.column(), m_selectionStart, m_selectionEnd);
}
y++;
}
}
void TextArea::ContentView::drawStringAt(KDContext * ctx, int line, int column, const char * text, int length, KDColor textColor, KDColor backgroundColor, const char * selectionStart, const char * selectionEnd, KDColor backgroundHighlightColor, bool isItalic) const {
if (length < 0) {
return;
}
const KDFont * usedFont = m_font;
if (isItalic) {
usedFont = m_font->toItalic();
}
KDSize glyphSize = usedFont->glyphSize();
bool drawSelection = selectionStart != nullptr && selectionEnd > text && selectionStart < text + length;
KDPoint nextPoint = ctx->drawString(
text,
KDPoint(column*glyphSize.width(), line*glyphSize.height()),
usedFont,
textColor,
backgroundColor,
drawSelection ? (selectionStart >= text ? std::min<KDCoordinate>(length, selectionStart - text) : 0) : length
);
if (!drawSelection) {
return;
}
const char * highlightedDrawStart = std::max(selectionStart, text);
size_t highlightedDrawLength = std::min(selectionEnd - highlightedDrawStart, length - (highlightedDrawStart - text));
nextPoint = ctx->drawString(
highlightedDrawStart,
nextPoint,
usedFont,
textColor,
backgroundHighlightColor,
highlightedDrawLength);
const char * notHighlightedDrawStart = highlightedDrawStart + highlightedDrawLength;
ctx->drawString(
notHighlightedDrawStart,
nextPoint,
usedFont,
textColor,
backgroundColor,
length - (notHighlightedDrawStart - text));
}
KDSize TextArea::ContentView::minimalSizeForOptimalDisplay() const {
KDSize span = m_text.span(m_font);
return KDSize(
/* We take into account the space required to draw a cursor at the end of
* line by adding glyphSize.width() to the width. */
span.width() + m_font->glyphSize().width(),
span.height()
);
}
void TextArea::ContentView::setText(char * textBuffer, size_t textBufferSize) {
m_text.setText(textBuffer, textBufferSize);
m_cursorLocation = text();
}
bool TextArea::ContentView::insertTextAtLocation(const char * text, char * location, int textLength) {
int textLen = textLength < 0 ? strlen(text) : textLength;
assert(textLen < 0 || textLen <= strlen(text));
if (m_text.textLength() + textLen >= m_text.bufferSize() || textLen == 0) {
return false;
}
// Scan for \n
bool lineBreak = UTF8Helper::HasCodePoint(text, '\n', text + textLen);
m_text.insertText(text, textLen, location);
// Replace System parentheses (used to keep layout tree structure) by normal parentheses
Poincare::SerializationHelper::ReplaceSystemParenthesesByUserParentheses(location, textLen);
reloadRectFromPosition(location, lineBreak);
return true;
}
bool TextArea::ContentView::removePreviousGlyph() {
if (cursorLocation() <= text()) {
assert(cursorLocation() == text());
return false;
}
bool lineBreak = false;
char * cursorLoc = const_cast<char *>(cursorLocation());
lineBreak = m_text.removePreviousGlyph(&cursorLoc) == '\n';
setCursorLocation(cursorLoc); // Update the cursor
layoutSubviews(); // Reposition the cursor
reloadRectFromPosition(cursorLocation(), lineBreak);
return true;
}
bool TextArea::ContentView::removeEndOfLine() {
size_t removedLine = m_text.removeRemainingLine(cursorLocation(), 1);
if (removedLine > 0) {
layoutSubviews();
reloadRectFromPosition(cursorLocation(), false);
return true;
}
return false;
}
bool TextArea::ContentView::removeStartOfLine() {
if (cursorLocation() <= text()) {
assert(cursorLocation() == text());
return false;
}
size_t removedLine = m_text.removeRemainingLine(cursorLocation(), -1);
if (removedLine > 0) {
assert(cursorLocation() >= text() + removedLine);
setCursorLocation(cursorLocation() - removedLine);
reloadRectFromPosition(cursorLocation(), true);
return true;
}
return false;
}
size_t TextArea::ContentView::removeText(const char * start, const char * end) {
return m_text.removeText(start, end);
}
size_t TextArea::ContentView::deleteSelection() {
assert(!selectionIsEmpty());
size_t removedLength = removeText(m_selectionStart, m_selectionEnd);
/* We cannot call resetSelection() because m_selectionStart and m_selectionEnd
* are invalid */
m_selectionStart = nullptr;
m_selectionEnd = nullptr;
return removedLength;
}
KDRect TextArea::ContentView::glyphFrameAtPosition(const char * text, const char * position) const {
assert(text == m_text.text());
KDSize glyphSize = m_font->glyphSize();
Text::Position p = m_text.positionAtPointer(position);
KDCoordinate x = 0;
bool found = false;
int y = 0;
for (Text::Line l : m_text) {
if (p.line() == y) {
x = m_font->stringSizeUntil(l.text(), position).width();
found = true;
break;
}
y++;
}
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(),
glyphSize.width(),
glyphSize.height()
);
}
void TextArea::ContentView::moveCursorGeo(int deltaX, int deltaY) {
Text::Position p = m_text.positionAtPointer(cursorLocation());
setCursorLocation(m_text.pointerAtPosition(Text::Position(p.column() + deltaX, p.line() + deltaY)));
}
void TextArea::selectUpDown(bool up, int step) {
const char * previousCursorLocation = contentView()->cursorLocation();
contentView()->moveCursorGeo(0, up ? -step : step);
const char * newCursorLocation = contentView()->cursorLocation();
contentView()->addSelection(up ? newCursorLocation : previousCursorLocation, up ? previousCursorLocation : newCursorLocation);
scrollToCursor();
}