Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Common/Data/Text/WrapText.cpp
5654 views
1
#include <cstring>
2
#include "Common/Render/DrawBuffer.h"
3
#include "Common/Data/Encoding/Utf8.h"
4
#include "Common/Data/Text/WrapText.h"
5
6
bool WordWrapper::IsCJK(uint32_t c) {
7
if (c < 0x1000) {
8
return false;
9
}
10
11
// CJK characters can be wrapped more freely.
12
bool result = (c >= 0x1100 && c <= 0x11FF); // Hangul Jamo.
13
result = result || (c >= 0x2E80 && c <= 0x2FFF); // Kangxi Radicals etc.
14
#if 0
15
result = result || (c >= 0x3040 && c <= 0x31FF); // Hiragana, Katakana, Hangul Compatibility Jamo etc.
16
result = result || (c >= 0x3200 && c <= 0x32FF); // CJK Enclosed
17
result = result || (c >= 0x3300 && c <= 0x33FF); // CJK Compatibility
18
result = result || (c >= 0x3400 && c <= 0x4DB5); // CJK Unified Ideographs Extension A
19
#else
20
result = result || (c >= 0x3040 && c <= 0x4DB5); // Above collapsed
21
#endif
22
result = result || (c >= 0x4E00 && c <= 0x9FBB); // CJK Unified Ideographs
23
result = result || (c >= 0xAC00 && c <= 0xD7AF); // Hangul Syllables
24
result = result || (c >= 0xF900 && c <= 0xFAD9); // CJK Compatibility Ideographs
25
result = result || (c >= 0x20000 && c <= 0x2A6D6); // CJK Unified Ideographs Extension B
26
result = result || (c >= 0x2F800 && c <= 0x2FA1D); // CJK Compatibility Supplement
27
return result;
28
}
29
30
bool WordWrapper::IsPunctuation(uint32_t c) {
31
switch (c) {
32
// TODO: This list of punctuation is very incomplete.
33
case ',':
34
case '.':
35
case ':':
36
case '!':
37
case ')':
38
case '?':
39
case 0x00AD: // SOFT HYPHEN
40
case 0x3001: // IDEOGRAPHIC COMMA
41
case 0x3002: // IDEOGRAPHIC FULL STOP
42
case 0x06D4: // ARABIC FULL STOP
43
case 0xFF01: // FULLWIDTH EXCLAMATION MARK
44
case 0xFF09: // FULLWIDTH RIGHT PARENTHESIS
45
case 0xFF1F: // FULLWIDTH QUESTION MARK
46
return true;
47
48
default:
49
return false;
50
}
51
}
52
53
bool WordWrapper::IsSpace(uint32_t c) {
54
switch (c) {
55
case '\t':
56
case ' ':
57
case 0x2002: // EN SPACE
58
case 0x2003: // EM SPACE
59
case 0x3000: // IDEOGRAPHIC SPACE
60
return true;
61
62
default:
63
return false;
64
}
65
}
66
67
bool WordWrapper::IsShy(uint32_t c) {
68
return c == 0x00AD; // SOFT HYPHEN
69
}
70
71
std::string WordWrapper::Wrapped() {
72
if (out_.empty()) {
73
Wrap();
74
// Hack: Remove trailing line breaks.
75
if (!out_.empty() && out_.back() == '\n') {
76
out_.pop_back();
77
}
78
}
79
return out_;
80
}
81
82
bool WordWrapper::WrapBeforeWord() {
83
if (flags_ & FLAG_WRAP_TEXT) {
84
if (x_ + wordWidth_ > maxW_ && !out_.empty()) {
85
if (IsShy(lastChar_)) {
86
// Soft hyphen, replace it with a real hyphen since we wrapped at it.
87
// TODO: There's an edge case here where the hyphen might not fit.
88
out_[out_.size() - 2] = '-';
89
out_[out_.size() - 1] = '\n';
90
} else {
91
out_ += "\n";
92
}
93
lastChar_ = '\n';
94
lastLineStart_ = out_.size();
95
x_ = 0.0f;
96
forceEarlyWrap_ = false;
97
return true;
98
}
99
}
100
if (flags_ & FLAG_ELLIPSIZE_TEXT) {
101
const bool hasEllipsis = out_.size() > 3 && out_.substr(out_.size() - 3) == "...";
102
if (x_ + wordWidth_ > maxW_ && !hasEllipsis) {
103
AddEllipsis();
104
skipNextWord_ = true;
105
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
106
scanForNewline_ = true;
107
}
108
}
109
}
110
return false;
111
}
112
113
void WordWrapper::AddEllipsis() {
114
if (!out_.empty() && IsSpaceOrShy(lastChar_)) {
115
UTF8 utf(out_.c_str(), (int)out_.size());
116
utf.bwd();
117
out_.resize(utf.byteIndex());
118
out_ += "...";
119
} else {
120
out_ += "...";
121
}
122
lastChar_ = '.';
123
x_ += ellipsisWidth_;
124
}
125
126
void WordWrapper::AppendWord(int endIndex, int lastChar, bool addNewline) {
127
int lastWordStartIndex = lastIndex_;
128
if (WrapBeforeWord()) {
129
// Advance to the first non-whitespace UTF-8 character in the following word (if any) to prevent starting the new line with a whitespace
130
UTF8 utf8Word(str_, lastWordStartIndex);
131
while (lastWordStartIndex < endIndex) {
132
const uint32_t c = utf8Word.next();
133
if (!IsSpace(c)) {
134
break;
135
}
136
lastWordStartIndex = utf8Word.byteIndex();
137
}
138
}
139
140
lastEllipsisIndex_ = -1;
141
if (skipNextWord_) {
142
lastIndex_ = endIndex;
143
return;
144
}
145
146
// This will include the newline.
147
if (x_ <= maxW_) {
148
out_.append(str_.data() + lastWordStartIndex, str_.data() + endIndex);
149
} else {
150
scanForNewline_ = true;
151
}
152
if (addNewline && (flags_ & FLAG_WRAP_TEXT)) {
153
out_ += "\n";
154
lastChar_ = '\n';
155
lastLineStart_ = out_.size();
156
scanForNewline_ = false;
157
x_ = 0.0f;
158
} else {
159
// We may have appended a newline - check.
160
size_t pos = out_.find_last_of('\n');
161
if (pos != out_.npos) {
162
lastLineStart_ = pos + 1;
163
}
164
165
if (lastChar == -1 && !out_.empty()) {
166
UTF8 utf(out_.c_str(), (int)out_.size());
167
utf.bwd();
168
lastChar = utf.next();
169
}
170
lastChar_ = lastChar;
171
172
if (lastLineStart_ != out_.size()) {
173
// To account for kerning around spaces, we recalculate the entire line width.
174
x_ = MeasureWidth(std::string_view(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_));
175
} else {
176
x_ = 0.0f;
177
}
178
}
179
lastIndex_ = endIndex;
180
wordWidth_ = 0.0f;
181
}
182
183
void WordWrapper::Wrap() {
184
// First, let's check if it fits as-is.
185
size_t len = str_.length();
186
if (MeasureWidth(str_) <= maxW_) {
187
// If it fits, we don't need to go through each character.
188
out_ = std::string(str_);
189
return;
190
}
191
192
out_.clear();
193
// We know it'll be approximately this size. It's fine if the guess is a little off.
194
out_.reserve(len + len / 16);
195
196
if (flags_ & FLAG_ELLIPSIZE_TEXT) {
197
ellipsisWidth_ = MeasureWidth("...");
198
}
199
200
for (UTF8 utf(str_); !utf.end(); ) {
201
int beforeIndex = utf.byteIndex();
202
uint32_t c = utf.next();
203
int afterIndex = utf.byteIndex();
204
205
// Is this a newline character, hard wrapping?
206
if (c == '\n') {
207
if (skipNextWord_) {
208
lastIndex_ = beforeIndex;
209
skipNextWord_ = false;
210
}
211
// This will include the newline character.
212
AppendWord(afterIndex, c, false);
213
// We wrapped once, so stop forcing.
214
forceEarlyWrap_ = false;
215
scanForNewline_ = false;
216
continue;
217
}
218
219
if (scanForNewline_) {
220
// We're discarding the rest of the characters until a newline (no wrapping.)
221
lastIndex_ = afterIndex;
222
continue;
223
}
224
225
// Measure the entire word for kerning purposes. May not be 100% perfect.
226
float newWordWidth = 0.0f;
227
if (afterIndex <= str_.length()) {
228
newWordWidth = MeasureWidth(str_.substr(lastIndex_, afterIndex - lastIndex_));
229
}
230
231
// Is this the end of a word (space)? We'll also output up to a soft hyphen.
232
if (wordWidth_ > 0.0f && IsSpaceOrShy(c)) {
233
AppendWord(afterIndex, c, false);
234
skipNextWord_ = false;
235
continue;
236
}
237
238
// We're scanning for the next word.
239
if (skipNextWord_)
240
continue;
241
242
if ((flags_ & FLAG_ELLIPSIZE_TEXT) != 0 && wordWidth_ > 0.0f && lastEllipsisIndex_ == -1) {
243
float checkX = x_;
244
// If we allow wrapping, assume we'll wrap as needed.
245
if ((flags_ & FLAG_WRAP_TEXT) != 0 && x_ >= maxW_) {
246
checkX = 0;
247
}
248
249
// If we can only fit an ellipsis, time to output and skip ahead.
250
// Ignore x for newWordWidth, because we might wrap.
251
if (checkX + wordWidth_ + ellipsisWidth_ <= maxW_ && newWordWidth + ellipsisWidth_ > maxW_) {
252
lastEllipsisIndex_ = beforeIndex;
253
continue;
254
}
255
}
256
257
// Can the word fit on a line even all by itself so far?
258
if (wordWidth_ > 0.0f && newWordWidth > maxW_) {
259
// If we had a good place for an ellipsis, let's do that.
260
if (lastEllipsisIndex_ != -1) {
261
AppendWord(lastEllipsisIndex_, -1, false);
262
AddEllipsis();
263
skipNextWord_ = true;
264
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
265
scanForNewline_ = true;
266
}
267
continue;
268
}
269
270
// Doesn't fit. Let's drop what's there so far onto its own line.
271
if (x_ > 0.0f && x_ + wordWidth_ > maxW_ && beforeIndex > lastIndex_ && (flags_ & FLAG_WRAP_TEXT) != 0) {
272
// Let's put as many characters as will fit on the previous line.
273
// This word can't fit on one line even, so it's going to be cut into pieces anyway.
274
// Better to avoid huge gaps, in that case.
275
forceEarlyWrap_ = true;
276
277
// Now rewind back to where the word started so we can wrap at the opportune moment.
278
wordWidth_ = 0.0f;
279
while (utf.byteIndex() > lastIndex_) {
280
utf.bwd();
281
}
282
continue;
283
}
284
// Now, add the word so far (without this latest character) and break.
285
AppendWord(beforeIndex, -1, true);
286
forceEarlyWrap_ = false;
287
// The current character will be handled as part of the next word.
288
continue;
289
}
290
291
if ((flags_ & FLAG_ELLIPSIZE_TEXT) && wordWidth_ > 0.0f && x_ + newWordWidth + ellipsisWidth_ > maxW_) {
292
if ((flags_ & FLAG_WRAP_TEXT) == 0 && x_ + wordWidth_ + ellipsisWidth_ <= maxW_) {
293
// Now, add the word so far (without this latest character) and show the ellipsis.
294
AppendWord(lastEllipsisIndex_ != -1 ? lastEllipsisIndex_ : beforeIndex, -1, false);
295
AddEllipsis();
296
forceEarlyWrap_ = false;
297
skipNextWord_ = true;
298
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
299
scanForNewline_ = true;
300
}
301
continue;
302
}
303
}
304
305
wordWidth_ = newWordWidth;
306
307
// Is this the end of a word via punctuation / CJK?
308
if (wordWidth_ > 0.0f && (IsCJK(c) || IsPunctuation(c) || forceEarlyWrap_)) {
309
// CJK doesn't require spaces, so we treat each letter as its own word.
310
AppendWord(afterIndex, c, false);
311
}
312
}
313
314
// Now insert the rest of the string - the last word.
315
AppendWord((int)len, 0, false);
316
}
317
318