Skip to content

Commit

Permalink
libshell: Revised string manipulation (text editor)
Browse files Browse the repository at this point in the history
Byte vs. character offsets/iteration.
  • Loading branch information
skyjake committed Sep 1, 2019
1 parent 71ebff3 commit b7dca68
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 64 deletions.
22 changes: 12 additions & 10 deletions doomsday/libs/shell/include/de/shell/libshell.h
Expand Up @@ -68,10 +68,12 @@ inline Address checkPort(Address const &address)
struct LIBSHELL_PUBLIC WrappedLine
{
String::ByteRange range;
bool isFinal;
String::CharPos width;
bool isFinal;

WrappedLine(const String::ByteRange &range_, bool final = false)
: range(range_)
WrappedLine(const String::ByteRange &range, const String::CharPos &width, bool final = false)
: range(range)
, width(width)
, isFinal(final)
{}
};
Expand All @@ -81,19 +83,19 @@ class LIBSHELL_PUBLIC ILineWrapping
public:
virtual ~ILineWrapping() {}

virtual bool isEmpty() const = 0;
virtual void clear() = 0;
virtual void wrapTextToWidth(String const &text, String::CharPos maxWidth) = 0;
virtual WrappedLine line(int index) const = 0;
virtual bool isEmpty() const = 0;
virtual void clear() = 0;
virtual void wrapTextToWidth(String const &text, String::CharPos maxWidth) = 0;
virtual WrappedLine line(int index) const = 0;

/// Determines the visible maximum width of the wrapped content.
virtual int width() const = 0;
virtual String::CharPos width() const = 0;

/// Determines the number of lines in the wrapped content.
virtual int height() const = 0;

/// Returns the advance width of the range.
virtual BytePos rangeWidth(const String::ByteRange &range) const = 0;
virtual String::CharPos rangeWidth(const String::ByteRange &range) const = 0;

/**
* Calculates which index in the provided content range occupies a
Expand All @@ -102,7 +104,7 @@ class LIBSHELL_PUBLIC ILineWrapping
* @param range Range within the content.
* @param width Advance width to check.
*/
virtual BytePos indexAtWidth(const String::ByteRange &range, BytePos width) const = 0;
virtual BytePos indexAtWidth(const String::ByteRange &range, String::CharPos width) const = 0;
};

} // namespace shell
Expand Down
11 changes: 6 additions & 5 deletions doomsday/libs/shell/include/de/shell/monospacelinewrapping.h
Expand Up @@ -49,13 +49,14 @@ class LIBSHELL_PUBLIC MonospaceLineWrapping : public ILineWrapping
*/
void wrapTextToWidth(String const &text, String::CharPos maxWidth);

WrappedLine line(int index) const { return _lines[index]; }
int width() const;
int height() const;
BytePos rangeWidth(const String::ByteRange &range) const;
BytePos indexAtWidth(const String::ByteRange &range, BytePos width) const;
WrappedLine line(int index) const { return _lines[index]; }
String::CharPos width() const;
int height() const;
String::CharPos rangeWidth(const String::ByteRange &range) const;
BytePos indexAtWidth(const String::ByteRange &range, String::CharPos width) const;

private:
String _text;
List<WrappedLine> _lines;
};

Expand Down
55 changes: 31 additions & 24 deletions doomsday/libs/shell/src/abstractlineeditor.cpp
Expand Up @@ -62,6 +62,8 @@ DE_PIMPL(AbstractLineEditor)
completion.reset();
}

mb_iterator iterator(BytePos pos) const { return {text.data() + pos, text.data()}; }

WrappedLine lineSpan(int line) const
{
DE_ASSERT(line < wraps->height());
Expand Down Expand Up @@ -109,13 +111,18 @@ DE_PIMPL(AbstractLineEditor)
for (pos.line = 0; pos.line < wraps->height(); ++pos.line)
{
WrappedLine span = lineSpan(pos.line);
if (!span.isFinal) span.range.end--;
if (!span.isFinal)
{
span.range.end = (iterator(span.range.end) - 1).pos();
}
if (mark >= span.range.start && mark <= span.range.end)
{
// Stop here. Mark is on this line.
break;
}
pos.x -= (span.range.end - span.range.start + 1).index;
pos.x -= span.range.size().index;
pos.x = (iterator(pos.x) - 1).pos();
//pos.x -= (span.range.end - span.range.start + 1).index;
}
return pos;
}
Expand All @@ -133,8 +140,7 @@ DE_PIMPL(AbstractLineEditor)
DE_ASSERT(lineOff == 1 || lineOff == -1);

const LineBytePos linePos = lineCursorPos();
const BytePos destWidth =
wraps->rangeWidth(String::ByteRange(lineSpan(linePos.line).range.start, cursor));
const auto destWidth = wraps->rangeWidth({lineSpan(linePos.line).range.start, cursor});

// Check for no room.
if (!linePos.line && lineOff < 0) return false;
Expand All @@ -143,7 +149,11 @@ DE_PIMPL(AbstractLineEditor)
// Move cursor onto the adjacent line.
WrappedLine span = lineSpan(linePos.line + lineOff);
cursor = wraps->indexAtWidth(span.range, destWidth);
if (!span.isFinal) span.range.end--;
if (!span.isFinal)
{
// Step back by one character.
span.range.end = (iterator(span.range.end) - 1).pos();
}
if (cursor > span.range.end) cursor = span.range.end;

self().cursorMoved();
Expand All @@ -163,9 +173,10 @@ DE_PIMPL(AbstractLineEditor)
if (rejectCompletion())
return;

if (!text.isEmpty() && cursor > 0)
if (text && cursor > 0)
{
text.remove(--cursor, 1);
cursor = (iterator(cursor) - 1).pos();
text.remove(cursor, 1);
rewrapNow();
}
}
Expand All @@ -174,7 +185,7 @@ DE_PIMPL(AbstractLineEditor)
{
rejectCompletion();

if (!text.isEmpty() && cursor > 0)
if (text && cursor > 0)
{
auto to = wordJumpLeft(cursor);
text.remove(to, cursor - to);
Expand All @@ -198,7 +209,7 @@ DE_PIMPL(AbstractLineEditor)

if (cursor > 0)
{
--cursor;
cursor = (iterator(cursor) - 1).pos();
self().cursorMoved();
return true;
}
Expand All @@ -211,7 +222,7 @@ DE_PIMPL(AbstractLineEditor)

if (cursor < text.size())
{
++cursor;
cursor = (iterator(cursor) + 1).pos();
self().cursorMoved();
return true;
}
Expand All @@ -220,9 +231,7 @@ DE_PIMPL(AbstractLineEditor)

BytePos wordJumpLeft(BytePos pos) const
{
//pos = de::min(pos, text.sizeb() - 1);

mb_iterator iter{text.data() + de::min(pos, text.sizeb() - 1), text.data()};
auto iter = iterator(de::min(pos, text.sizeb() - 1));

// First jump over any non-word chars.
//while (pos > 0 && !text[pos].isLetterOrNumber()) pos--;
Expand All @@ -249,7 +258,7 @@ DE_PIMPL(AbstractLineEditor)
acceptCompletion();

const auto last = text.end();
mb_iterator iter{text.data() + cursor};
mb_iterator iter = iterator(cursor);

// If inside a word, jump to its end.
while (iter != last && iswalnum(*iter))
Expand Down Expand Up @@ -281,7 +290,11 @@ DE_PIMPL(AbstractLineEditor)
acceptCompletion();

WrappedLine const span = lineSpan(lineCursorPos().line);
cursor = span.range.end - (span.isFinal? 0 : 1);
cursor = span.range.end; // - (span.isFinal? 0 : 1);
if (!span.isFinal)
{
cursor = (iterator(cursor) - 1).pos();
}
self().cursorMoved();
}

Expand All @@ -294,14 +307,13 @@ DE_PIMPL(AbstractLineEditor)
bool suggestingCompletion() const
{
return suggesting;
//return completion.size > 0;
}

String wordBehindPos(BytePos pos) const
{
String word;
/// @todo Could alternatively find a range and do a single insertion...
mb_iterator iter{text.data() + pos, text.data()};
mb_iterator iter = iterator(pos);
--iter;
while (iter.pos() >= 0 && lexicon.isWordChar(*iter))
{
Expand Down Expand Up @@ -372,14 +384,9 @@ DE_PIMPL(AbstractLineEditor)
}
if (!suggestions.isEmpty())
{
completion.ordinal = -1; //(forwardCycle? 0 : suggestions.size() - 1);
/*String comp = suggestions[completion.ordinal];
comp.remove(0, base.size());*/
completion.ordinal = -1;
completion.pos = cursor;
completion.size = 0; //comp.size();
//text.insert(cursor, comp);
//cursor += completion.size;
//rewrapNow();
completion.size = 0;
suggesting = true;
// Notify immediately.
self().autoCompletionBegan(base);
Expand Down
2 changes: 1 addition & 1 deletion doomsday/libs/shell/src/labelwidget.cpp
Expand Up @@ -125,7 +125,7 @@ void LabelTextWidget::draw()

// Use the wrapped lines to determine width and height.
DE_ASSERT(!d->wraps.isEmpty());
Vec2i labelSize(d->wraps.width(), d->wraps.height());
Vec2i labelSize(d->wraps.width().index, d->wraps.height());

// Determine position of the label based on alignment.
Vec2i labelPos;
Expand Down
10 changes: 7 additions & 3 deletions doomsday/libs/shell/src/lineeditwidget.cpp
Expand Up @@ -65,8 +65,12 @@ LineEditTextWidget::LineEditTextWidget(de::String const &name)
Vec2i LineEditTextWidget::cursorPosition() const
{
de::Rectanglei pos = rule().recti();
/// @todo Must calculate CharPos on the line.
return pos.topLeft + Vec2i(prompt().size(), 0) + lineCursorPos();
// Calculate CharPos on the cursor's line.
const auto linePos = lineCursorPos();
const auto curLineSpan = lineWraps().line(linePos.line);
String::CharPos x = lineWraps().rangeWidth({curLineSpan.range.start, linePos.x});
int y = linePos.line;
return pos.topLeft + Vec2i(prompt().sizei(), 0) + Vec2i(x.index, y);
}

void LineEditTextWidget::viewResized()
Expand Down Expand Up @@ -104,7 +108,7 @@ void LineEditTextWidget::draw()
{
txt = String(txt.size(), '*');
}
buf.drawWrappedText(Vec2i(prompt().size(), 0), txt, lineWraps(), attr);
buf.drawWrappedText(Vec2i(prompt().sizei(), 0), txt, lineWraps(), attr);

targetCanvas().draw(buf, pos.topLeft);
}
Expand Down
47 changes: 28 additions & 19 deletions doomsday/libs/shell/src/monospacelinewrapping.cpp
Expand Up @@ -32,25 +32,28 @@ bool MonospaceLineWrapping::isEmpty() const
void MonospaceLineWrapping::clear()
{
_lines.clear();
_text.clear();
}

void MonospaceLineWrapping::wrapTextToWidth(String const &text, String::CharPos maxWidth)
void MonospaceLineWrapping::wrapTextToWidth(const String &text, String::CharPos maxWidth)
{
clear();
_text = text;

if (maxWidth < 1) return; // No room to wrap.

const Char newline = '\n';
const String::CharPos lineWidth = maxWidth;

dsize curLineWidth = 0;
mb_iterator begin = text.data();
const mb_iterator textEnd = text.data() + text.size();
for (;;)
{
String::CharPos curLineWidth{0};

// Newlines always cause a wrap.
mb_iterator end = begin;
while (curLineWidth < lineWidth.index && end != textEnd && *end != newline)
while (curLineWidth < lineWidth && end != textEnd && *end != newline)
{
++end;
++curLineWidth;
Expand All @@ -59,7 +62,7 @@ void MonospaceLineWrapping::wrapTextToWidth(String const &text, String::CharPos
if (end == textEnd)
{
// Time to stop.
_lines << WrappedLine({begin.pos(), text.sizeb()});
_lines << WrappedLine({begin.pos(), text.sizeb()}, curLineWidth);
break;
}

Expand All @@ -68,6 +71,7 @@ void MonospaceLineWrapping::wrapTextToWidth(String const &text, String::CharPos
while (!iswspace(*end))
{
--end;
--curLineWidth;
if (end == begin)
{
// Ran out of non-space chars, force a break.
Expand All @@ -79,13 +83,13 @@ void MonospaceLineWrapping::wrapTextToWidth(String const &text, String::CharPos
if (*end == newline)
{
// The newline will be omitted from the wrapped lines.
_lines << WrappedLine({begin.pos(), end.pos()});
_lines << WrappedLine({begin.pos(), end.pos()}, curLineWidth);
begin = end + 1;
}
else
{
if (iswspace(*end)) ++end;
_lines << WrappedLine({begin.pos(), end.pos()});
_lines << WrappedLine({begin.pos(), end.pos()}, curLineWidth);
begin = end;
}
}
Expand All @@ -94,34 +98,39 @@ void MonospaceLineWrapping::wrapTextToWidth(String const &text, String::CharPos
_lines.last().isFinal = true;
}

int MonospaceLineWrapping::width() const
String::CharPos MonospaceLineWrapping::width() const
{
dsize w = 0;
for (duint i = 0; i < _lines.size(); ++i)
String::CharPos w{0};
for (const auto &line : _lines)
{
WrappedLine const &span = _lines[i];
w = de::max(w, span.range.size().index);
w = de::max(w, line.width);
}
return int(w);
return w;
}

int MonospaceLineWrapping::height() const
{
return int(_lines.size());
return _lines.sizei();
}

BytePos MonospaceLineWrapping::rangeWidth(const String::ByteRange &range) const
String::CharPos MonospaceLineWrapping::rangeWidth(const String::ByteRange &range) const
{
return range.size();
String::CharPos w{0};
for (mb_iterator i = _text.data() + range.start, end = _text.data() + range.end; i < end; ++i)
{
++w;
}
return w;
}

BytePos MonospaceLineWrapping::indexAtWidth(const String::ByteRange &range, BytePos width) const
BytePos MonospaceLineWrapping::indexAtWidth(const String::ByteRange &range, String::CharPos width) const
{
if (width <= range.size())
mb_iterator i = _text.data() + range.start;
while (width-- > 0)
{
return range.start + width;
++i;
}
return range.end;
return i.pos();
}

}} // namespace de::shell
4 changes: 2 additions & 2 deletions doomsday/libs/shell/src/textcanvas.cpp
Expand Up @@ -230,11 +230,11 @@ void TextCanvas::drawWrappedText(const Vec2i & pos,
const AttribChar::Attribs &attribs,
const Alignment & lineAlignment)
{
int const width = wraps.width();
const int width = wraps.width().index;

for (int y = 0; y < wraps.height(); ++y)
{
WrappedLine const &span = wraps.line(y);
const WrappedLine span = wraps.line(y);
String part = text.substr(span.range);
int x = 0;
if (lineAlignment.testFlag(AlignRight))
Expand Down

0 comments on commit b7dca68

Please sign in to comment.