Added a text area remake to replace the old and unreliable SimpleTextArea

This commit is contained in:
SMJSGaming 2024-09-01 18:22:58 +02:00
parent 7aa9e774c4
commit 047d55f263
10 changed files with 632 additions and 14 deletions

View file

@ -18,5 +18,6 @@
#include "ui/ScrollLayer.hpp"
#include "ui/SelectList.hpp"
#include "ui/Scrollbar.hpp"
#include "ui/TextArea.hpp"
#include "ui/TextAreaV2.hpp"
#include "ui/SimpleTextArea.hpp"
#include "ui/TextRenderer.hpp"

View file

@ -22,7 +22,7 @@ namespace geode {
*
* Contact me on Discord (\@smjs) if you have any questions, suggestions or bugs.
*/
class GEODE_DLL SimpleTextArea : public cocos2d::CCNode {
class GEODE_DLL [[deprecated("Use TextArea instead")]] SimpleTextArea : public cocos2d::CCNode {
public:
static SimpleTextArea* create(const std::string& text, const std::string& font = "chatFont.fnt", const float scale = 1);
static SimpleTextArea* create(const std::string& text, const std::string& font, const float scale, const float width);

View file

@ -0,0 +1,241 @@
#pragma once
#include <cocos2d.h>
#include "PaddingNode.hpp"
namespace geode {
class GEODE_DLL TextAreaV2 : public PaddingNode {
public:
enum class WrappingMode {
// Doesn't wrap the text and completely ignores bounds
NoWrap,
// Wraps the text on the last special character (spaces included) before the width is exceeded
WordWrap,
// Wraps the text on the last space before the width is exceeded
SpaceWrap,
// Wraps the text on the exact character that exceeds the width
CutoffWrap
};
enum class Alignment {
// Aligns the text to the left
Left,
// Aligns the text in the center
Center,
// Aligns the text to the right
Right
};
struct Line {
std::string text;
std::string overflow;
cocos2d::CCLabelBMFont* label;
int lineNumber;
float currentHeight;
bool isLastLine;
};
static TextAreaV2* create(const std::string& text, const std::string& font, const float scale = 1, const float width = -1, const float height = -1, const bool deferUpdates = false);
/**
* Sets the font of the text area
*/
void setFont(const std::string& font);
/**
* Gets the font of the text area
*/
std::string getFont() const;
/**
* Sets the text of the text area
*/
void setText(const std::string& text);
/**
* Gets the text of the text area
*/
std::string getText() const;
/**
* Sets the color of the text in the text area
*/
void setTextColor(const cocos2d::ccColor4B& color);
/**
* Gets the color of the text in the text area
*/
cocos2d::ccColor4B getTextColor() const;
/**
* Sets horizontal the alignment of the text in the text area
*/
void setAlignment(const Alignment alignment);
/**
* Gets the horizontal alignment of the text in the text area
*/
Alignment getAlignment() const;
/**
* Sets the wrapping mode of the text in the text area
*/
void setWrappingMode(const WrappingMode mode);
/**
* Gets the wrapping mode of the text in the text area
*/
WrappingMode getWrappingMode() const;
/**
* Sets the maximum number of lines in the text area
*
* @note If this is set to a value smaller than 0, it will stop keeping track of the lines and leave either all the lines or as many as the max height allows
*/
void setMaxLines(const int maxLines);
/**
* Gets the maximum number of lines in the text area
*/
int getMaxLines() const;
/**
* Sets the scale of the text in the text area
*/
void setTextScale(const float scale);
/**
* Gets the scale of the text in the text area
*/
float getTextScale() const;
/**
* Sets the padding between lines in the text area
*/
void setLinePadding(const float padding);
/**
* Gets the padding between lines in the text area
*/
float getLinePadding() const;
/**
* Sets a minimum bound for the width of the text area
*
* @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it at the max width
*/
void setMinWidth(const float width);
/**
* Resets the minimum width to prevent it from rescaling the container
*/
void resetMinWidth();
/**
* Gets the min width of the text area
*/
float getMinWidth() const;
/**
* Sets the max width of the text area to define the wrapping bounds
*
* @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it as big as the text or the min width
*/
void setMaxWidth(const float width);
/**
* Resets the maximum width to prevent it from rescaling the container
*/
void resetMaxWidth();
/**
* Gets the max width of the text area
*/
float getMaxWidth() const;
/**
* Sets a minimum bound for the height of the text area
*
* @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it at the max height
*/
void setMinHeight(const float height);
/**
* Resets the minimum height to prevent it from rescaling the container
*/
void resetMinHeight();
/**
* Gets the min height of the text area
*/
float getMinHeight() const;
/**
* Sets the max height of the text area to define the maximum number of lines through a height limit
*
* @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it as big as the lines or the min height
*/
void setMaxHeight(const float height);
/**
* Resets the maximum height to prevent it from rescaling the container
*/
void resetMaxHeight();
/**
* Gets the max height of the text area
*/
float getMaxHeight() const;
/**
* Resets the minimum bounds for the width and height
*/
void resetMinBounds();
/**
* Resets the maximum bounds for the width and height
*/
void resetMaxBounds();
/**
* Sets whether the text should be hyphenated
*/
void setHyphenate(const bool hyphenate);
/**
* Gets whether the text should be hyphenated
*/
bool getHyphenate() const;
/**
* Sets whether the text area should show an ellipsis when the text overflows
*
* @note This will only show an ellipsis if the last line doesn't end with an ellipsis already
*/
void setEllipsis(const bool ellipsis);
/**
* Gets whether the text area should show an ellipsis when the text overflows
*/
bool getEllipsis() const;
/**
* Sets whether the text area should defer updates until the next frame
*
* @warning If set to true, all nodes won't immediately update and will only update on the next frame. This can cause side effects if not accounted for
*/
void setDeferUpdates(const bool defer);
/**
* Gets whether the text area should defer updates until the next frame
*/
bool getDeferUpdates() const;
/**
* Gets the height of a line including padding
*/
float getLineHeight() const;
/**
* Gets the labels of the text area
*/
std::vector<Line> getLines() const;
protected:
std::string m_font;
std::string m_text;
cocos2d::ccColor4B m_textColor;
Alignment m_alignment;
WrappingMode m_wrappingMode;
int m_maxLines;
float m_textScale;
float m_linePadding;
float m_minWidth;
float m_maxWidth;
float m_minHeight;
float m_maxHeight;
bool m_hyphenate;
bool m_ellipsis;
bool m_deferUpdates;
bool m_deferred;
std::vector<Line> m_lines;
TextAreaV2(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates);
bool init() override;
void update();
void updatePadding() override;
void updateContainer(const float dt = 0);
void updateLineAlignment(cocos2d::CCLabelBMFont* line);
void addEllipsis(Line& line);
bool isWidthOverflowing(const cocos2d::CCLabelBMFont* line);
size_t getOverflowAmount(cocos2d::CCLabelBMFont* line, const size_t lineSize);
Line createLine(const std::string& text, Line& previousLine);
std::vector<Line> createLines();
std::vector<Line> createNotWrap(const Line& reference);
std::vector<Line> createCutoffWrap(const std::string& text, Line& reference);
std::vector<Line> createDelimitedWrap(const std::string& text, Line& reference, const std::string& delimiters);
std::vector<Line> wrapper(const std::string& text, Line& reference, const std::function<std::vector<Line>(Line& currentLine)>& onOverflow);
};
}

View file

@ -2,7 +2,7 @@
#include <Geode/ui/General.hpp>
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/ui/TextArea.hpp>
#include <Geode/ui/TextAreaV2.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/binding/SetTextPopupDelegate.hpp>
#include <Geode/binding/SetIDPopupDelegate.hpp>

View file

@ -10,7 +10,6 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/TextArea.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include <GUI/CCControlExtension/CCScale9Sprite.h>

View file

@ -323,9 +323,9 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
m_statusDetailsBtn->setID("status-details-button");
m_statusContainer->addChild(m_statusDetailsBtn);
m_statusDetails = SimpleTextArea::create("", "chatFont.fnt", .6f);
m_statusDetails = TextAreaV2::create("", "chatFont.fnt", .6f);
m_statusDetails->setID("status-details-input");
m_statusDetails->setAlignment(kCCTextAlignmentCenter);
m_statusDetails->setAlignment(TextAreaV2::Alignment::Center);
m_statusContainer->addChild(m_statusDetails);
m_statusLoadingCircle = createLoadingCircle(50);

View file

@ -2,7 +2,7 @@
#include <Geode/ui/General.hpp>
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/ui/TextArea.hpp>
#include <Geode/ui/TextAreaV2.hpp>
#include <Geode/ui/TextInput.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/binding/TextArea.hpp>
@ -26,7 +26,7 @@ protected:
ScrollLayer* m_list;
CCMenu* m_statusContainer;
CCLabelBMFont* m_statusTitle;
SimpleTextArea* m_statusDetails;
TextAreaV2* m_statusDetails;
CCMenuItemSpriteExtra* m_statusDetailsBtn;
CCNode* m_statusLoadingCircle;
Slider* m_statusLoadingBar;

View file

@ -11,7 +11,7 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/TextArea.hpp>
#include <Geode/ui/TextAreaV2.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include <GUI/CCControlExtension/CCScale9Sprite.h>
@ -57,15 +57,15 @@ bool ModProblemItem::init(Mod* source, LoadProblem problem, CCSize const& size)
CCPoint { 10.0f, 0.0f }
);
auto label = SimpleTextArea::create(
auto label = TextAreaV2::create(
message.c_str(),
"bigFont.fnt"
);
label->setWrappingMode(WrappingMode::SPACE_WRAP);
label->setWrappingMode(TextAreaV2::WrappingMode::SpaceWrap);
label->setAnchorPoint({ 0.0f, 0.5f });
label->setMaxLines(4);
if (this->showFixButton() || this->showInfoButton()) {
label->setWidth(size.width * 0.7f);
label->setMaxWidth(size.width * 0.7f);
auto helpMenu = CCMenu::create();
helpMenu->setAnchorPoint({ 1.0f, 0.5f });
@ -92,7 +92,7 @@ bool ModProblemItem::init(Mod* source, LoadProblem problem, CCSize const& size)
// Left + Right + Space between
constexpr float paddings = 30.0f;
float calc = size.width - paddings - icon->getScaledContentWidth();
label->setWidth(calc);
label->setMaxWidth(calc);
}
label->setScale(0.4f);
this->addChildAtPosition(

View file

@ -1,4 +1,4 @@
#include <Geode/ui/TextArea.hpp>
#include <Geode/ui/SimpleTextArea.hpp>
using namespace geode::prelude;

View file

@ -0,0 +1,377 @@
#include <Geode/ui/TextAreaV2.hpp>
#include <Geode/DefaultInclude.hpp>
using namespace geode::prelude;
#define IMPL_GETTER(type, name, methodName) \
type geode::TextAreaV2::get##methodName() const { return m_##name; }
#define IMPL_GETTER_SETTER(type, paramType, name, methodName) \
void geode::TextAreaV2::set##methodName(const paramType name) { m_##name = name; this->update(); } \
IMPL_GETTER(type, name, methodName)
geode::TextAreaV2* geode::TextAreaV2::create(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates) {
TextAreaV2* area = new TextAreaV2(text, font, scale, width, height, deferUpdates);
if (area && area->init()) {
area->autorelease();
return area;
} else {
CC_SAFE_DELETE(area);
return nullptr;
}
}
geode::TextAreaV2::TextAreaV2(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates) :
PaddingNode(CCNode::create()),
m_text(text),
m_font(font),
m_textColor({ 255, 255, 255, 255 }),
m_alignment(Alignment::Left),
m_wrappingMode(WrappingMode::WordWrap),
m_maxLines(-1),
m_textScale(scale),
m_linePadding(0),
m_minWidth(-1),
m_maxWidth(width),
m_minHeight(-1),
m_maxHeight(height),
m_hyphenate(true),
m_ellipsis(true),
m_deferUpdates(deferUpdates),
m_deferred(false) { }
bool geode::TextAreaV2::init() {
if (!PaddingNode::init()) {
return false;
}
this->update();
return true;
}
IMPL_GETTER_SETTER(std::string, std::string&, font, Font)
IMPL_GETTER_SETTER(std::string, std::string&, text, Text)
IMPL_GETTER_SETTER(ccColor4B, ccColor4B&, textColor, TextColor)
IMPL_GETTER_SETTER(geode::TextAreaV2::Alignment, Alignment, alignment, Alignment)
IMPL_GETTER_SETTER(geode::TextAreaV2::WrappingMode, WrappingMode, wrappingMode, WrappingMode)
IMPL_GETTER_SETTER(int, int, maxLines, MaxLines)
IMPL_GETTER_SETTER(float, float, textScale, TextScale)
IMPL_GETTER_SETTER(float, float, linePadding, LinePadding)
IMPL_GETTER_SETTER(float, float, minWidth, MinWidth)
IMPL_GETTER_SETTER(float, float, maxWidth, MaxWidth)
IMPL_GETTER_SETTER(float, float, minHeight, MinHeight)
IMPL_GETTER_SETTER(float, float, maxHeight, MaxHeight)
IMPL_GETTER_SETTER(bool, bool, hyphenate, Hyphenate)
IMPL_GETTER_SETTER(bool, bool, ellipsis, Ellipsis)
IMPL_GETTER(bool, deferUpdates, DeferUpdates)
IMPL_GETTER(std::vector<geode::TextAreaV2::Line>, lines, Lines)
void geode::TextAreaV2::resetMinWidth() {
this->setMinWidth(-1);
}
void geode::TextAreaV2::resetMaxWidth() {
this->setMaxWidth(-1);
}
void geode::TextAreaV2::resetMinHeight() {
this->setMinHeight(-1);
}
void geode::TextAreaV2::resetMaxHeight() {
this->setMaxHeight(-1);
}
void geode::TextAreaV2::resetMinBounds() {
this->resetMinWidth();
this->resetMinHeight();
}
void geode::TextAreaV2::resetMaxBounds() {
this->resetMaxWidth();
this->resetMaxHeight();
}
void geode::TextAreaV2::setDeferUpdates(const bool defer) {
m_deferUpdates = defer;
// If already deferred, update the container immediately and unschedule the deferred update
if (m_deferred) {
this->unschedule(schedule_selector(TextAreaV2::updateContainer));
this->updateContainer();
}
}
float geode::TextAreaV2::getLineHeight() const {
return m_lines.empty() ? 0 : m_lines.front().label->getScaledContentHeight() + m_linePadding;
}
void geode::TextAreaV2::update() {
if (!m_deferred) {
if (m_deferUpdates) {
m_deferred = true;
this->scheduleOnce(schedule_selector(TextAreaV2::updateContainer), 0);
} else {
this->updateContainer();
}
}
}
void geode::TextAreaV2::updatePadding() {
this->update();
}
void geode::TextAreaV2::updateContainer(const float dt) {
PaddingNode::updatePadding();
m_container->removeAllChildren();
m_deferred = false;
m_lines = this->createLines();
const bool upperBoundWidth = m_maxWidth >= 0;
const bool upperBoundHeight = m_maxHeight >= 0;
const bool artificialWidth = m_minWidth >= 0 || upperBoundWidth;
const bool artificialHeight = m_minHeight >= 0 || upperBoundHeight;
const bool inheritedNodeSize = this->getContentSize() == this->getPaddedContainerSize();
float height = m_lines.empty() ? 0 : m_lines.back().currentHeight;
float width = 0;
// First determine the container size before manipulating the node anchors and true positions
for (const Line& line : m_lines) {
width = std::max(width, line.label->getScaledContentWidth());
}
if (artificialWidth) {
width = std::max(m_minWidth, width);
m_container->setContentWidth(upperBoundWidth ? std::min(m_maxWidth, width) : width);
} else {
m_container->setContentWidth(width);
}
if (artificialHeight) {
height = std::max(m_minHeight, height);
m_container->setContentHeight(upperBoundHeight ? std::min(m_maxHeight, height) : height);
} else {
m_container->setContentHeight(height);
}
if (inheritedNodeSize) {
this->setContentSize(this->getPaddedContainerSize());
}
for (const Line& line : m_lines) {
this->updateLineAlignment(line.label);
// Correct the Y position to be relative to the container height
line.label->setPositionY(m_container->getContentHeight() - line.currentHeight + line.label->getScaledContentHeight());
m_container->addChild(line.label);
}
}
void geode::TextAreaV2::updateLineAlignment(CCLabelBMFont* line) {
switch (m_alignment) {
case Alignment::Left:
line->setAnchorPoint({ 0, 1 });
line->setPositionX(0);
break;
case Alignment::Center:
line->setAnchorPoint({ 0.5f, 1 });
line->setPositionX(m_container->getContentWidth() / 2);
break;
case Alignment::Right:
line->setAnchorPoint({ 1, 1 });
line->setPositionX(m_container->getContentWidth());
break;
}
}
void geode::TextAreaV2::addEllipsis(Line& line) {
if (!m_ellipsis || line.lineNumber == -1) {
return;
}
line.text = line.text.find_first_not_of(' ') == std::string::npos ?
"..." :
line.text + std::string("...").substr(std::min<size_t>(3, line.text.size() - line.text.find_last_not_of('.') - 1));
line.label->setString(line.text.c_str());
if (this->isWidthOverflowing(line.label)) {
const size_t lineSize = line.text.size();
const size_t overflow = this->getOverflowAmount(line.label, lineSize + 3);
line.overflow = line.text.substr(lineSize - overflow - 3, overflow) + line.overflow;
line.label->setString((line.text = line.text.substr(0, lineSize - overflow - 3) + "...").c_str());
}
}
bool geode::TextAreaV2::isWidthOverflowing(const CCLabelBMFont* line) {
return m_wrappingMode != WrappingMode::NoWrap && m_maxWidth >= 0 && line->getScaledContentWidth() + this->getTotalPaddingX() > m_maxWidth;
}
size_t geode::TextAreaV2::getOverflowAmount(CCLabelBMFont* line, const size_t lineSize) {
for (size_t overflow = 1; overflow < lineSize; overflow++) {
CCNode* character = cocos::getChild(line, lineSize - overflow);
if ((character->getPositionX() - character->getContentWidth() / 2) * m_textScale <= m_maxWidth) {
return overflow;
}
}
return 0;
}
geode::TextAreaV2::Line geode::TextAreaV2::createLine(const std::string& text, Line& previousLine) {
CCLabelBMFont* line = CCLabelBMFont::create(text.c_str(), m_font.c_str());
line->setScale(m_textScale);
line->setColor({ m_textColor.r, m_textColor.g, m_textColor.b });
line->setOpacity(m_textColor.a);
const size_t lineSize = text.size();
const float currentHeight = m_linePadding + previousLine.currentHeight + line->getScaledContentHeight();
const size_t overflow = this->isWidthOverflowing(line) ? this->getOverflowAmount(line, lineSize) : 0;
const size_t overflowStart = lineSize - overflow;
const std::string finalText = text.substr(0, overflowStart);
const bool exceededLines = (m_maxLines != -1 && previousLine.lineNumber + 1 >= m_maxLines) ||
(m_maxHeight != -1 && currentHeight + this->getTotalPaddingY() > m_maxHeight);
if (overflow > 0) {
line->setString(finalText.c_str());
}
previousLine.isLastLine = exceededLines;
return {
.text = finalText,
.overflow = text.substr(overflowStart, overflow),
.label = line,
.lineNumber = previousLine.lineNumber + 1,
.currentHeight = currentHeight,
.isLastLine = false
};
}
std::vector<geode::TextAreaV2::Line> geode::TextAreaV2::createLines() {
Line placeholderLine = {
.lineNumber = -1,
.currentHeight = -m_linePadding
};
switch (m_wrappingMode) {
case WrappingMode::NoWrap: return this->createNotWrap(placeholderLine);
case WrappingMode::WordWrap: return this->createDelimitedWrap(m_text, placeholderLine, " `~!@#$%^&*()-_=+[{}];:'\",<.>/?\\|");
case WrappingMode::SpaceWrap: return this->createDelimitedWrap(m_text, placeholderLine, " ");
case WrappingMode::CutoffWrap: return this->createCutoffWrap(m_text, placeholderLine);
}
}
std::vector<geode::TextAreaV2::Line> geode::TextAreaV2::createNotWrap(const Line& reference) {
Line previousLine = reference;
std::stringstream stream(m_text);
std::vector<Line> lines;
std::string line;
while (std::getline(stream, line)) {
const Line currentLine = this->createLine(line, previousLine);
if (previousLine.isLastLine) {
this->addEllipsis(previousLine);
break;
} else {
lines.push_back(previousLine = currentLine);
}
}
return lines;
}
std::vector<geode::TextAreaV2::Line> geode::TextAreaV2::createCutoffWrap(const std::string& text, Line& reference) {
return this->wrapper(text, reference, [this](Line& currentLine) {
if (currentLine.text.empty()) return std::vector<Line>();
if (m_hyphenate && currentLine.text.back() != '-') {
currentLine.overflow = currentLine.text.back() + currentLine.overflow;
currentLine.text.pop_back();
currentLine.text = currentLine.text.substr(0, currentLine.text.find_last_not_of(' ') + 1) + "-";
currentLine.label->setString(currentLine.text.c_str());
}
return this->createCutoffWrap(currentLine.overflow.erase(0, currentLine.overflow.find_first_not_of(' ')), currentLine);
});
}
std::vector<geode::TextAreaV2::Line> geode::TextAreaV2::createDelimitedWrap(const std::string& text, Line& reference, const std::string& delimiters) {
return this->wrapper(text, reference, [this, delimiters](Line& currentLine) {
if (currentLine.text.empty()) return std::vector<Line>();
const size_t lineSize = currentLine.text.size();
size_t additionalOverflow = 0;
while (additionalOverflow < lineSize && delimiters.find(currentLine.text[lineSize - additionalOverflow - 1]) == std::string::npos) {
additionalOverflow++;
}
if (additionalOverflow == lineSize) additionalOverflow = 0;
currentLine.overflow = currentLine.text.substr(lineSize - additionalOverflow) + currentLine.overflow;
currentLine.text = currentLine.text.substr(0, lineSize - additionalOverflow);
if (m_hyphenate && currentLine.text.back() != '-') {
if (additionalOverflow == 0) {
currentLine.overflow = currentLine.text.back() + currentLine.overflow;
currentLine.text.pop_back();
}
currentLine.text = currentLine.text.substr(0, currentLine.text.find_last_not_of(' ') + 1) + "-";
}
currentLine.label->setString(currentLine.text.c_str());
return this->createDelimitedWrap(currentLine.overflow.erase(0, currentLine.overflow.find_first_not_of(' ')), currentLine, delimiters);
});
}
std::vector<geode::TextAreaV2::Line> geode::TextAreaV2::wrapper(const std::string& text, Line& reference, const std::function<std::vector<Line>(Line& currentLine)>& onOverflow) {
Line previousLine = reference;
std::stringstream stream(text);
std::vector<Line> lines;
std::string line;
while (std::getline(stream, line)) {
Line currentLine = this->createLine(line, previousLine);
if (previousLine.isLastLine) {
if (previousLine.lineNumber == reference.lineNumber) {
reference.isLastLine = true;
this->addEllipsis(reference);
} else {
this->addEllipsis(previousLine);
}
break;
} else if (currentLine.overflow.empty()) {
lines.push_back(previousLine = currentLine);
} else {
const std::vector<Line> overflowedLines = onOverflow(currentLine);
lines.push_back(currentLine);
lines.insert(lines.end(), overflowedLines.begin(), overflowedLines.end());
if ((previousLine = lines.back()).isLastLine) break;
}
}
return lines;
}