Date:2014-07-19 02:33:55 (4 months 4 days ago)
Author:Nebuleon Fumika
Commit:69d6c0006c0f3e4eb9369b0dcb3e2ff7be18454f
Message: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.
Files: src/textdialog.cpp (1 diff)
src/utilities.cpp (1 diff)
src/utilities.h (1 diff)

Change Details

src/textdialog.cpp
3737
3838void TextDialog::preProcess() {
3939    unsigned i = 0;
40    string row;
41
42    while (i<text->size()) {
43        //clean this row
44        row = trim(text->at(i));
45
46        //check if this row is not too long
47        if (gmenu2x->font->getTextWidth(row)>(int)gmenu2x->resX-15) {
48            vector<string> words;
49            split(words, row, " ");
50
51            unsigned numWords = words.size();
52            //find the maximum number of rows that can be printed on screen
53            while (gmenu2x->font->getTextWidth(row)>(int)gmenu2x->resX-15 && numWords>0) {
54                numWords--;
55                row = "";
56                for (unsigned x=0; x<numWords; x++)
57                    row += words[x] + " ";
58                row = trim(row);
59            }
6040
61            //if numWords==0 then the string must be printed as-is, it cannot be split
62            if (numWords>0) {
63                //replace with the shorter version
64                text->at(i) = row;
41    while (i < text->size()) {
42        /* Clean the end of the string, allowing lines that are indented at
43         * the start to stay as such. */
44        string line = rtrim(text->at(i));
6545
66                //build the remaining text in another row
67                row = "";
68                for (unsigned x=numWords; x<words.size(); x++)
69                    row += words[x] + " ";
70                row = trim(row);
46        if (gmenu2x->font->getTextWidth(line) > (int) gmenu2x->resX - 15) {
47            /* At least one full character must fit, in order to advance. */
48            size_t fits = 1;
49            while (fits < line.length() && !isUTF8Starter(line[fits])) {
50                fits++;
51            }
52            size_t doesntFit = fits;
53
54            /* This preprocessing finds an upper bound on the number of
55             * bytes of full characters that fit on the screen, 2^n, in
56             * n steps. */
57            do {
58                fits = doesntFit; /* what didn't fit has been determined to fit by a previous iteration */
59                doesntFit = min(2 * fits, line.length());
60                while (doesntFit < line.length() && !isUTF8Starter(line[doesntFit])) {
61                    doesntFit++;
62                }
63            } while (doesntFit <= line.length()
64                  && gmenu2x->font->getTextWidth(line.substr(0, doesntFit)) <= (int) gmenu2x->resX - 15);
65
66            /* End this loop when N characters fit but N + 1 don't. */
67            while (fits + 1 < doesntFit) {
68                size_t guess = fits + (doesntFit - fits) / 2;
69                if (!isUTF8Starter(line[guess]))
70                {
71                    size_t oldGuess = guess;
72                    /* Adjust the guess to the nearest UTF-8 starter that is
73                     * not 'fits' or 'doesntFit'. */
74                    for (size_t offset = 1; offset < (doesntFit - fits) / 2 - 1; offset++) {
75                        if (isUTF8Starter(line[guess - offset])) {
76                            guess -= offset;
77                            break;
78                        } else if (isUTF8Starter(line[guess + offset])) {
79                            guess += offset;
80                            break;
81                        }
82                    }
83                    /* If there's no such character, exit early. */
84                    if (guess == oldGuess) {
85                        break;
86                    }
87                }
88                if (gmenu2x->font->getTextWidth(line.substr(0, guess)) <= (int) gmenu2x->resX - 15) {
89                    fits = guess;
90                } else {
91                    doesntFit = guess;
92                }
93            }
7194
72                if (!row.empty())
73                    text->insert(text->begin()+i+1, row);
95            /* The line shall be split at the last space-separated word that
96             * fully fits, or otherwise at the last character that fits. */
97            size_t lastSpace = line.find_last_of(" \t\r", fits);
98            if (lastSpace != string::npos) {
99                fits = lastSpace;
74100            }
101
102            /* Insert the rest in a new slot after this line.
103             * TODO (Nebuleon) Don't use a vector for this, because all later
104             * elements are moved, which is inefficient. */
105            text->insert(text->begin() + i + 1, ltrim(line.substr(fits)));
106            line = rtrim(line.substr(0, fits));
75107        }
108
109        /* Put the trimmed whole line or the smaller split of the split line
110         * back into the same slot */
111        text->at(i) = line;
112
76113        i++;
77114    }
78115}
src/utilities.cpp
4646  return b == string::npos ? "" : string(s, b, e + 1 - b);
4747}
4848
49string ltrim(const string& s) {
50  auto b = s.find_first_not_of(" \t\r");
51  return b == string::npos ? "" : string(s, b);
52}
53
54string rtrim(const string& s) {
55  auto e = s.find_last_not_of(" \t\r");
56  return e == string::npos ? "" : string(s, 0, e + 1);
57}
58
4959bool fileExists(const string &file) {
5060    fstream fin;
5161    fin.open(file.c_str() ,ios::in);
src/utilities.h
3535    bool operator()(const std::string &left, const std::string &right) const;
3636};
3737
38inline bool isUTF8Starter(char c) {
39    return (c & 0xC0) != 0x80;
40}
41
3842/** Returns the string with whitespace stripped from both ends. */
3943std::string trim(const std::string& s);
44/** Returns the string with whitespace stripped from the start. */
45std::string ltrim(const std::string& s);
46/** Returns the string with whitespace stripped from the end. */
47std::string rtrim(const std::string& s);
4048
4149std::string strreplace(std::string orig, const std::string &search, const std::string &replace);
4250std::string cmdclean(std::string cmdline);

Archive Download the corresponding diff file



interactive