OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 // | 4 // |
5 // This file implements utility functions for eliding and formatting UI text. | 5 // This file implements utility functions for eliding and formatting UI text. |
6 // | 6 // |
7 // Note that several of the functions declared in text_elider.h are implemented | 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. | 8 // in this file using helper classes in an unnamed namespace. |
9 | 9 |
10 #include "ui/gfx/text_elider.h" | 10 #include "ui/gfx/text_elider.h" |
(...skipping 21 matching lines...) Expand all Loading... |
32 | 32 |
33 namespace gfx { | 33 namespace gfx { |
34 | 34 |
35 // U+2026 in utf8 | 35 // U+2026 in utf8 |
36 const char kEllipsis[] = "\xE2\x80\xA6"; | 36 const char kEllipsis[] = "\xE2\x80\xA6"; |
37 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; | 37 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; |
38 const base::char16 kForwardSlash = '/'; | 38 const base::char16 kForwardSlash = '/'; |
39 | 39 |
40 namespace { | 40 namespace { |
41 | 41 |
| 42 // Helper class to split + elide text, while respecting UTF16 surrogate pairs. |
| 43 class StringSlicer { |
| 44 public: |
| 45 StringSlicer(const base::string16& text, |
| 46 const base::string16& ellipsis, |
| 47 bool elide_in_middle) |
| 48 : text_(text), |
| 49 ellipsis_(ellipsis), |
| 50 elide_in_middle_(elide_in_middle) { |
| 51 } |
| 52 |
| 53 // Cuts |text_| to be |length| characters long. If |elide_in_middle_| is true, |
| 54 // the middle of the string is removed to leave equal-length pieces from the |
| 55 // beginning and end of the string; otherwise, the end of the string is |
| 56 // removed and only the beginning remains. If |insert_ellipsis| is true, |
| 57 // then an ellipsis character will be inserted at the cut point. |
| 58 base::string16 CutString(size_t length, bool insert_ellipsis) { |
| 59 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ |
| 60 : base::string16(); |
| 61 |
| 62 if (!elide_in_middle_) |
| 63 return text_.substr(0, FindValidBoundaryBefore(length)) + ellipsis_text; |
| 64 |
| 65 // We put the extra character, if any, before the cut. |
| 66 const size_t half_length = length / 2; |
| 67 const size_t prefix_length = FindValidBoundaryBefore(length - half_length); |
| 68 const size_t suffix_start_guess = text_.length() - half_length; |
| 69 const size_t suffix_start = FindValidBoundaryAfter(suffix_start_guess); |
| 70 const size_t suffix_length = |
| 71 half_length - (suffix_start_guess - suffix_start); |
| 72 return text_.substr(0, prefix_length) + ellipsis_text + |
| 73 text_.substr(suffix_start, suffix_length); |
| 74 } |
| 75 |
| 76 private: |
| 77 // Returns a valid cut boundary at or before |index|. |
| 78 size_t FindValidBoundaryBefore(size_t index) const { |
| 79 DCHECK_LE(index, text_.length()); |
| 80 if (index != text_.length()) |
| 81 U16_SET_CP_START(text_.data(), 0, index); |
| 82 return index; |
| 83 } |
| 84 |
| 85 // Returns a valid cut boundary at or after |index|. |
| 86 size_t FindValidBoundaryAfter(size_t index) const { |
| 87 DCHECK_LE(index, text_.length()); |
| 88 if (index != text_.length()) |
| 89 U16_SET_CP_LIMIT(text_.data(), 0, index, text_.length()); |
| 90 return index; |
| 91 } |
| 92 |
| 93 // The text to be sliced. |
| 94 const base::string16& text_; |
| 95 |
| 96 // Ellipsis string to use. |
| 97 const base::string16& ellipsis_; |
| 98 |
| 99 // If true, the middle of the string will be elided. |
| 100 bool elide_in_middle_; |
| 101 |
| 102 DISALLOW_COPY_AND_ASSIGN(StringSlicer); |
| 103 }; |
42 | 104 |
43 // Build a path from the first |num_components| elements in |path_elements|. | 105 // Build a path from the first |num_components| elements in |path_elements|. |
44 // Prepends |path_prefix|, appends |filename|, inserts ellipsis if appropriate. | 106 // Prepends |path_prefix|, appends |filename|, inserts ellipsis if appropriate. |
45 base::string16 BuildPathFromComponents( | 107 base::string16 BuildPathFromComponents( |
46 const base::string16& path_prefix, | 108 const base::string16& path_prefix, |
47 const std::vector<base::string16>& path_elements, | 109 const std::vector<base::string16>& path_elements, |
48 const base::string16& filename, | 110 const base::string16& filename, |
49 size_t num_components) { | 111 size_t num_components) { |
50 // Add the initial elements of the path. | 112 // Add the initial elements of the path. |
51 base::string16 path = path_prefix; | 113 base::string16 path = path_prefix; |
(...skipping 29 matching lines...) Expand all Loading... |
81 if (available_pixel_width >= GetStringWidthF(elided_path, font_list)) | 143 if (available_pixel_width >= GetStringWidthF(elided_path, font_list)) |
82 return ElideText(elided_path + url_query, font_list, | 144 return ElideText(elided_path + url_query, font_list, |
83 available_pixel_width, ELIDE_AT_END); | 145 available_pixel_width, ELIDE_AT_END); |
84 } | 146 } |
85 | 147 |
86 return base::string16(); | 148 return base::string16(); |
87 } | 149 } |
88 | 150 |
89 } // namespace | 151 } // namespace |
90 | 152 |
91 StringSlicer::StringSlicer(const base::string16& text, | |
92 const base::string16& ellipsis, | |
93 bool elide_in_middle) | |
94 : text_(text), | |
95 ellipsis_(ellipsis), | |
96 elide_in_middle_(elide_in_middle) { | |
97 } | |
98 | |
99 base::string16 StringSlicer::CutString(size_t length, bool insert_ellipsis) { | |
100 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ | |
101 : base::string16(); | |
102 | |
103 if (!elide_in_middle_) | |
104 return text_.substr(0, FindValidBoundaryBefore(length)) + ellipsis_text; | |
105 | |
106 // We put the extra character, if any, before the cut. | |
107 const size_t half_length = length / 2; | |
108 const size_t prefix_length = FindValidBoundaryBefore(length - half_length); | |
109 const size_t suffix_start_guess = text_.length() - half_length; | |
110 const size_t suffix_start = FindValidBoundaryAfter(suffix_start_guess); | |
111 const size_t suffix_length = | |
112 half_length - (suffix_start_guess - suffix_start); | |
113 return text_.substr(0, prefix_length) + ellipsis_text + | |
114 text_.substr(suffix_start, suffix_length); | |
115 } | |
116 | |
117 size_t StringSlicer::FindValidBoundaryBefore(size_t index) const { | |
118 DCHECK_LE(index, text_.length()); | |
119 if (index != text_.length()) | |
120 U16_SET_CP_START(text_.data(), 0, index); | |
121 return index; | |
122 } | |
123 | |
124 size_t StringSlicer::FindValidBoundaryAfter(size_t index) const { | |
125 DCHECK_LE(index, text_.length()); | |
126 if (index != text_.length()) | |
127 U16_SET_CP_LIMIT(text_.data(), 0, index, text_.length()); | |
128 return index; | |
129 } | |
130 | |
131 base::string16 ElideEmail(const base::string16& email, | 153 base::string16 ElideEmail(const base::string16& email, |
132 const FontList& font_list, | 154 const FontList& font_list, |
133 float available_pixel_width) { | 155 float available_pixel_width) { |
134 if (GetStringWidthF(email, font_list) <= available_pixel_width) | 156 if (GetStringWidthF(email, font_list) <= available_pixel_width) |
135 return email; | 157 return email; |
136 | 158 |
137 // Split the email into its local-part (username) and domain-part. The email | 159 // Split the email into its local-part (username) and domain-part. The email |
138 // spec technically allows for @ symbols in the local-part (username) of the | 160 // spec technically allows for @ symbols in the local-part (username) of the |
139 // email under some special requirements. It is guaranteed that there is no @ | 161 // email under some special requirements. It is guaranteed that there is no @ |
140 // symbol in the domain part of the email however so splitting at the last @ | 162 // symbol in the domain part of the email however so splitting at the last @ |
(...skipping 307 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
448 | 470 |
449 // Pango will return 0 width for absurdly long strings. Cut the string in | 471 // Pango will return 0 width for absurdly long strings. Cut the string in |
450 // half and try again. | 472 // half and try again. |
451 // This is caused by an int overflow in Pango (specifically, in | 473 // This is caused by an int overflow in Pango (specifically, in |
452 // pango_glyph_string_extents_range). It's actually more subtle than just | 474 // pango_glyph_string_extents_range). It's actually more subtle than just |
453 // returning 0, since on super absurdly long strings, the int can wrap and | 475 // returning 0, since on super absurdly long strings, the int can wrap and |
454 // return positive numbers again. Detecting that is probably not worth it | 476 // return positive numbers again. Detecting that is probably not worth it |
455 // (eliding way too much from a ridiculous string is probably still | 477 // (eliding way too much from a ridiculous string is probably still |
456 // ridiculous), but we should check other widths for bogus values as well. | 478 // ridiculous), but we should check other widths for bogus values as well. |
457 if (current_text_pixel_width <= 0 && !text.empty()) { | 479 if (current_text_pixel_width <= 0 && !text.empty()) { |
458 const base::string16 cut = | 480 const base::string16 cut = slicer.CutString(text.length() / 2, false); |
459 slicer.CutString(text.length() / 2, insert_ellipsis); | |
460 return ElideText(cut, font_list, available_pixel_width, elide_behavior); | 481 return ElideText(cut, font_list, available_pixel_width, elide_behavior); |
461 } | 482 } |
462 | 483 |
463 if (current_text_pixel_width <= available_pixel_width) | 484 if (current_text_pixel_width <= available_pixel_width) |
464 return text; | 485 return text; |
465 | 486 |
466 if (insert_ellipsis && | 487 if (insert_ellipsis && |
467 GetStringWidthF(ellipsis, font_list) > available_pixel_width) | 488 GetStringWidthF(ellipsis, font_list) > available_pixel_width) |
468 return base::string16(); | 489 return base::string16(); |
469 | 490 |
470 // Use binary search to compute the elided text. | 491 // Use binary search to compute the elided text. |
471 size_t lo = 0; | 492 size_t lo = 0; |
472 size_t hi = text.length() - 1; | 493 size_t hi = text.length() - 1; |
473 size_t guess; | 494 size_t guess; |
474 for (guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { | 495 for (guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { |
475 // We check the width of the whole desired string at once to ensure we | 496 // We check the length of the whole desired string at once to ensure we |
476 // handle kerning/ligatures/etc. correctly. | 497 // handle kerning/ligatures/etc. correctly. |
477 // TODO(skanuj) : Handle directionality of ellipsis based on adjacent | |
478 // characters. See crbug.com/327963. | |
479 const base::string16 cut = slicer.CutString(guess, insert_ellipsis); | 498 const base::string16 cut = slicer.CutString(guess, insert_ellipsis); |
480 const float guess_width = GetStringWidthF(cut, font_list); | 499 const float guess_length = GetStringWidthF(cut, font_list); |
481 if (guess_width == available_pixel_width) | 500 // Check again that we didn't hit a Pango width overflow. If so, cut the |
482 break; | 501 // current string in half and start over. |
483 if (guess_width > available_pixel_width) { | 502 if (guess_length <= 0) { |
| 503 return ElideText(slicer.CutString(guess / 2, false), |
| 504 font_list, available_pixel_width, elide_behavior); |
| 505 } |
| 506 if (guess_length > available_pixel_width) |
484 hi = guess - 1; | 507 hi = guess - 1; |
485 // Move back if we are on loop terminating condition, and guess is wider | 508 else |
486 // than available. | |
487 if (hi < lo) | |
488 lo = hi; | |
489 } else { | |
490 lo = guess + 1; | 509 lo = guess + 1; |
491 } | |
492 } | 510 } |
493 | 511 |
494 return slicer.CutString(guess, insert_ellipsis); | 512 return slicer.CutString(guess, insert_ellipsis); |
495 } | 513 } |
496 | 514 |
497 base::string16 ElideText(const base::string16& text, | 515 base::string16 ElideText(const base::string16& text, |
498 const Font& font, | 516 const Font& font, |
499 float available_pixel_width, | 517 float available_pixel_width, |
500 ElideBehavior elide_behavior) { | 518 ElideBehavior elide_behavior) { |
501 return ElideText(text, FontList(font), available_pixel_width, elide_behavior); | 519 return ElideText(text, FontList(font), available_pixel_width, elide_behavior); |
(...skipping 618 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1120 index = char_iterator.getIndex(); | 1138 index = char_iterator.getIndex(); |
1121 } else { | 1139 } else { |
1122 // String has leading whitespace, return the elide string. | 1140 // String has leading whitespace, return the elide string. |
1123 return kElideString; | 1141 return kElideString; |
1124 } | 1142 } |
1125 } | 1143 } |
1126 return string.substr(0, index) + kElideString; | 1144 return string.substr(0, index) + kElideString; |
1127 } | 1145 } |
1128 | 1146 |
1129 } // namespace gfx | 1147 } // namespace gfx |
OLD | NEW |