mirror of
git://projects.qi-hardware.com/gmenu2x.git
synced 2024-11-05 08:49:43 +02:00
TextDialog: Improve the average and worst-case performance of word wrapping
This affects manuals, About GMenu2X, and the Log Viewer. Instead of trying to compute the width of the entire string, then backing off one word at a time, TextDialog::preProcess now performs a binary search on Font::getTextWidth(string) and backs off to the last fitting space, if there is one, at the last moment. In Japanese and Chinese text, words are not usually separated by spaces. Text in these languages is now wrapped when it would reach the edge of the screen.
This commit is contained in:
parent
9b93eabcc5
commit
69d6c0006c
@ -37,42 +37,79 @@ TextDialog::TextDialog(GMenu2X *gmenu2x, const string &title, const string &desc
|
||||
|
||||
void TextDialog::preProcess() {
|
||||
unsigned i = 0;
|
||||
string row;
|
||||
|
||||
while (i<text->size()) {
|
||||
//clean this row
|
||||
row = trim(text->at(i));
|
||||
while (i < text->size()) {
|
||||
/* Clean the end of the string, allowing lines that are indented at
|
||||
* the start to stay as such. */
|
||||
string line = rtrim(text->at(i));
|
||||
|
||||
//check if this row is not too long
|
||||
if (gmenu2x->font->getTextWidth(row)>(int)gmenu2x->resX-15) {
|
||||
vector<string> words;
|
||||
split(words, row, " ");
|
||||
if (gmenu2x->font->getTextWidth(line) > (int) gmenu2x->resX - 15) {
|
||||
/* At least one full character must fit, in order to advance. */
|
||||
size_t fits = 1;
|
||||
while (fits < line.length() && !isUTF8Starter(line[fits])) {
|
||||
fits++;
|
||||
}
|
||||
size_t doesntFit = fits;
|
||||
|
||||
unsigned numWords = words.size();
|
||||
//find the maximum number of rows that can be printed on screen
|
||||
while (gmenu2x->font->getTextWidth(row)>(int)gmenu2x->resX-15 && numWords>0) {
|
||||
numWords--;
|
||||
row = "";
|
||||
for (unsigned x=0; x<numWords; x++)
|
||||
row += words[x] + " ";
|
||||
row = trim(row);
|
||||
/* This preprocessing finds an upper bound on the number of
|
||||
* bytes of full characters that fit on the screen, 2^n, in
|
||||
* n steps. */
|
||||
do {
|
||||
fits = doesntFit; /* what didn't fit has been determined to fit by a previous iteration */
|
||||
doesntFit = min(2 * fits, line.length());
|
||||
while (doesntFit < line.length() && !isUTF8Starter(line[doesntFit])) {
|
||||
doesntFit++;
|
||||
}
|
||||
} while (doesntFit <= line.length()
|
||||
&& gmenu2x->font->getTextWidth(line.substr(0, doesntFit)) <= (int) gmenu2x->resX - 15);
|
||||
|
||||
/* End this loop when N characters fit but N + 1 don't. */
|
||||
while (fits + 1 < doesntFit) {
|
||||
size_t guess = fits + (doesntFit - fits) / 2;
|
||||
if (!isUTF8Starter(line[guess]))
|
||||
{
|
||||
size_t oldGuess = guess;
|
||||
/* Adjust the guess to the nearest UTF-8 starter that is
|
||||
* not 'fits' or 'doesntFit'. */
|
||||
for (size_t offset = 1; offset < (doesntFit - fits) / 2 - 1; offset++) {
|
||||
if (isUTF8Starter(line[guess - offset])) {
|
||||
guess -= offset;
|
||||
break;
|
||||
} else if (isUTF8Starter(line[guess + offset])) {
|
||||
guess += offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* If there's no such character, exit early. */
|
||||
if (guess == oldGuess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gmenu2x->font->getTextWidth(line.substr(0, guess)) <= (int) gmenu2x->resX - 15) {
|
||||
fits = guess;
|
||||
} else {
|
||||
doesntFit = guess;
|
||||
}
|
||||
}
|
||||
|
||||
//if numWords==0 then the string must be printed as-is, it cannot be split
|
||||
if (numWords>0) {
|
||||
//replace with the shorter version
|
||||
text->at(i) = row;
|
||||
|
||||
//build the remaining text in another row
|
||||
row = "";
|
||||
for (unsigned x=numWords; x<words.size(); x++)
|
||||
row += words[x] + " ";
|
||||
row = trim(row);
|
||||
|
||||
if (!row.empty())
|
||||
text->insert(text->begin()+i+1, row);
|
||||
/* The line shall be split at the last space-separated word that
|
||||
* fully fits, or otherwise at the last character that fits. */
|
||||
size_t lastSpace = line.find_last_of(" \t\r", fits);
|
||||
if (lastSpace != string::npos) {
|
||||
fits = lastSpace;
|
||||
}
|
||||
|
||||
/* Insert the rest in a new slot after this line.
|
||||
* TODO (Nebuleon) Don't use a vector for this, because all later
|
||||
* elements are moved, which is inefficient. */
|
||||
text->insert(text->begin() + i + 1, ltrim(line.substr(fits)));
|
||||
line = rtrim(line.substr(0, fits));
|
||||
}
|
||||
|
||||
/* Put the trimmed whole line or the smaller split of the split line
|
||||
* back into the same slot */
|
||||
text->at(i) = line;
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,16 @@ string trim(const string& s) {
|
||||
return b == string::npos ? "" : string(s, b, e + 1 - b);
|
||||
}
|
||||
|
||||
string ltrim(const string& s) {
|
||||
auto b = s.find_first_not_of(" \t\r");
|
||||
return b == string::npos ? "" : string(s, b);
|
||||
}
|
||||
|
||||
string rtrim(const string& s) {
|
||||
auto e = s.find_last_not_of(" \t\r");
|
||||
return e == string::npos ? "" : string(s, 0, e + 1);
|
||||
}
|
||||
|
||||
bool fileExists(const string &file) {
|
||||
fstream fin;
|
||||
fin.open(file.c_str() ,ios::in);
|
||||
|
@ -35,8 +35,16 @@ public:
|
||||
bool operator()(const std::string &left, const std::string &right) const;
|
||||
};
|
||||
|
||||
inline bool isUTF8Starter(char c) {
|
||||
return (c & 0xC0) != 0x80;
|
||||
}
|
||||
|
||||
/** Returns the string with whitespace stripped from both ends. */
|
||||
std::string trim(const std::string& s);
|
||||
/** Returns the string with whitespace stripped from the start. */
|
||||
std::string ltrim(const std::string& s);
|
||||
/** Returns the string with whitespace stripped from the end. */
|
||||
std::string rtrim(const std::string& s);
|
||||
|
||||
std::string strreplace(std::string orig, const std::string &search, const std::string &replace);
|
||||
std::string cmdclean(std::string cmdline);
|
||||
|
Loading…
Reference in New Issue
Block a user