Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(197)

Side by Side Diff: ui/gfx/text_elider.cc

Issue 312233003: Add fade eliding for Views Labels; related cleanup. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Refine alignment check; minor additional cleanup. Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « ui/gfx/text_elider.h ('k') | ui/gfx/text_elider_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
OLDNEW
« no previous file with comments | « ui/gfx/text_elider.h ('k') | ui/gfx/text_elider_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698