Files
Upsilon/escher/src/text_area.cpp

469 lines
15 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 <stddef.h>
#include <assert.h>
#include <limits.h>
static inline const char * minPointer(const char * x, const char * y) { return x < y ? x : y; }
/* 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(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;
}
/* Compute the indentation. If the text cannot be inserted with the
* indentation, stop here. */
int spacesCount = 0;
int totalIndentationSize = 0;
int textLen = strlen(text);
char * insertionPosition = const_cast<char *>(cursorLocation());
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()) {
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 + textLen);
}
const char * endOfInsertedText = insertionPosition + textLen + 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;
} else if (handleBoxEvent(app(), event)) {
return true;
} else if (event == Ion::Events::Left) {
return TextInput::moveCursorLeft();
} else if (event == Ion::Events::Right) {
return TextInput::moveCursorRight();
} else if (event == Ion::Events::Up) {
contentView()->moveCursorGeo(0, -1);
} else if (event == Ion::Events::Down) {
contentView()->moveCursorGeo(0, 1);
} else if (event == Ion::Events::ShiftLeft) {
contentView()->moveCursorGeo(-INT_MAX/2, 0);
} else if (event == Ion::Events::ShiftRight) {
contentView()->moveCursorGeo(INT_MAX/2, 0);
} else if (event == Ion::Events::Backspace) {
return removePreviousGlyph();
} else if (event.hasText()) {
return handleEventWithText(event.text());
} else if (event == Ion::Events::EXE) {
return handleEventWithText("\n");
} else if (event == Ion::Events::Clear) {
if (!contentView()->removeEndOfLine()) {
contentView()->removeStartOfLine();
}
} else if (event == Ion::Events::Paste) {
return handleEventWithText(Clipboard::sharedClipboard()->storedText());
} 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 minPointer(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);
size_t sizeToMove = strlen(location) + 1;
assert(location + textLength + sizeToMove <= m_buffer + m_bufferSize);
memmove(location + textLength, location, sizeToMove);
memmove(location, s, 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 = UTF8Helper::RemovePreviousGlyph(m_buffer, *position, &removedCodePoint);
assert(removedSize > 0);
// Set the new cursor position
*position = *position - removedSize;
return removedCodePoint;
}
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();
}
}
char * dst = const_cast<char* >(direction > 0 ? location : codePointPosition);
char * src = const_cast<char* >(direction > 0 ? codePointPosition : location);
assert(src >= dst);
size_t delta = src - dst;
if (delta == 0) {
return 0;
}
for (size_t index = src - m_buffer; index < m_bufferSize; index++) {
*dst = *src;
if (*src == 0) {
return delta;
}
dst++;
src++;
}
assert(false);
return 0;
}
/* 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());
}
y++;
}
}
void TextArea::ContentView::drawStringAt(KDContext * ctx, int line, int column, const char * text, size_t length, KDColor textColor, KDColor backgroundColor) const {
KDSize glyphSize = m_font->glyphSize();
ctx->drawString(
text,
KDPoint(column*glyphSize.width(), line*glyphSize.height()),
m_font,
textColor,
backgroundColor,
length
);
}
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::TextArea::ContentView::setText(char * textBuffer, size_t textBufferSize) {
m_text.setText(textBuffer, textBufferSize);
m_cursorLocation = text();
}
bool TextArea::TextArea::ContentView::insertTextAtLocation(const char * text, const char * location) {
int textSize = strlen(text);
if (m_text.textLength() + textSize >= m_text.bufferSize() || textSize == 0) {
return false;
}
bool lineBreak = false;
// Scan for \n and 0
const char * nullLocation = UTF8Helper::PerformAtCodePoints(
text, '\n',
[](int codePointOffset, void * lineBreak, int context1, int context2) {
*((bool *)lineBreak) = true;
},
[](int c1, void * c2, int c3, int c4) { },
&lineBreak, 0);
assert(UTF8Helper::CodePointIs(nullLocation, 0));
m_text.insertText(text, nullLocation - text, const_cast<char *>(location));
reloadRectFromPosition(location, lineBreak);
return true;
}
bool TextArea::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;
}
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;
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)));
}