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