1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 //
5 // This file implements utility functions for eliding and formatting UI text.
6 //
7 // Note that several of the functions declared in text_elider.h are implemented
8 // in this file using helper classes in an unnamed namespace.
9
10 #include "ui/gfx/text_elider.h"
11
12 #include <stdint.h>
13
14 #include <algorithm>
15 #include <memory>
16 #include <string>
17 #include <vector>
18
19 #include "base/check_op.h"
20 #include "base/files/file_path.h"
21 #include "base/i18n/break_iterator.h"
22 #include "base/i18n/char_iterator.h"
23 #include "base/i18n/rtl.h"
24 #include "base/macros.h"
25 #include "base/notreached.h"
26 #include "base/strings/string_split.h"
27 #include "base/strings/string_util.h"
28 #include "base/strings/sys_string_conversions.h"
29 #include "base/strings/utf_string_conversions.h"
30 #include "build/build_config.h"
31 #include "third_party/icu/source/common/unicode/rbbi.h"
32 #include "third_party/icu/source/common/unicode/uloc.h"
33 #include "third_party/icu/source/common/unicode/umachine.h"
34 #include "ui/gfx/font_list.h"
35 #include "ui/gfx/geometry/rect_conversions.h"
36 #include "ui/gfx/render_text.h"
37 #include "ui/gfx/text_utils.h"
38
39 using base::ASCIIToUTF16;
40 using base::UTF8ToUTF16;
41 using base::WideToUTF16;
42
43 namespace gfx {
44
45 namespace {
46
47 #if defined(OS_IOS)
48 // The returned string will have at least one character besides the ellipsis
49 // on either side of '@'; if that's impossible, a single ellipsis is returned.
50 // If possible, only the username is elided. Otherwise, the domain is elided
51 // in the middle, splitting available width equally with the elided username.
52 // If the username is short enough that it doesn't need half the available
53 // width, the elided domain will occupy that extra width.
ElideEmail(const base::string16 & email,const FontList & font_list,float available_pixel_width)54 base::string16 ElideEmail(const base::string16& email,
55 const FontList& font_list,
56 float available_pixel_width) {
57 if (GetStringWidthF(email, font_list) <= available_pixel_width)
58 return email;
59
60 // Split the email into its local-part (username) and domain-part. The email
61 // spec allows for @ symbols in the username under some special requirements,
62 // but not in the domain part, so splitting at the last @ symbol is safe.
63 const size_t split_index = email.find_last_of('@');
64 DCHECK_NE(split_index, base::string16::npos);
65 base::string16 username = email.substr(0, split_index);
66 base::string16 domain = email.substr(split_index + 1);
67 DCHECK(!username.empty());
68 DCHECK(!domain.empty());
69
70 // Subtract the @ symbol from the available width as it is mandatory.
71 const base::string16 kAtSignUTF16 = ASCIIToUTF16("@");
72 available_pixel_width -= GetStringWidthF(kAtSignUTF16, font_list);
73
74 // Check whether eliding the domain is necessary: if eliding the username
75 // is sufficient, the domain will not be elided.
76 const float full_username_width = GetStringWidthF(username, font_list);
77 const float available_domain_width =
78 available_pixel_width -
79 std::min(full_username_width,
80 GetStringWidthF(username.substr(0, 1) + kEllipsisUTF16,
81 font_list));
82 if (GetStringWidthF(domain, font_list) > available_domain_width) {
83 // Elide the domain so that it only takes half of the available width.
84 // Should the username not need all the width available in its half, the
85 // domain will occupy the leftover width.
86 // If |desired_domain_width| is greater than |available_domain_width|: the
87 // minimal username elision allowed by the specifications will not fit; thus
88 // |desired_domain_width| must be <= |available_domain_width| at all cost.
89 const float desired_domain_width =
90 std::min(available_domain_width,
91 std::max(available_pixel_width - full_username_width,
92 available_pixel_width / 2));
93 domain = ElideText(domain, font_list, desired_domain_width, ELIDE_MIDDLE);
94 // Failing to elide the domain such that at least one character remains
95 // (other than the ellipsis itself) remains: return a single ellipsis.
96 if (domain.length() <= 1U)
97 return base::string16(kEllipsisUTF16);
98 }
99
100 // Fit the username in the remaining width (at this point the elided username
101 // is guaranteed to fit with at least one character remaining given all the
102 // precautions taken earlier).
103 available_pixel_width -= GetStringWidthF(domain, font_list);
104 username = ElideText(username, font_list, available_pixel_width, ELIDE_TAIL);
105 return username + kAtSignUTF16 + domain;
106 }
107 #endif
108
GetDefaultWhitespaceElision(bool elide_in_middle,bool elide_at_beginning)109 bool GetDefaultWhitespaceElision(bool elide_in_middle,
110 bool elide_at_beginning) {
111 return elide_at_beginning || !elide_in_middle;
112 }
113
114 } // namespace
115
116 // U+2026 in utf8
117 const char kEllipsis[] = "\xE2\x80\xA6";
118 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 };
119 const base::char16 kForwardSlash = '/';
120
StringSlicer(const base::string16 & text,const base::string16 & ellipsis,bool elide_in_middle,bool elide_at_beginning,base::Optional<bool> elide_whitespace)121 StringSlicer::StringSlicer(const base::string16& text,
122 const base::string16& ellipsis,
123 bool elide_in_middle,
124 bool elide_at_beginning,
125 base::Optional<bool> elide_whitespace)
126 : text_(text),
127 ellipsis_(ellipsis),
128 elide_in_middle_(elide_in_middle),
129 elide_at_beginning_(elide_at_beginning),
130 elide_whitespace_(elide_whitespace
131 ? *elide_whitespace
132 : GetDefaultWhitespaceElision(elide_in_middle,
133 elide_at_beginning)) {
134 }
135
CutString(size_t length,bool insert_ellipsis) const136 base::string16 StringSlicer::CutString(size_t length,
137 bool insert_ellipsis) const {
138 const base::string16 ellipsis_text =
139 insert_ellipsis ? ellipsis_ : base::string16();
140
141 // For visual consistency, when eliding at either end of the string, excess
142 // space should be trimmed from the text to return "Foo bar..." instead of
143 // "Foo bar ...".
144
145 if (elide_at_beginning_) {
146 return ellipsis_text +
147 text_.substr(FindValidBoundaryAfter(text_, text_.length() - length,
148 elide_whitespace_));
149 }
150
151 if (!elide_in_middle_) {
152 return text_.substr(
153 0, FindValidBoundaryBefore(text_, length, elide_whitespace_)) +
154 ellipsis_text;
155 }
156
157 // Put the extra character, if any, before the cut.
158 // Extra space around the ellipses will *not* be trimmed for |elide_in_middle|
159 // mode (we can change this later). The reason is that when laying out a
160 // column of middle-trimmed lines of text (such as a list of paths), the
161 // desired appearance is to be fully justified and the elipses should more or
162 // less line up; eliminating space would make the text look more ragged.
163 const size_t half_length = length / 2;
164 const size_t prefix_length =
165 FindValidBoundaryBefore(text_, length - half_length, elide_whitespace_);
166 const size_t suffix_start = FindValidBoundaryAfter(
167 text_, text_.length() - half_length, elide_whitespace_);
168 return text_.substr(0, prefix_length) + ellipsis_text +
169 text_.substr(suffix_start);
170 }
171
ElideFilename(const base::FilePath & filename,const FontList & font_list,float available_pixel_width)172 base::string16 ElideFilename(const base::FilePath& filename,
173 const FontList& font_list,
174 float available_pixel_width) {
175 #if defined(OS_WIN)
176 base::string16 filename_utf16 = WideToUTF16(filename.value());
177 base::string16 extension = WideToUTF16(filename.Extension());
178 base::string16 rootname =
179 WideToUTF16(filename.BaseName().RemoveExtension().value());
180 #elif defined(OS_POSIX) || defined(OS_FUCHSIA)
181 base::string16 filename_utf16 = WideToUTF16(base::SysNativeMBToWide(
182 filename.value()));
183 base::string16 extension = WideToUTF16(base::SysNativeMBToWide(
184 filename.Extension()));
185 base::string16 rootname = WideToUTF16(base::SysNativeMBToWide(
186 filename.BaseName().RemoveExtension().value()));
187 #endif
188
189 const float full_width = GetStringWidthF(filename_utf16, font_list);
190 if (full_width <= available_pixel_width)
191 return base::i18n::GetDisplayStringInLTRDirectionality(filename_utf16);
192
193 if (rootname.empty() || extension.empty()) {
194 const base::string16 elided_name =
195 ElideText(filename_utf16, font_list, available_pixel_width, ELIDE_TAIL);
196 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name);
197 }
198
199 const float ext_width = GetStringWidthF(extension, font_list);
200 const float root_width = GetStringWidthF(rootname, font_list);
201
202 // We may have trimmed the path.
203 if (root_width + ext_width <= available_pixel_width) {
204 const base::string16 elided_name = rootname + extension;
205 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name);
206 }
207
208 if (ext_width >= available_pixel_width) {
209 const base::string16 elided_name = ElideText(
210 rootname + extension, font_list, available_pixel_width, ELIDE_MIDDLE);
211 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name);
212 }
213
214 float available_root_width = available_pixel_width - ext_width;
215 base::string16 elided_name =
216 ElideText(rootname, font_list, available_root_width, ELIDE_TAIL);
217 elided_name += extension;
218 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name);
219 }
220
ElideText(const base::string16 & text,const FontList & font_list,float available_pixel_width,ElideBehavior behavior)221 base::string16 ElideText(const base::string16& text,
222 const FontList& font_list,
223 float available_pixel_width,
224 ElideBehavior behavior) {
225 #if !defined(OS_IOS)
226 DCHECK_NE(behavior, FADE_TAIL);
227 std::unique_ptr<RenderText> render_text = RenderText::CreateRenderText();
228 render_text->SetCursorEnabled(false);
229 render_text->SetFontList(font_list);
230 available_pixel_width = std::ceil(available_pixel_width);
231 render_text->SetDisplayRect(
232 gfx::ToEnclosingRect(gfx::RectF(gfx::SizeF(available_pixel_width, 1))));
233 render_text->SetElideBehavior(behavior);
234 render_text->SetText(text);
235 return render_text->GetDisplayText();
236 #else
237 DCHECK_NE(behavior, FADE_TAIL);
238 if (text.empty() || behavior == FADE_TAIL || behavior == NO_ELIDE ||
239 GetStringWidthF(text, font_list) <= available_pixel_width) {
240 return text;
241 }
242 if (behavior == ELIDE_EMAIL)
243 return ElideEmail(text, font_list, available_pixel_width);
244
245 const bool elide_in_middle = (behavior == ELIDE_MIDDLE);
246 const bool elide_at_beginning = (behavior == ELIDE_HEAD);
247 const bool insert_ellipsis = (behavior != TRUNCATE);
248 const base::string16 ellipsis = base::string16(kEllipsisUTF16);
249 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning);
250
251 if (insert_ellipsis &&
252 GetStringWidthF(ellipsis, font_list) > available_pixel_width)
253 return base::string16();
254
255 // Use binary search to compute the elided text.
256 size_t lo = 0;
257 size_t hi = text.length() - 1;
258 size_t guess;
259 base::string16 cut;
260 for (guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) {
261 // We check the width of the whole desired string at once to ensure we
262 // handle kerning/ligatures/etc. correctly.
263 // TODO(skanuj) : Handle directionality of ellipsis based on adjacent
264 // characters. See crbug.com/327963.
265 cut = slicer.CutString(guess, insert_ellipsis);
266 const float guess_width = GetStringWidthF(cut, font_list);
267 if (guess_width == available_pixel_width)
268 break;
269 if (guess_width > available_pixel_width) {
270 hi = guess - 1;
271 // Move back on the loop terminating condition when the guess is too wide.
272 if (hi < lo)
273 lo = hi;
274 } else {
275 lo = guess + 1;
276 }
277 }
278
279 return cut;
280 #endif
281 }
282
ElideString(const base::string16 & input,size_t max_len,base::string16 * output)283 bool ElideString(const base::string16& input,
284 size_t max_len,
285 base::string16* output) {
286 if (input.length() <= max_len) {
287 output->assign(input);
288 return false;
289 }
290
291 switch (max_len) {
292 case 0:
293 output->clear();
294 break;
295 case 1:
296 output->assign(input.substr(0, 1));
297 break;
298 case 2:
299 output->assign(input.substr(0, 2));
300 break;
301 case 3:
302 output->assign(input.substr(0, 1) + ASCIIToUTF16(".") +
303 input.substr(input.length() - 1));
304 break;
305 case 4:
306 output->assign(input.substr(0, 1) + ASCIIToUTF16("..") +
307 input.substr(input.length() - 1));
308 break;
309 default: {
310 size_t rstr_len = (max_len - 3) / 2;
311 size_t lstr_len = rstr_len + ((max_len - 3) % 2);
312 output->assign(input.substr(0, lstr_len) + ASCIIToUTF16("...") +
313 input.substr(input.length() - rstr_len));
314 break;
315 }
316 }
317
318 return true;
319 }
320
321 namespace {
322
323 // Internal class used to track progress of a rectangular string elide
324 // operation. Exists so the top-level ElideRectangleString() function
325 // can be broken into smaller methods sharing this state.
326 class RectangleString {
327 public:
RectangleString(size_t max_rows,size_t max_cols,bool strict,base::string16 * output)328 RectangleString(size_t max_rows, size_t max_cols,
329 bool strict, base::string16 *output)
330 : max_rows_(max_rows),
331 max_cols_(max_cols),
332 current_row_(0),
333 current_col_(0),
334 strict_(strict),
335 suppressed_(false),
336 output_(output) {}
337
338 // Perform deferred initializations following creation. Must be called
339 // before any input can be added via AddString().
Init()340 void Init() { output_->clear(); }
341
342 // Add an input string, reformatting to fit the desired dimensions.
343 // AddString() may be called multiple times to concatenate together
344 // multiple strings into the region (the current caller doesn't do
345 // this, however).
346 void AddString(const base::string16& input);
347
348 // Perform any deferred output processing. Must be called after the
349 // last AddString() call has occurred.
350 bool Finalize();
351
352 private:
353 // Add a line to the rectangular region at the current position,
354 // either by itself or by breaking it into words.
355 void AddLine(const base::string16& line);
356
357 // Add a word to the rectangular region at the current position,
358 // either by itself or by breaking it into characters.
359 void AddWord(const base::string16& word);
360
361 // Add text to the output string if the rectangular boundaries
362 // have not been exceeded, advancing the current position.
363 void Append(const base::string16& string);
364
365 // Set the current position to the beginning of the next line. If
366 // |output| is true, add a newline to the output string if the rectangular
367 // boundaries have not been exceeded. If |output| is false, we assume
368 // some other mechanism will (likely) do similar breaking after the fact.
369 void NewLine(bool output);
370
371 // Maximum number of rows allowed in the output string.
372 size_t max_rows_;
373
374 // Maximum number of characters allowed in the output string.
375 size_t max_cols_;
376
377 // Current row position, always incremented and may exceed max_rows_
378 // when the input can not fit in the region. We stop appending to
379 // the output string, however, when this condition occurs. In the
380 // future, we may want to expose this value to allow the caller to
381 // determine how many rows would actually be required to hold the
382 // formatted string.
383 size_t current_row_;
384
385 // Current character position, should never exceed max_cols_.
386 size_t current_col_;
387
388 // True when we do whitespace to newline conversions ourselves.
389 bool strict_;
390
391 // True when some of the input has been truncated.
392 bool suppressed_;
393
394 // String onto which the output is accumulated.
395 base::string16* output_;
396
397 DISALLOW_COPY_AND_ASSIGN(RectangleString);
398 };
399
AddString(const base::string16 & input)400 void RectangleString::AddString(const base::string16& input) {
401 base::i18n::BreakIterator lines(input,
402 base::i18n::BreakIterator::BREAK_NEWLINE);
403 if (lines.Init()) {
404 while (lines.Advance())
405 AddLine(lines.GetString());
406 } else {
407 NOTREACHED() << "BreakIterator (lines) init failed";
408 }
409 }
410
Finalize()411 bool RectangleString::Finalize() {
412 if (suppressed_) {
413 output_->append(ASCIIToUTF16("..."));
414 return true;
415 }
416 return false;
417 }
418
AddLine(const base::string16 & line)419 void RectangleString::AddLine(const base::string16& line) {
420 if (line.length() < max_cols_) {
421 Append(line);
422 } else {
423 base::i18n::BreakIterator words(line,
424 base::i18n::BreakIterator::BREAK_SPACE);
425 if (words.Init()) {
426 while (words.Advance())
427 AddWord(words.GetString());
428 } else {
429 NOTREACHED() << "BreakIterator (words) init failed";
430 }
431 }
432 // Account for naturally-occuring newlines.
433 ++current_row_;
434 current_col_ = 0;
435 }
436
AddWord(const base::string16 & word)437 void RectangleString::AddWord(const base::string16& word) {
438 if (word.length() < max_cols_) {
439 // Word can be made to fit, no need to fragment it.
440 if (current_col_ + word.length() >= max_cols_)
441 NewLine(strict_);
442 Append(word);
443 } else {
444 // Word is so big that it must be fragmented.
445 size_t array_start = 0;
446 int char_start = 0;
447 base::i18n::UTF16CharIterator chars(word);
448 for (; !chars.end(); chars.Advance()) {
449 // When boundary is hit, add as much as will fit on this line.
450 if (current_col_ + (chars.char_offset() - char_start) >= max_cols_) {
451 Append(word.substr(array_start, chars.array_pos() - array_start));
452 NewLine(true);
453 array_start = chars.array_pos();
454 char_start = chars.char_offset();
455 }
456 }
457 // Add the last remaining fragment, if any.
458 if (array_start != chars.array_pos())
459 Append(word.substr(array_start, chars.array_pos() - array_start));
460 }
461 }
462
Append(const base::string16 & string)463 void RectangleString::Append(const base::string16& string) {
464 if (current_row_ < max_rows_)
465 output_->append(string);
466 else
467 suppressed_ = true;
468 current_col_ += string.length();
469 }
470
NewLine(bool output)471 void RectangleString::NewLine(bool output) {
472 if (current_row_ < max_rows_) {
473 if (output)
474 output_->append(ASCIIToUTF16("\n"));
475 } else {
476 suppressed_ = true;
477 }
478 ++current_row_;
479 current_col_ = 0;
480 }
481
482 // Internal class used to track progress of a rectangular text elide
483 // operation. Exists so the top-level ElideRectangleText() function
484 // can be broken into smaller methods sharing this state.
485 class RectangleText {
486 public:
RectangleText(const FontList & font_list,float available_pixel_width,int available_pixel_height,WordWrapBehavior wrap_behavior,std::vector<base::string16> * lines)487 RectangleText(const FontList& font_list,
488 float available_pixel_width,
489 int available_pixel_height,
490 WordWrapBehavior wrap_behavior,
491 std::vector<base::string16>* lines)
492 : font_list_(font_list),
493 line_height_(font_list.GetHeight()),
494 available_pixel_width_(available_pixel_width),
495 available_pixel_height_(available_pixel_height),
496 wrap_behavior_(wrap_behavior),
497 lines_(lines) {}
498
499 // Perform deferred initializations following creation. Must be called
500 // before any input can be added via AddString().
Init()501 void Init() { lines_->clear(); }
502
503 // Add an input string, reformatting to fit the desired dimensions.
504 // AddString() may be called multiple times to concatenate together
505 // multiple strings into the region (the current caller doesn't do
506 // this, however).
507 void AddString(const base::string16& input);
508
509 // Perform any deferred output processing. Must be called after the last
510 // AddString() call has occurred. Returns a combination of
511 // |ReformattingResultFlags| indicating whether the given width or height was
512 // insufficient, leading to elision or truncation.
513 int Finalize();
514
515 private:
516 // Add a line to the rectangular region at the current position,
517 // either by itself or by breaking it into words.
518 void AddLine(const base::string16& line);
519
520 // Wrap the specified word across multiple lines.
521 int WrapWord(const base::string16& word);
522
523 // Add a long word - wrapping, eliding or truncating per the wrap behavior.
524 int AddWordOverflow(const base::string16& word);
525
526 // Add a word to the rectangular region at the current position.
527 int AddWord(const base::string16& word);
528
529 // Append the specified |text| to the current output line, incrementing the
530 // running width by the specified amount. This is an optimization over
531 // |AddToCurrentLine()| when |text_width| is already known.
532 void AddToCurrentLineWithWidth(const base::string16& text, float text_width);
533
534 // Append the specified |text| to the current output line.
535 void AddToCurrentLine(const base::string16& text);
536
537 // Set the current position to the beginning of the next line.
538 bool NewLine();
539
540 // The font list used for measuring text width.
541 const FontList& font_list_;
542
543 // The height of each line of text.
544 const int line_height_;
545
546 // The number of pixels of available width in the rectangle.
547 const float available_pixel_width_;
548
549 // The number of pixels of available height in the rectangle.
550 const int available_pixel_height_;
551
552 // The wrap behavior for words that are too long to fit on a single line.
553 const WordWrapBehavior wrap_behavior_;
554
555 // The current running width.
556 float current_width_ = 0;
557
558 // The current running height.
559 int current_height_ = 0;
560
561 // The current line of text.
562 base::string16 current_line_;
563
564 // Indicates whether the last line ended with \n.
565 bool last_line_ended_in_lf_ = false;
566
567 // The output vector of lines.
568 std::vector<base::string16>* lines_;
569
570 // Indicates whether a word was so long that it had to be truncated or elided
571 // to fit the available width.
572 bool insufficient_width_ = false;
573
574 // Indicates whether there were too many lines for the available height.
575 bool insufficient_height_ = false;
576
577 // Indicates whether the very first word was truncated.
578 bool first_word_truncated_ = false;
579
580 DISALLOW_COPY_AND_ASSIGN(RectangleText);
581 };
582
AddString(const base::string16 & input)583 void RectangleText::AddString(const base::string16& input) {
584 base::i18n::BreakIterator lines(input,
585 base::i18n::BreakIterator::BREAK_NEWLINE);
586 if (lines.Init()) {
587 while (!insufficient_height_ && lines.Advance()) {
588 base::string16 line = lines.GetString();
589 // The BREAK_NEWLINE iterator will keep the trailing newline character,
590 // except in the case of the last line, which may not have one. Remove
591 // the newline character, if it exists.
592 last_line_ended_in_lf_ = !line.empty() && line.back() == '\n';
593 if (last_line_ended_in_lf_)
594 line.resize(line.length() - 1);
595 AddLine(line);
596 }
597 } else {
598 NOTREACHED() << "BreakIterator (lines) init failed";
599 }
600 }
601
Finalize()602 int RectangleText::Finalize() {
603 // Remove trailing whitespace from the last line or remove the last line
604 // completely, if it's just whitespace.
605 if (!insufficient_height_ && !lines_->empty()) {
606 base::TrimWhitespace(lines_->back(), base::TRIM_TRAILING, &lines_->back());
607 if (lines_->back().empty() && !last_line_ended_in_lf_)
608 lines_->pop_back();
609 }
610 if (last_line_ended_in_lf_)
611 lines_->push_back(base::string16());
612 return (insufficient_width_ ? INSUFFICIENT_SPACE_HORIZONTAL : 0) |
613 (insufficient_height_ ? INSUFFICIENT_SPACE_VERTICAL : 0) |
614 (first_word_truncated_ ? INSUFFICIENT_SPACE_FOR_FIRST_WORD : 0);
615 }
616
AddLine(const base::string16 & line)617 void RectangleText::AddLine(const base::string16& line) {
618 const float line_width = GetStringWidthF(line, font_list_);
619 if (line_width <= available_pixel_width_) {
620 AddToCurrentLineWithWidth(line, line_width);
621 } else {
622 // Iterate over positions that are valid to break the line at. In general,
623 // these are word boundaries but after any punctuation following the word.
624 base::i18n::BreakIterator words(line,
625 base::i18n::BreakIterator::BREAK_LINE);
626 if (words.Init()) {
627 while (words.Advance()) {
628 const bool truncate = !current_line_.empty();
629 const base::string16& word = words.GetString();
630 const int lines_added = AddWord(word);
631 if (lines_added) {
632 if (truncate) {
633 // Trim trailing whitespace from the line that was added.
634 const size_t new_line = lines_->size() - lines_added;
635 base::TrimWhitespace(lines_->at(new_line), base::TRIM_TRAILING,
636 &lines_->at(new_line));
637 }
638 if (base::ContainsOnlyChars(word, base::kWhitespaceUTF16)) {
639 // Skip the first space if the previous line was carried over.
640 current_width_ = 0;
641 current_line_.clear();
642 }
643 }
644 }
645 } else {
646 NOTREACHED() << "BreakIterator (words) init failed";
647 }
648 }
649 // Account for naturally-occuring newlines.
650 NewLine();
651 }
652
WrapWord(const base::string16 & word)653 int RectangleText::WrapWord(const base::string16& word) {
654 // Word is so wide that it must be fragmented.
655 base::string16 text = word;
656 int lines_added = 0;
657 bool first_fragment = true;
658 while (!insufficient_height_ && !text.empty()) {
659 base::string16 fragment =
660 ElideText(text, font_list_, available_pixel_width_, TRUNCATE);
661 // At least one character has to be added at every line, even if the
662 // available space is too small.
663 if (fragment.empty())
664 fragment = text.substr(0, 1);
665 if (!first_fragment && NewLine())
666 lines_added++;
667 AddToCurrentLine(fragment);
668 text = text.substr(fragment.length());
669 first_fragment = false;
670 }
671 return lines_added;
672 }
673
AddWordOverflow(const base::string16 & word)674 int RectangleText::AddWordOverflow(const base::string16& word) {
675 int lines_added = 0;
676
677 // Unless this is the very first word, put it on a new line.
678 if (!current_line_.empty()) {
679 if (!NewLine())
680 return 0;
681 lines_added++;
682 } else if (lines_->empty()) {
683 first_word_truncated_ = true;
684 }
685
686 if (wrap_behavior_ == IGNORE_LONG_WORDS) {
687 current_line_ = word;
688 current_width_ = available_pixel_width_;
689 } else if (wrap_behavior_ == WRAP_LONG_WORDS) {
690 lines_added += WrapWord(word);
691 } else {
692 const ElideBehavior elide_behavior =
693 (wrap_behavior_ == ELIDE_LONG_WORDS ? ELIDE_TAIL : TRUNCATE);
694 const base::string16 elided_word =
695 ElideText(word, font_list_, available_pixel_width_, elide_behavior);
696 AddToCurrentLine(elided_word);
697 insufficient_width_ = true;
698 }
699
700 return lines_added;
701 }
702
AddWord(const base::string16 & word)703 int RectangleText::AddWord(const base::string16& word) {
704 int lines_added = 0;
705 base::string16 trimmed;
706 base::TrimWhitespace(word, base::TRIM_TRAILING, &trimmed);
707 const float trimmed_width = GetStringWidthF(trimmed, font_list_);
708 if (trimmed_width <= available_pixel_width_) {
709 // Word can be made to fit, no need to fragment it.
710 if ((current_width_ + trimmed_width > available_pixel_width_) && NewLine())
711 lines_added++;
712 // Append the non-trimmed word, in case more words are added after.
713 AddToCurrentLine(word);
714 } else {
715 lines_added = AddWordOverflow(wrap_behavior_ == IGNORE_LONG_WORDS ?
716 trimmed : word);
717 }
718 return lines_added;
719 }
720
AddToCurrentLine(const base::string16 & text)721 void RectangleText::AddToCurrentLine(const base::string16& text) {
722 AddToCurrentLineWithWidth(text, GetStringWidthF(text, font_list_));
723 }
724
AddToCurrentLineWithWidth(const base::string16 & text,float text_width)725 void RectangleText::AddToCurrentLineWithWidth(const base::string16& text,
726 float text_width) {
727 if (current_height_ >= available_pixel_height_) {
728 insufficient_height_ = true;
729 return;
730 }
731 current_line_.append(text);
732 current_width_ += text_width;
733 }
734
NewLine()735 bool RectangleText::NewLine() {
736 bool line_added = false;
737 if (current_height_ < available_pixel_height_) {
738 lines_->push_back(current_line_);
739 current_line_.clear();
740 line_added = true;
741 } else {
742 insufficient_height_ = true;
743 }
744 current_height_ += line_height_;
745 current_width_ = 0;
746 return line_added;
747 }
748
749 } // namespace
750
ElideRectangleString(const base::string16 & input,size_t max_rows,size_t max_cols,bool strict,base::string16 * output)751 bool ElideRectangleString(const base::string16& input, size_t max_rows,
752 size_t max_cols, bool strict,
753 base::string16* output) {
754 RectangleString rect(max_rows, max_cols, strict, output);
755 rect.Init();
756 rect.AddString(input);
757 return rect.Finalize();
758 }
759
ElideRectangleText(const base::string16 & input,const FontList & font_list,float available_pixel_width,int available_pixel_height,WordWrapBehavior wrap_behavior,std::vector<base::string16> * lines)760 int ElideRectangleText(const base::string16& input,
761 const FontList& font_list,
762 float available_pixel_width,
763 int available_pixel_height,
764 WordWrapBehavior wrap_behavior,
765 std::vector<base::string16>* lines) {
766 RectangleText rect(font_list, available_pixel_width, available_pixel_height,
767 wrap_behavior, lines);
768 rect.Init();
769 rect.AddString(input);
770 return rect.Finalize();
771 }
772
TruncateString(const base::string16 & string,size_t length,BreakType break_type)773 base::string16 TruncateString(const base::string16& string,
774 size_t length,
775 BreakType break_type) {
776 const bool word_break = break_type == WORD_BREAK;
777 DCHECK(word_break || (break_type == CHARACTER_BREAK));
778
779 if (string.size() <= length)
780 return string; // No need to elide.
781
782 if (length == 0)
783 return base::string16(); // No room for anything, even an ellipsis.
784
785 // Added to the end of strings that are too big.
786 static const base::char16 kElideString[] = { 0x2026, 0 };
787
788 if (length == 1)
789 return kElideString; // Only room for an ellipsis.
790
791 int32_t index = static_cast<int32_t>(length - 1);
792 if (word_break) {
793 // Use a word iterator to find the first boundary.
794 UErrorCode status = U_ZERO_ERROR;
795 std::unique_ptr<icu::BreakIterator> bi(
796 icu::RuleBasedBreakIterator::createWordInstance(
797 icu::Locale::getDefault(), status));
798 if (U_FAILURE(status))
799 return string.substr(0, length - 1) + kElideString;
800 icu::UnicodeString bi_text(string.c_str());
801 bi->setText(bi_text);
802 index = bi->preceding(static_cast<int32_t>(length));
803 if (index == icu::BreakIterator::DONE || index == 0) {
804 // We either found no valid word break at all, or one right at the
805 // beginning of the string. Go back to the end; we'll have to break in the
806 // middle of a word.
807 index = static_cast<int32_t>(length - 1);
808 }
809 }
810
811 // By this point, |index| should point at the character that's a candidate for
812 // replacing with an ellipsis. Use a character iterator to check previous
813 // characters and stop as soon as we find a previous non-whitespace character.
814 icu::StringCharacterIterator char_iterator(string.c_str());
815 char_iterator.setIndex(index);
816 while (char_iterator.hasPrevious()) {
817 char_iterator.previous();
818 if (!(u_isspace(char_iterator.current()) ||
819 u_charType(char_iterator.current()) == U_CONTROL_CHAR ||
820 u_charType(char_iterator.current()) == U_NON_SPACING_MARK)) {
821 // Not a whitespace character. Truncate to everything up to and including
822 // this character, and append an ellipsis.
823 char_iterator.next();
824 return string.substr(0, char_iterator.getIndex()) + kElideString;
825 }
826 }
827
828 // Couldn't find a previous non-whitespace character. If we were originally
829 // word-breaking, and index != length - 1, then the string is |index|
830 // whitespace characters followed by a word we're trying to break in the midst
831 // of, and we can fit at least one character of the word in the elided string.
832 // Do that rather than just returning an ellipsis.
833 if (word_break && (index != static_cast<int32_t>(length - 1)))
834 return string.substr(0, length - 1) + kElideString;
835
836 // Trying to break after only whitespace, elide all of it.
837 return kElideString;
838 }
839
840 } // namespace gfx
841