diff --git a/src/font.cpp b/src/font.cpp index 96c51e7..9737082 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -74,6 +74,120 @@ int Font::getTextWidth(const string &text) } } +string Font::wordWrap(const string &text, int width) +{ + string result; + result.reserve(text.length()); + + size_t start = 0, end = text.find('\n'); + while (end != string::npos) { + if (start != end) { + result.append(wordWrapSingleLine(text, start, end, width)); + } + result.append("\n"); + start = end + 1; + end = text.find('\n', start); + } + + if (start < text.length()) { + result.append(wordWrapSingleLine(text, start, text.length(), width)); + } + + return result; +} + +string Font::wordWrapSingleLine(const string &text, size_t start, size_t end, int width) +{ + string result; + result.reserve(end - start); + + while (start != end) { + /* Clean the end of the string, allowing lines that are indented at + * the start to stay as such. */ + string run = rtrim(text.substr(start, end - start)); + int runWidth = getTextWidth(run); + + if (runWidth > width) { + size_t fits = 0, doesntFit = run.length(); + /* First guess: width / runWidth approximates the proportion of + * the run that should fit. */ + size_t guess = min(run.length(), (size_t) (doesntFit * ((float) width / runWidth))); + /* Adjust that to fully include any partial UTF-8 character. */ + while (guess < run.length() && !isUTF8Starter(run[guess])) { + guess++; + } + + if (getTextWidth(run.substr(0, guess)) <= width) { + fits = guess; + doesntFit = fits; + /* Prime doesntFit, which should be closer to 2 * fits than + * to run.length() / 2 if the run is long. */ + do { + fits = doesntFit; // determined to fit by a previous iteration + doesntFit = min(2 * fits, run.length()); + while (doesntFit < run.length() && !isUTF8Starter(run[doesntFit])) { + doesntFit++; + } + } while (doesntFit < run.length() && getTextWidth(run.substr(0, doesntFit)) <= width); + } else { + doesntFit = guess; + } + + /* End this loop when N full characters fit but N + 1 don't. */ + while (fits + 1 < doesntFit) { + size_t guess = fits + (doesntFit - fits) / 2; + if (!isUTF8Starter(run[guess])) { + size_t oldGuess = guess; + /* Adjust the guess to fully include a UTF-8 character. */ + for (size_t offset = 1; offset < (doesntFit - fits) / 2 - 1; offset++) { + if (isUTF8Starter(run[guess - offset])) { + guess -= offset; + break; + } else if (isUTF8Starter(run[guess + offset])) { + guess += offset; + break; + } + } + /* If there's no such character, exit early. */ + if (guess == oldGuess) { + break; + } + } + if (getTextWidth(run.substr(0, guess)) <= width) { + fits = guess; + } else { + doesntFit = guess; + } + } + + /* The run shall be split at the last space-separated word that + * fully fits, or otherwise at the last character that fits. */ + size_t lastSpace = run.find_last_of(" \t\r", fits); + if (lastSpace != string::npos) { + fits = lastSpace; + } + + /* If 0 characters fit, we'll have to make 1 fit anyway, otherwise + * we're in for an infinite loop. This can happen if the font size + * is large. */ + if (fits == 0) { + fits = 1; + while (fits < run.length() && !isUTF8Starter(run[fits])) { + fits++; + } + } + + result.append(rtrim(run.substr(0, fits))).append("\n"); + start = min(end, text.find_first_not_of(" \t\r", start + fits)); + } else { + result.append(rtrim(run)); + start = end; + } + } + + return result; +} + int Font::getTextHeight(const string &text) { int nLines = 1; diff --git a/src/font.h b/src/font.h index 1703726..dce6904 100644 --- a/src/font.h +++ b/src/font.h @@ -22,6 +22,8 @@ public: Font(const std::string &path, unsigned int size); ~Font(); + std::string wordWrap(const std::string &text, int width); + int getTextWidth(const std::string& text); int getTextHeight(const std::string& text); @@ -37,6 +39,9 @@ public: private: Font(TTF_Font *font); + std::string wordWrapSingleLine(const std::string &text, + size_t start, size_t end, int width); + void writeLine(Surface *surface, std::string const& text, int x, int y, HAlign halign, VAlign valign);