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" |
11 | 11 |
12 #include <string> | 12 #include <string> |
13 #include <vector> | 13 #include <vector> |
14 | 14 |
15 #include "base/files/file_path.h" | 15 #include "base/files/file_path.h" |
16 #include "base/i18n/break_iterator.h" | 16 #include "base/i18n/break_iterator.h" |
17 #include "base/i18n/char_iterator.h" | 17 #include "base/i18n/char_iterator.h" |
18 #include "base/i18n/rtl.h" | 18 #include "base/i18n/rtl.h" |
19 #include "base/memory/scoped_ptr.h" | 19 #include "base/memory/scoped_ptr.h" |
20 #include "base/strings/string_split.h" | 20 #include "base/strings/string_split.h" |
21 #include "base/strings/string_util.h" | 21 #include "base/strings/string_util.h" |
22 #include "base/strings/sys_string_conversions.h" | 22 #include "base/strings/sys_string_conversions.h" |
23 #include "base/strings/utf_string_conversions.h" | 23 #include "base/strings/utf_string_conversions.h" |
24 #include "third_party/icu/source/common/unicode/rbbi.h" | 24 #include "third_party/icu/source/common/unicode/rbbi.h" |
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/render_text.h" |
27 #include "ui/gfx/text_utils.h" | 28 #include "ui/gfx/text_utils.h" |
28 | 29 |
29 using base::ASCIIToUTF16; | 30 using base::ASCIIToUTF16; |
30 using base::UTF8ToUTF16; | 31 using base::UTF8ToUTF16; |
31 using base::WideToUTF16; | 32 using base::WideToUTF16; |
32 | 33 |
33 namespace gfx { | 34 namespace gfx { |
34 | 35 |
35 namespace { | |
36 | |
37 // Elides a well-formed email address (e.g. username@domain.com) to fit into | |
38 // |available_pixel_width| using the specified |font_list|. | |
39 // This function guarantees that the string returned will contain at least one | |
40 // character, other than the ellipses, on either side of the '@'. If it is | |
41 // impossible to achieve these requirements: only an ellipsis will be returned. | |
42 // If possible: this elides only the username portion of the |email|. Otherwise, | |
43 // the domain is elided in the middle so that it splits the available width | |
44 // equally with the elided username (should the username be short enough that it | |
45 // doesn't need half the available width: the elided domain will occupy that | |
46 // extra width). | |
47 base::string16 ElideEmail(const base::string16& email, | |
48 const FontList& font_list, | |
49 float available_pixel_width) { | |
50 if (GetStringWidthF(email, font_list) <= available_pixel_width) | |
51 return email; | |
52 | |
53 // Split the email into its local-part (username) and domain-part. The email | |
54 // spec technically allows for @ symbols in the local-part (username) of the | |
55 // email under some special requirements. It is guaranteed that there is no @ | |
56 // symbol in the domain part of the email however so splitting at the last @ | |
57 // symbol is safe. | |
58 const size_t split_index = email.find_last_of('@'); | |
59 DCHECK_NE(split_index, base::string16::npos); | |
60 base::string16 username = email.substr(0, split_index); | |
61 base::string16 domain = email.substr(split_index + 1); | |
62 DCHECK(!username.empty()); | |
63 DCHECK(!domain.empty()); | |
64 | |
65 // Subtract the @ symbol from the available width as it is mandatory. | |
66 const base::string16 kAtSignUTF16 = ASCIIToUTF16("@"); | |
67 available_pixel_width -= GetStringWidthF(kAtSignUTF16, font_list); | |
68 | |
69 // Check whether eliding the domain is necessary: if eliding the username | |
70 // is sufficient, the domain will not be elided. | |
71 const float full_username_width = GetStringWidthF(username, font_list); | |
72 const float available_domain_width = | |
73 available_pixel_width - | |
74 std::min(full_username_width, | |
75 GetStringWidthF(username.substr(0, 1) + kEllipsisUTF16, | |
76 font_list)); | |
77 if (GetStringWidthF(domain, font_list) > available_domain_width) { | |
78 // Elide the domain so that it only takes half of the available width. | |
79 // Should the username not need all the width available in its half, the | |
80 // domain will occupy the leftover width. | |
81 // If |desired_domain_width| is greater than |available_domain_width|: the | |
82 // minimal username elision allowed by the specifications will not fit; thus | |
83 // |desired_domain_width| must be <= |available_domain_width| at all cost. | |
84 const float desired_domain_width = | |
85 std::min(available_domain_width, | |
86 std::max(available_pixel_width - full_username_width, | |
87 available_pixel_width / 2)); | |
88 domain = ElideText(domain, font_list, desired_domain_width, ELIDE_MIDDLE); | |
89 // Failing to elide the domain such that at least one character remains | |
90 // (other than the ellipsis itself) remains: return a single ellipsis. | |
91 if (domain.length() <= 1U) | |
92 return base::string16(kEllipsisUTF16); | |
93 } | |
94 | |
95 // Fit the username in the remaining width (at this point the elided username | |
96 // is guaranteed to fit with at least one character remaining given all the | |
97 // precautions taken earlier). | |
98 available_pixel_width -= GetStringWidthF(domain, font_list); | |
99 username = ElideText(username, font_list, available_pixel_width, ELIDE_TAIL); | |
100 return username + kAtSignUTF16 + domain; | |
101 } | |
102 | |
103 } // namespace | |
104 | |
105 // U+2026 in utf8 | 36 // U+2026 in utf8 |
106 const char kEllipsis[] = "\xE2\x80\xA6"; | 37 const char kEllipsis[] = "\xE2\x80\xA6"; |
107 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; | 38 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; |
108 const base::char16 kForwardSlash = '/'; | 39 const base::char16 kForwardSlash = '/'; |
109 | 40 |
110 StringSlicer::StringSlicer(const base::string16& text, | 41 StringSlicer::StringSlicer(const base::string16& text, |
111 const base::string16& ellipsis, | 42 const base::string16& ellipsis, |
112 bool elide_in_middle, | 43 bool elide_in_middle, |
113 bool elide_at_beginning) | 44 bool elide_at_beginning) |
114 : text_(text), | 45 : text_(text), |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
199 ElideText(rootname, font_list, available_root_width, ELIDE_TAIL); | 130 ElideText(rootname, font_list, available_root_width, ELIDE_TAIL); |
200 elided_name += extension; | 131 elided_name += extension; |
201 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); | 132 return base::i18n::GetDisplayStringInLTRDirectionality(elided_name); |
202 } | 133 } |
203 | 134 |
204 base::string16 ElideText(const base::string16& text, | 135 base::string16 ElideText(const base::string16& text, |
205 const FontList& font_list, | 136 const FontList& font_list, |
206 float available_pixel_width, | 137 float available_pixel_width, |
207 ElideBehavior behavior) { | 138 ElideBehavior behavior) { |
208 DCHECK_NE(behavior, FADE_TAIL); | 139 DCHECK_NE(behavior, FADE_TAIL); |
209 if (text.empty() || behavior == FADE_TAIL) | 140 scoped_ptr<RenderText> render_text(RenderText::CreateInstance()); |
210 return text; | 141 render_text->SetCursorEnabled(false); |
211 if (behavior == ELIDE_EMAIL) | 142 render_text->SetFontList(font_list); |
212 return ElideEmail(text, font_list, available_pixel_width); | 143 return render_text->Elide(text, available_pixel_width, behavior); |
213 | |
214 const float current_text_pixel_width = GetStringWidthF(text, font_list); | |
215 const bool elide_in_middle = (behavior == ELIDE_MIDDLE); | |
216 const bool elide_at_beginning = (behavior == ELIDE_HEAD); | |
217 const bool insert_ellipsis = (behavior != TRUNCATE); | |
218 const base::string16 ellipsis = base::string16(kEllipsisUTF16); | |
219 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); | |
220 | |
221 // Pango will return 0 width for absurdly long strings. Cut the string in | |
222 // half and try again. | |
223 // This is caused by an int overflow in Pango (specifically, in | |
224 // pango_glyph_string_extents_range). It's actually more subtle than just | |
225 // returning 0, since on super absurdly long strings, the int can wrap and | |
226 // return positive numbers again. Detecting that is probably not worth it | |
227 // (eliding way too much from a ridiculous string is probably still | |
228 // ridiculous), but we should check other widths for bogus values as well. | |
229 if (current_text_pixel_width <= 0) { | |
230 const base::string16 cut = | |
231 slicer.CutString(text.length() / 2, insert_ellipsis); | |
232 return ElideText(cut, font_list, available_pixel_width, behavior); | |
233 } | |
234 | |
235 if (current_text_pixel_width <= available_pixel_width) | |
236 return text; | |
237 | |
238 if (insert_ellipsis && | |
239 GetStringWidthF(ellipsis, font_list) > available_pixel_width) | |
240 return base::string16(); | |
241 | |
242 // Use binary search to compute the elided text. | |
243 size_t lo = 0; | |
244 size_t hi = text.length() - 1; | |
245 size_t guess; | |
246 for (guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { | |
247 // We check the width of the whole desired string at once to ensure we | |
248 // handle kerning/ligatures/etc. correctly. | |
249 // TODO(skanuj) : Handle directionality of ellipsis based on adjacent | |
250 // characters. See crbug.com/327963. | |
251 const base::string16 cut = slicer.CutString(guess, insert_ellipsis); | |
252 const float guess_width = GetStringWidthF(cut, font_list); | |
253 if (guess_width == available_pixel_width) | |
254 break; | |
255 if (guess_width > available_pixel_width) { | |
256 hi = guess - 1; | |
257 // Move back if we are on loop terminating condition, and guess is wider | |
258 // than available. | |
259 if (hi < lo) | |
260 lo = hi; | |
261 } else { | |
262 lo = guess + 1; | |
263 } | |
264 } | |
265 | |
266 return slicer.CutString(guess, insert_ellipsis); | |
267 } | 144 } |
268 | 145 |
269 bool ElideString(const base::string16& input, | 146 bool ElideString(const base::string16& input, |
270 int max_len, | 147 int max_len, |
271 base::string16* output) { | 148 base::string16* output) { |
272 DCHECK_GE(max_len, 0); | 149 DCHECK_GE(max_len, 0); |
273 if (static_cast<int>(input.length()) <= max_len) { | 150 if (static_cast<int>(input.length()) <= max_len) { |
274 output->assign(input); | 151 output->assign(input); |
275 return false; | 152 return false; |
276 } | 153 } |
(...skipping 539 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
816 index = char_iterator.getIndex(); | 693 index = char_iterator.getIndex(); |
817 } else { | 694 } else { |
818 // String has leading whitespace, return the elide string. | 695 // String has leading whitespace, return the elide string. |
819 return kElideString; | 696 return kElideString; |
820 } | 697 } |
821 } | 698 } |
822 return string.substr(0, index) + kElideString; | 699 return string.substr(0, index) + kElideString; |
823 } | 700 } |
824 | 701 |
825 } // namespace gfx | 702 } // namespace gfx |
OLD | NEW |