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 14 matching lines...) Expand all Loading... |
25 #include "third_party/icu/source/common/unicode/uloc.h" | 25 #include "third_party/icu/source/common/unicode/uloc.h" |
26 #include "ui/gfx/font_list.h" | 26 #include "ui/gfx/font_list.h" |
27 #include "ui/gfx/text_utils.h" | 27 #include "ui/gfx/text_utils.h" |
28 | 28 |
29 using base::ASCIIToUTF16; | 29 using base::ASCIIToUTF16; |
30 using base::UTF8ToUTF16; | 30 using base::UTF8ToUTF16; |
31 using base::WideToUTF16; | 31 using base::WideToUTF16; |
32 | 32 |
33 namespace gfx { | 33 namespace gfx { |
34 | 34 |
35 // U+2026 in utf8 | 35 namespace { |
36 const char kEllipsis[] = "\xE2\x80\xA6"; | |
37 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; | |
38 const base::char16 kForwardSlash = '/'; | |
39 | 36 |
40 StringSlicer::StringSlicer(const base::string16& text, | 37 // Elides a well-formed email address (e.g. username@domain.com) to fit into |
41 const base::string16& ellipsis, | 38 // |available_pixel_width| using the specified |font_list|. |
42 bool elide_in_middle, | 39 // This function guarantees that the string returned will contain at least one |
43 bool elide_at_beginning) | 40 // character, other than the ellipses, on either side of the '@'. If it is |
44 : text_(text), | 41 // impossible to achieve these requirements: only an ellipsis will be returned. |
45 ellipsis_(ellipsis), | 42 // If possible: this elides only the username portion of the |email|. Otherwise, |
46 elide_in_middle_(elide_in_middle), | 43 // the domain is elided in the middle so that it splits the available width |
47 elide_at_beginning_(elide_at_beginning) { | 44 // equally with the elided username (should the username be short enough that it |
48 } | 45 // doesn't need half the available width: the elided domain will occupy that |
49 | 46 // extra width). |
50 base::string16 StringSlicer::CutString(size_t length, bool insert_ellipsis) { | |
51 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ | |
52 : base::string16(); | |
53 | |
54 if (elide_at_beginning_) | |
55 return ellipsis_text + | |
56 text_.substr(FindValidBoundaryBefore(text_.length() - length)); | |
57 | |
58 if (!elide_in_middle_) | |
59 return text_.substr(0, FindValidBoundaryBefore(length)) + ellipsis_text; | |
60 | |
61 // We put the extra character, if any, before the cut. | |
62 const size_t half_length = length / 2; | |
63 const size_t prefix_length = FindValidBoundaryBefore(length - half_length); | |
64 const size_t suffix_start_guess = text_.length() - half_length; | |
65 const size_t suffix_start = FindValidBoundaryAfter(suffix_start_guess); | |
66 const size_t suffix_length = | |
67 half_length - (suffix_start_guess - suffix_start); | |
68 return text_.substr(0, prefix_length) + ellipsis_text + | |
69 text_.substr(suffix_start, suffix_length); | |
70 } | |
71 | |
72 size_t StringSlicer::FindValidBoundaryBefore(size_t index) const { | |
73 DCHECK_LE(index, text_.length()); | |
74 if (index != text_.length()) | |
75 U16_SET_CP_START(text_.data(), 0, index); | |
76 return index; | |
77 } | |
78 | |
79 size_t StringSlicer::FindValidBoundaryAfter(size_t index) const { | |
80 DCHECK_LE(index, text_.length()); | |
81 if (index != text_.length()) | |
82 U16_SET_CP_LIMIT(text_.data(), 0, index, text_.length()); | |
83 return index; | |
84 } | |
85 | |
86 base::string16 ElideEmail(const base::string16& email, | 47 base::string16 ElideEmail(const base::string16& email, |
87 const FontList& font_list, | 48 const FontList& font_list, |
88 float available_pixel_width) { | 49 float available_pixel_width) { |
89 if (GetStringWidthF(email, font_list) <= available_pixel_width) | 50 if (GetStringWidthF(email, font_list) <= available_pixel_width) |
90 return email; | 51 return email; |
91 | 52 |
92 // Split the email into its local-part (username) and domain-part. The email | 53 // Split the email into its local-part (username) and domain-part. The email |
93 // spec technically allows for @ symbols in the local-part (username) of the | 54 // spec technically allows for @ symbols in the local-part (username) of the |
94 // email under some special requirements. It is guaranteed that there is no @ | 55 // email under some special requirements. It is guaranteed that there is no @ |
95 // symbol in the domain part of the email however so splitting at the last @ | 56 // symbol in the domain part of the email however so splitting at the last @ |
(...skipping 21 matching lines...) Expand all Loading... |
117 // Elide the domain so that it only takes half of the available width. | 78 // Elide the domain so that it only takes half of the available width. |
118 // Should the username not need all the width available in its half, the | 79 // Should the username not need all the width available in its half, the |
119 // domain will occupy the leftover width. | 80 // domain will occupy the leftover width. |
120 // If |desired_domain_width| is greater than |available_domain_width|: the | 81 // If |desired_domain_width| is greater than |available_domain_width|: the |
121 // minimal username elision allowed by the specifications will not fit; thus | 82 // minimal username elision allowed by the specifications will not fit; thus |
122 // |desired_domain_width| must be <= |available_domain_width| at all cost. | 83 // |desired_domain_width| must be <= |available_domain_width| at all cost. |
123 const float desired_domain_width = | 84 const float desired_domain_width = |
124 std::min(available_domain_width, | 85 std::min(available_domain_width, |
125 std::max(available_pixel_width - full_username_width, | 86 std::max(available_pixel_width - full_username_width, |
126 available_pixel_width / 2)); | 87 available_pixel_width / 2)); |
127 domain = ElideText(domain, font_list, desired_domain_width, | 88 domain = ElideText(domain, font_list, desired_domain_width, ELIDE_MIDDLE); |
128 ELIDE_IN_MIDDLE); | |
129 // Failing to elide the domain such that at least one character remains | 89 // Failing to elide the domain such that at least one character remains |
130 // (other than the ellipsis itself) remains: return a single ellipsis. | 90 // (other than the ellipsis itself) remains: return a single ellipsis. |
131 if (domain.length() <= 1U) | 91 if (domain.length() <= 1U) |
132 return base::string16(kEllipsisUTF16); | 92 return base::string16(kEllipsisUTF16); |
133 } | 93 } |
134 | 94 |
135 // Fit the username in the remaining width (at this point the elided username | 95 // Fit the username in the remaining width (at this point the elided username |
136 // is guaranteed to fit with at least one character remaining given all the | 96 // is guaranteed to fit with at least one character remaining given all the |
137 // precautions taken earlier). | 97 // precautions taken earlier). |
138 available_pixel_width -= GetStringWidthF(domain, font_list); | 98 available_pixel_width -= GetStringWidthF(domain, font_list); |
139 username = ElideText(username, font_list, available_pixel_width, | 99 username = ElideText(username, font_list, available_pixel_width, ELIDE_TAIL); |
140 ELIDE_AT_END); | 100 return username + kAtSignUTF16 + domain; |
| 101 } |
141 | 102 |
142 return username + kAtSignUTF16 + domain; | 103 } // namespace |
| 104 |
| 105 // U+2026 in utf8 |
| 106 const char kEllipsis[] = "\xE2\x80\xA6"; |
| 107 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; |
| 108 const base::char16 kForwardSlash = '/'; |
| 109 |
| 110 StringSlicer::StringSlicer(const base::string16& text, |
| 111 const base::string16& ellipsis, |
| 112 bool elide_in_middle, |
| 113 bool elide_at_beginning) |
| 114 : text_(text), |
| 115 ellipsis_(ellipsis), |
| 116 elide_in_middle_(elide_in_middle), |
| 117 elide_at_beginning_(elide_at_beginning) { |
| 118 } |
| 119 |
| 120 base::string16 StringSlicer::CutString(size_t length, bool insert_ellipsis) { |
| 121 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ |
| 122 : base::string16(); |
| 123 |
| 124 if (elide_at_beginning_) |
| 125 return ellipsis_text + |
| 126 text_.substr(FindValidBoundaryBefore(text_.length() - length)); |
| 127 |
| 128 if (!elide_in_middle_) |
| 129 return text_.substr(0, FindValidBoundaryBefore(length)) + ellipsis_text; |
| 130 |
| 131 // We put the extra character, if any, before the cut. |
| 132 const size_t half_length = length / 2; |
| 133 const size_t prefix_length = FindValidBoundaryBefore(length - half_length); |
| 134 const size_t suffix_start_guess = text_.length() - half_length; |
| 135 const size_t suffix_start = FindValidBoundaryAfter(suffix_start_guess); |
| 136 const size_t suffix_length = |
| 137 half_length - (suffix_start_guess - suffix_start); |
| 138 return text_.substr(0, prefix_length) + ellipsis_text + |
| 139 text_.substr(suffix_start, suffix_length); |
| 140 } |
| 141 |
| 142 size_t StringSlicer::FindValidBoundaryBefore(size_t index) const { |
| 143 DCHECK_LE(index, text_.length()); |
| 144 if (index != text_.length()) |
| 145 U16_SET_CP_START(text_.data(), 0, index); |
| 146 return index; |
| 147 } |
| 148 |
| 149 size_t StringSlicer::FindValidBoundaryAfter(size_t index) const { |
| 150 DCHECK_LE(index, text_.length()); |
| 151 if (index != text_.length()) |
| 152 U16_SET_CP_LIMIT(text_.data(), 0, index, text_.length()); |
| 153 return index; |
143 } | 154 } |
144 | 155 |
145 base::string16 ElideFilename(const base::FilePath& filename, | 156 base::string16 ElideFilename(const base::FilePath& filename, |
146 const FontList& font_list, | 157 const FontList& font_list, |
147 float available_pixel_width) { | 158 float available_pixel_width) { |
148 #if defined(OS_WIN) | 159 #if defined(OS_WIN) |
149 base::string16 filename_utf16 = filename.value(); | 160 base::string16 filename_utf16 = filename.value(); |
150 base::string16 extension = filename.Extension(); | 161 base::string16 extension = filename.Extension(); |
151 base::string16 rootname = filename.BaseName().RemoveExtension().value(); | 162 base::string16 rootname = filename.BaseName().RemoveExtension().value(); |
152 #elif defined(OS_POSIX) | 163 #elif defined(OS_POSIX) |
153 base::string16 filename_utf16 = WideToUTF16(base::SysNativeMBToWide( | 164 base::string16 filename_utf16 = WideToUTF16(base::SysNativeMBToWide( |
154 filename.value())); | 165 filename.value())); |
155 base::string16 extension = WideToUTF16(base::SysNativeMBToWide( | 166 base::string16 extension = WideToUTF16(base::SysNativeMBToWide( |
156 filename.Extension())); | 167 filename.Extension())); |
157 base::string16 rootname = WideToUTF16(base::SysNativeMBToWide( | 168 base::string16 rootname = WideToUTF16(base::SysNativeMBToWide( |
158 filename.BaseName().RemoveExtension().value())); | 169 filename.BaseName().RemoveExtension().value())); |
159 #endif | 170 #endif |
160 | 171 |
161 const float full_width = GetStringWidthF(filename_utf16, font_list); | 172 const float full_width = GetStringWidthF(filename_utf16, font_list); |
162 if (full_width <= available_pixel_width) | 173 if (full_width <= available_pixel_width) |
163 return base::i18n::GetDisplayStringInLTRDirectionality(filename_utf16); | 174 return base::i18n::GetDisplayStringInLTRDirectionality(filename_utf16); |
164 | 175 |
165 if (rootname.empty() || extension.empty()) { | 176 if (rootname.empty() || extension.empty()) { |
166 const base::string16 elided_name = ElideText(filename_utf16, font_list, | 177 const base::string16 elided_name = |
167 available_pixel_width, ELIDE_AT_END); | 178 ElideText(filename_utf16, font_list, available_pixel_width, ELIDE_TAIL); |
168 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); | 179 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); |
169 } | 180 } |
170 | 181 |
171 const float ext_width = GetStringWidthF(extension, font_list); | 182 const float ext_width = GetStringWidthF(extension, font_list); |
172 const float root_width = GetStringWidthF(rootname, font_list); | 183 const float root_width = GetStringWidthF(rootname, font_list); |
173 | 184 |
174 // We may have trimmed the path. | 185 // We may have trimmed the path. |
175 if (root_width + ext_width <= available_pixel_width) { | 186 if (root_width + ext_width <= available_pixel_width) { |
176 const base::string16 elided_name = rootname + extension; | 187 const base::string16 elided_name = rootname + extension; |
177 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); | 188 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); |
178 } | 189 } |
179 | 190 |
180 if (ext_width >= available_pixel_width) { | 191 if (ext_width >= available_pixel_width) { |
181 const base::string16 elided_name = ElideText( | 192 const base::string16 elided_name = ElideText( |
182 rootname + extension, font_list, available_pixel_width, | 193 rootname + extension, font_list, available_pixel_width, ELIDE_MIDDLE); |
183 ELIDE_IN_MIDDLE); | |
184 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); | 194 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); |
185 } | 195 } |
186 | 196 |
187 float available_root_width = available_pixel_width - ext_width; | 197 float available_root_width = available_pixel_width - ext_width; |
188 base::string16 elided_name = | 198 base::string16 elided_name = |
189 ElideText(rootname, font_list, available_root_width, ELIDE_AT_END); | 199 ElideText(rootname, font_list, available_root_width, ELIDE_TAIL); |
190 elided_name += extension; | 200 elided_name += extension; |
191 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); | 201 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); |
192 } | 202 } |
193 | 203 |
194 base::string16 ElideText(const base::string16& text, | 204 base::string16 ElideText(const base::string16& text, |
195 const FontList& font_list, | 205 const FontList& font_list, |
196 float available_pixel_width, | 206 float available_pixel_width, |
197 ElideBehavior elide_behavior) { | 207 ElideBehavior behavior) { |
198 if (text.empty()) | 208 DCHECK_NE(behavior, FADE_TAIL); |
| 209 if (text.empty() || behavior == FADE_TAIL) |
199 return text; | 210 return text; |
| 211 if (behavior == ELIDE_EMAIL) |
| 212 return ElideEmail(text, font_list, available_pixel_width); |
200 | 213 |
201 const float current_text_pixel_width = GetStringWidthF(text, font_list); | 214 const float current_text_pixel_width = GetStringWidthF(text, font_list); |
202 const bool elide_in_middle = (elide_behavior == ELIDE_IN_MIDDLE); | 215 const bool elide_in_middle = (behavior == ELIDE_MIDDLE); |
203 const bool elide_at_beginning = (elide_behavior == ELIDE_AT_BEGINNING); | 216 const bool elide_at_beginning = (behavior == ELIDE_HEAD); |
204 const bool insert_ellipsis = (elide_behavior != TRUNCATE_AT_END); | 217 const bool insert_ellipsis = (behavior != TRUNCATE); |
205 | |
206 const base::string16 ellipsis = base::string16(kEllipsisUTF16); | 218 const base::string16 ellipsis = base::string16(kEllipsisUTF16); |
207 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); | 219 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); |
208 | 220 |
209 // Pango will return 0 width for absurdly long strings. Cut the string in | 221 // Pango will return 0 width for absurdly long strings. Cut the string in |
210 // half and try again. | 222 // half and try again. |
211 // This is caused by an int overflow in Pango (specifically, in | 223 // This is caused by an int overflow in Pango (specifically, in |
212 // pango_glyph_string_extents_range). It's actually more subtle than just | 224 // pango_glyph_string_extents_range). It's actually more subtle than just |
213 // returning 0, since on super absurdly long strings, the int can wrap and | 225 // returning 0, since on super absurdly long strings, the int can wrap and |
214 // return positive numbers again. Detecting that is probably not worth it | 226 // return positive numbers again. Detecting that is probably not worth it |
215 // (eliding way too much from a ridiculous string is probably still | 227 // (eliding way too much from a ridiculous string is probably still |
216 // ridiculous), but we should check other widths for bogus values as well. | 228 // ridiculous), but we should check other widths for bogus values as well. |
217 if (current_text_pixel_width <= 0 && !text.empty()) { | 229 if (current_text_pixel_width <= 0) { |
218 const base::string16 cut = | 230 const base::string16 cut = |
219 slicer.CutString(text.length() / 2, insert_ellipsis); | 231 slicer.CutString(text.length() / 2, insert_ellipsis); |
220 return ElideText(cut, font_list, available_pixel_width, elide_behavior); | 232 return ElideText(cut, font_list, available_pixel_width, behavior); |
221 } | 233 } |
222 | 234 |
223 if (current_text_pixel_width <= available_pixel_width) | 235 if (current_text_pixel_width <= available_pixel_width) |
224 return text; | 236 return text; |
225 | 237 |
226 if (insert_ellipsis && | 238 if (insert_ellipsis && |
227 GetStringWidthF(ellipsis, font_list) > available_pixel_width) | 239 GetStringWidthF(ellipsis, font_list) > available_pixel_width) |
228 return base::string16(); | 240 return base::string16(); |
229 | 241 |
230 // Use binary search to compute the elided text. | 242 // Use binary search to compute the elided text. |
(...skipping 16 matching lines...) Expand all Loading... |
247 if (hi < lo) | 259 if (hi < lo) |
248 lo = hi; | 260 lo = hi; |
249 } else { | 261 } else { |
250 lo = guess + 1; | 262 lo = guess + 1; |
251 } | 263 } |
252 } | 264 } |
253 | 265 |
254 return slicer.CutString(guess, insert_ellipsis); | 266 return slicer.CutString(guess, insert_ellipsis); |
255 } | 267 } |
256 | 268 |
257 bool ElideString(const base::string16& input, int max_len, | 269 bool ElideString(const base::string16& input, |
| 270 int max_len, |
258 base::string16* output) { | 271 base::string16* output) { |
259 DCHECK_GE(max_len, 0); | 272 DCHECK_GE(max_len, 0); |
260 if (static_cast<int>(input.length()) <= max_len) { | 273 if (static_cast<int>(input.length()) <= max_len) { |
261 output->assign(input); | 274 output->assign(input); |
262 return false; | 275 return false; |
263 } | 276 } |
264 | 277 |
265 switch (max_len) { | 278 switch (max_len) { |
266 case 0: | 279 case 0: |
267 output->clear(); | 280 output->clear(); |
(...skipping 358 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
626 NewLine(); | 639 NewLine(); |
627 } | 640 } |
628 | 641 |
629 int RectangleText::WrapWord(const base::string16& word) { | 642 int RectangleText::WrapWord(const base::string16& word) { |
630 // Word is so wide that it must be fragmented. | 643 // Word is so wide that it must be fragmented. |
631 base::string16 text = word; | 644 base::string16 text = word; |
632 int lines_added = 0; | 645 int lines_added = 0; |
633 bool first_fragment = true; | 646 bool first_fragment = true; |
634 while (!insufficient_height_ && !text.empty()) { | 647 while (!insufficient_height_ && !text.empty()) { |
635 base::string16 fragment = | 648 base::string16 fragment = |
636 ElideText(text, font_list_, available_pixel_width_, | 649 ElideText(text, font_list_, available_pixel_width_, TRUNCATE); |
637 TRUNCATE_AT_END); | |
638 // At least one character has to be added at every line, even if the | 650 // At least one character has to be added at every line, even if the |
639 // available space is too small. | 651 // available space is too small. |
640 if(fragment.empty()) | 652 if (fragment.empty()) |
641 fragment = text.substr(0, 1); | 653 fragment = text.substr(0, 1); |
642 if (!first_fragment && NewLine()) | 654 if (!first_fragment && NewLine()) |
643 lines_added++; | 655 lines_added++; |
644 AddToCurrentLine(fragment); | 656 AddToCurrentLine(fragment); |
645 text = text.substr(fragment.length()); | 657 text = text.substr(fragment.length()); |
646 first_fragment = false; | 658 first_fragment = false; |
647 } | 659 } |
648 return lines_added; | 660 return lines_added; |
649 } | 661 } |
650 | 662 |
651 int RectangleText::AddWordOverflow(const base::string16& word) { | 663 int RectangleText::AddWordOverflow(const base::string16& word) { |
652 int lines_added = 0; | 664 int lines_added = 0; |
653 | 665 |
654 // Unless this is the very first word, put it on a new line. | 666 // Unless this is the very first word, put it on a new line. |
655 if (!current_line_.empty()) { | 667 if (!current_line_.empty()) { |
656 if (!NewLine()) | 668 if (!NewLine()) |
657 return 0; | 669 return 0; |
658 lines_added++; | 670 lines_added++; |
659 } | 671 } |
660 | 672 |
661 if (wrap_behavior_ == IGNORE_LONG_WORDS) { | 673 if (wrap_behavior_ == IGNORE_LONG_WORDS) { |
662 current_line_ = word; | 674 current_line_ = word; |
663 current_width_ = available_pixel_width_; | 675 current_width_ = available_pixel_width_; |
664 } else if (wrap_behavior_ == WRAP_LONG_WORDS) { | 676 } else if (wrap_behavior_ == WRAP_LONG_WORDS) { |
665 lines_added += WrapWord(word); | 677 lines_added += WrapWord(word); |
666 } else { | 678 } else { |
667 const ElideBehavior elide_behavior = | 679 const ElideBehavior elide_behavior = |
668 (wrap_behavior_ == ELIDE_LONG_WORDS ? ELIDE_AT_END : TRUNCATE_AT_END); | 680 (wrap_behavior_ == ELIDE_LONG_WORDS ? ELIDE_TAIL : TRUNCATE); |
669 const base::string16 elided_word = | 681 const base::string16 elided_word = |
670 ElideText(word, font_list_, available_pixel_width_, elide_behavior); | 682 ElideText(word, font_list_, available_pixel_width_, elide_behavior); |
671 AddToCurrentLine(elided_word); | 683 AddToCurrentLine(elided_word); |
672 insufficient_width_ = true; | 684 insufficient_width_ = true; |
673 } | 685 } |
674 | 686 |
675 return lines_added; | 687 return lines_added; |
676 } | 688 } |
677 | 689 |
678 int RectangleText::AddWord(const base::string16& word) { | 690 int RectangleText::AddWord(const base::string16& word) { |
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
804 index = char_iterator.getIndex(); | 816 index = char_iterator.getIndex(); |
805 } else { | 817 } else { |
806 // String has leading whitespace, return the elide string. | 818 // String has leading whitespace, return the elide string. |
807 return kElideString; | 819 return kElideString; |
808 } | 820 } |
809 } | 821 } |
810 return string.substr(0, index) + kElideString; | 822 return string.substr(0, index) + kElideString; |
811 } | 823 } |
812 | 824 |
813 } // namespace gfx | 825 } // namespace gfx |
OLD | NEW |