| 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 |