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 #include "ui/gfx/render_text.h" | 5 #include "ui/gfx/render_text.h" |
6 | 6 |
7 #include <algorithm> | 7 #include <algorithm> |
8 #include <climits> | 8 #include <climits> |
9 | 9 |
10 #include "base/command_line.h" | 10 #include "base/command_line.h" |
11 #include "base/i18n/break_iterator.h" | 11 #include "base/i18n/break_iterator.h" |
12 #include "base/logging.h" | 12 #include "base/logging.h" |
13 #include "base/stl_util.h" | 13 #include "base/stl_util.h" |
14 #include "base/strings/utf_string_conversions.h" | 14 #include "base/strings/utf_string_conversions.h" |
15 #include "third_party/icu/source/common/unicode/rbbi.h" | 15 #include "third_party/icu/source/common/unicode/rbbi.h" |
16 #include "third_party/icu/source/common/unicode/utf16.h" | 16 #include "third_party/icu/source/common/unicode/utf16.h" |
17 #include "third_party/skia/include/core/SkTypeface.h" | 17 #include "third_party/skia/include/core/SkTypeface.h" |
18 #include "third_party/skia/include/effects/SkGradientShader.h" | 18 #include "third_party/skia/include/effects/SkGradientShader.h" |
19 #include "ui/gfx/canvas.h" | 19 #include "ui/gfx/canvas.h" |
20 #include "ui/gfx/insets.h" | 20 #include "ui/gfx/insets.h" |
21 #include "ui/gfx/render_text_harfbuzz.h" | 21 #include "ui/gfx/render_text_harfbuzz.h" |
22 #include "ui/gfx/scoped_canvas.h" | 22 #include "ui/gfx/scoped_canvas.h" |
23 #include "ui/gfx/skia_util.h" | 23 #include "ui/gfx/skia_util.h" |
24 #include "ui/gfx/switches.h" | 24 #include "ui/gfx/switches.h" |
25 #include "ui/gfx/text_constants.h" | |
26 #include "ui/gfx/text_elider.h" | 25 #include "ui/gfx/text_elider.h" |
27 #include "ui/gfx/text_utils.h" | 26 #include "ui/gfx/text_utils.h" |
28 #include "ui/gfx/utf16_indexing.h" | 27 #include "ui/gfx/utf16_indexing.h" |
29 | 28 |
30 namespace gfx { | 29 namespace gfx { |
31 | 30 |
32 namespace { | 31 namespace { |
33 | 32 |
34 // All chars are replaced by this char when the password style is set. | 33 // All chars are replaced by this char when the password style is set. |
35 // TODO(benrg): GTK uses the first of U+25CF, U+2022, U+2731, U+273A, '*' | 34 // TODO(benrg): GTK uses the first of U+25CF, U+2022, U+2731, U+273A, '*' |
(...skipping 470 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
506 UpdateLayoutText(); | 505 UpdateLayoutText(); |
507 } | 506 } |
508 } | 507 } |
509 | 508 |
510 void RenderText::SetDisplayRect(const Rect& r) { | 509 void RenderText::SetDisplayRect(const Rect& r) { |
511 if (r != display_rect_) { | 510 if (r != display_rect_) { |
512 display_rect_ = r; | 511 display_rect_ = r; |
513 baseline_ = kInvalidBaseline; | 512 baseline_ = kInvalidBaseline; |
514 cached_bounds_and_offset_valid_ = false; | 513 cached_bounds_and_offset_valid_ = false; |
515 lines_.clear(); | 514 lines_.clear(); |
516 if (elide_behavior_ != TRUNCATE) | 515 if (elide_behavior_ != NO_ELIDE) |
517 UpdateLayoutText(); | 516 UpdateLayoutText(); |
518 } | 517 } |
519 } | 518 } |
520 | 519 |
521 void RenderText::SetCursorPosition(size_t position) { | 520 void RenderText::SetCursorPosition(size_t position) { |
522 MoveCursorTo(position, false); | 521 MoveCursorTo(position, false); |
523 } | 522 } |
524 | 523 |
525 void RenderText::MoveCursor(BreakType break_type, | 524 void RenderText::MoveCursor(BreakType break_type, |
526 VisualCursorDirection direction, | 525 VisualCursorDirection direction, |
(...skipping 356 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
883 selection_color_(kDefaultColor), | 882 selection_color_(kDefaultColor), |
884 selection_background_focused_color_(kDefaultSelectionBackgroundColor), | 883 selection_background_focused_color_(kDefaultSelectionBackgroundColor), |
885 focused_(false), | 884 focused_(false), |
886 composition_range_(Range::InvalidRange()), | 885 composition_range_(Range::InvalidRange()), |
887 colors_(kDefaultColor), | 886 colors_(kDefaultColor), |
888 styles_(NUM_TEXT_STYLES), | 887 styles_(NUM_TEXT_STYLES), |
889 composition_and_selection_styles_applied_(false), | 888 composition_and_selection_styles_applied_(false), |
890 obscured_(false), | 889 obscured_(false), |
891 obscured_reveal_index_(-1), | 890 obscured_reveal_index_(-1), |
892 truncate_length_(0), | 891 truncate_length_(0), |
893 elide_behavior_(TRUNCATE), | 892 elide_behavior_(NO_ELIDE), |
894 multiline_(false), | 893 multiline_(false), |
895 background_is_transparent_(false), | 894 background_is_transparent_(false), |
896 clip_to_display_rect_(true), | 895 clip_to_display_rect_(true), |
897 baseline_(kInvalidBaseline), | 896 baseline_(kInvalidBaseline), |
898 cached_bounds_and_offset_valid_(false) { | 897 cached_bounds_and_offset_valid_(false) { |
899 } | 898 } |
900 | 899 |
901 const Vector2d& RenderText::GetUpdatedDisplayOffset() { | 900 const Vector2d& RenderText::GetUpdatedDisplayOffset() { |
902 UpdateCachedBoundsAndOffset(); | 901 UpdateCachedBoundsAndOffset(); |
903 return display_offset_; | 902 return display_offset_; |
(...skipping 248 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1152 layout_text_.replace(cp_start, 1, text_.substr(start, end - start)); | 1151 layout_text_.replace(cp_start, 1, text_.substr(start, end - start)); |
1153 } | 1152 } |
1154 } else { | 1153 } else { |
1155 layout_text_ = text_; | 1154 layout_text_ = text_; |
1156 } | 1155 } |
1157 | 1156 |
1158 const base::string16& text = layout_text_; | 1157 const base::string16& text = layout_text_; |
1159 if (truncate_length_ > 0 && truncate_length_ < text.length()) { | 1158 if (truncate_length_ > 0 && truncate_length_ < text.length()) { |
1160 // Truncate the text at a valid character break and append an ellipsis. | 1159 // Truncate the text at a valid character break and append an ellipsis. |
1161 icu::StringCharacterIterator iter(text.c_str()); | 1160 icu::StringCharacterIterator iter(text.c_str()); |
1162 iter.setIndex32(truncate_length_ - 1); | 1161 // Respect ELIDE_HEAD and ELIDE_MIDDLE preferences during truncation. |
Alexei Svitkine (slow)
2014/07/03 20:12:33
Can you add test coverage for this?
msw
2014/07/08 19:07:36
I added this code because otherwise TextEliderTest
| |
1163 layout_text_.assign(text.substr(0, iter.getIndex()) + kEllipsisUTF16); | 1162 if (elide_behavior_ == ELIDE_HEAD) { |
1163 iter.setIndex32(text.length() - truncate_length_ + 1); | |
1164 layout_text_.assign(kEllipsisUTF16 + text.substr(iter.getIndex())); | |
1165 } else if (elide_behavior_ == ELIDE_MIDDLE) { | |
1166 iter.setIndex32(truncate_length_ / 2); | |
1167 const size_t ellipsis_start = iter.getIndex(); | |
1168 iter.setIndex32(text.length() - (truncate_length_ / 2)); | |
1169 const size_t ellipsis_end = iter.getIndex(); | |
1170 layout_text_.assign(text.substr(0, ellipsis_start) + kEllipsisUTF16 + | |
1171 text.substr(ellipsis_end)); | |
1172 } else { | |
1173 iter.setIndex32(truncate_length_ - 1); | |
1174 layout_text_.assign(text.substr(0, iter.getIndex()) + kEllipsisUTF16); | |
1175 } | |
1164 } | 1176 } |
1165 | 1177 |
1166 if (elide_behavior_ != TRUNCATE && elide_behavior_ != FADE_TAIL && | 1178 if (elide_behavior_ != NO_ELIDE && elide_behavior_ != FADE_TAIL && |
1167 display_rect_.width() > 0 && !layout_text_.empty() && | 1179 !layout_text_.empty() && GetContentWidth() > display_rect_.width()) { |
1168 GetContentWidth() > display_rect_.width()) { | |
1169 // This doesn't trim styles so ellipsis may get rendered as a different | 1180 // This doesn't trim styles so ellipsis may get rendered as a different |
1170 // style than the preceding text. See crbug.com/327850. | 1181 // style than the preceding text. See crbug.com/327850. |
1171 layout_text_.assign(ElideText(layout_text_)); | 1182 layout_text_.assign( |
1183 Elide(layout_text_, display_rect_.width(), elide_behavior_)); | |
1172 } | 1184 } |
1173 | 1185 |
1174 ResetLayout(); | 1186 ResetLayout(); |
1175 } | 1187 } |
1176 | 1188 |
1177 // TODO(skanuj): Fix code duplication with ElideText in ui/gfx/text_elider.cc | 1189 base::string16 RenderText::Elide(const base::string16& text, |
1178 // See crbug.com/327846 | 1190 float available_width, |
1179 base::string16 RenderText::ElideText(const base::string16& text) { | 1191 ElideBehavior behavior) { |
1180 const bool insert_ellipsis = (elide_behavior_ != TRUNCATE); | 1192 if (available_width <= 0 || text.empty()) |
1193 return base::string16(); | |
1194 if (behavior == ELIDE_EMAIL) | |
1195 return ElideEmail(text, available_width); | |
1196 | |
1181 // Create a RenderText copy with attributes that affect the rendering width. | 1197 // Create a RenderText copy with attributes that affect the rendering width. |
1182 scoped_ptr<RenderText> render_text(CreateInstance()); | 1198 scoped_ptr<RenderText> render_text(CreateInstance()); |
1183 render_text->SetFontList(font_list_); | 1199 render_text->SetFontList(font_list_); |
1184 render_text->SetDirectionalityMode(directionality_mode_); | 1200 render_text->SetDirectionalityMode(directionality_mode_); |
1185 render_text->SetCursorEnabled(cursor_enabled_); | 1201 render_text->SetCursorEnabled(cursor_enabled_); |
1186 | 1202 render_text->set_truncate_length(truncate_length_); |
1187 render_text->styles_ = styles_; | 1203 render_text->styles_ = styles_; |
1188 render_text->colors_ = colors_; | 1204 render_text->colors_ = colors_; |
1189 render_text->SetText(text); | 1205 render_text->SetText(text); |
1190 const int current_text_pixel_width = render_text->GetContentWidth(); | 1206 if (render_text->GetContentWidth() <= available_width) |
1207 return text; | |
1191 | 1208 |
1192 const base::string16 ellipsis = base::string16(kEllipsisUTF16); | 1209 const base::string16 ellipsis = base::string16(kEllipsisUTF16); |
1193 const bool elide_in_middle = false; | 1210 const bool insert_ellipsis = (behavior != TRUNCATE); |
1194 const bool elide_at_beginning = false; | 1211 const bool elide_in_middle = (behavior == ELIDE_MIDDLE); |
1212 const bool elide_at_beginning = (behavior == ELIDE_HEAD); | |
1195 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); | 1213 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); |
1196 | 1214 |
1197 // Pango will return 0 width for absurdly long strings. Cut the string in | |
1198 // half and try again. | |
1199 // This is caused by an int overflow in Pango (specifically, in | |
1200 // pango_glyph_string_extents_range). It's actually more subtle than just | |
1201 // returning 0, since on super absurdly long strings, the int can wrap and | |
1202 // return positive numbers again. Detecting that is probably not worth it | |
1203 // (eliding way too much from a ridiculous string is probably still | |
1204 // ridiculous), but we should check other widths for bogus values as well. | |
1205 if (current_text_pixel_width <= 0 && !text.empty()) | |
1206 return ElideText(slicer.CutString(text.length() / 2, insert_ellipsis)); | |
1207 | |
1208 if (current_text_pixel_width <= display_rect_.width()) | |
1209 return text; | |
1210 | |
1211 render_text->SetText(base::string16()); | |
1212 render_text->SetText(ellipsis); | 1215 render_text->SetText(ellipsis); |
1213 const int ellipsis_width = render_text->GetContentWidth(); | 1216 const int ellipsis_width = render_text->GetContentWidth(); |
1214 | 1217 |
1215 if (insert_ellipsis && (ellipsis_width >= display_rect_.width())) | 1218 if (insert_ellipsis && (ellipsis_width > available_width)) |
1216 return base::string16(); | 1219 return base::string16(); |
1217 | 1220 |
1218 // Use binary search to compute the elided text. | 1221 // Use binary search to compute the elided text. |
1219 size_t lo = 0; | 1222 size_t lo = 0; |
1220 size_t hi = text.length() - 1; | 1223 size_t hi = text.length() - 1; |
1221 const base::i18n::TextDirection text_direction = GetTextDirection(); | 1224 const base::i18n::TextDirection text_direction = GetTextDirection(); |
1222 for (size_t guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { | 1225 for (size_t guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { |
1223 // Restore styles and colors. They will be truncated to size by SetText. | 1226 // Restore styles and colors. They will be truncated to size by SetText. |
1224 render_text->styles_ = styles_; | 1227 render_text->styles_ = styles_; |
1225 render_text->colors_ = colors_; | 1228 render_text->colors_ = colors_; |
1226 base::string16 new_text = slicer.CutString(guess, false); | 1229 base::string16 new_text = |
1230 slicer.CutString(guess, insert_ellipsis && behavior != ELIDE_TAIL); | |
1227 render_text->SetText(new_text); | 1231 render_text->SetText(new_text); |
1228 | 1232 |
1229 // This has to be an additional step so that the ellipsis is rendered with | 1233 // This has to be an additional step so that the ellipsis is rendered with |
1230 // same style as trailing part of the text. | 1234 // same style as trailing part of the text. |
1231 if (insert_ellipsis) { | 1235 if (insert_ellipsis && behavior == ELIDE_TAIL) { |
1232 // When ellipsis follows text whose directionality is not the same as that | 1236 // When ellipsis follows text whose directionality is not the same as that |
1233 // of the whole text, it will be rendered with the directionality of the | 1237 // of the whole text, it will be rendered with the directionality of the |
1234 // whole text. Since we want ellipsis to indicate continuation of the | 1238 // whole text. Since we want ellipsis to indicate continuation of the |
1235 // preceding text, we force the directionality of ellipsis to be same as | 1239 // preceding text, we force the directionality of ellipsis to be same as |
1236 // the preceding text using LTR or RTL markers. | 1240 // the preceding text using LTR or RTL markers. |
1237 base::i18n::TextDirection trailing_text_direction = | 1241 base::i18n::TextDirection trailing_text_direction = |
1238 base::i18n::GetLastStrongCharacterDirection(new_text); | 1242 base::i18n::GetLastStrongCharacterDirection(new_text); |
1239 new_text.append(ellipsis); | 1243 new_text.append(ellipsis); |
1240 if (trailing_text_direction != text_direction) { | 1244 if (trailing_text_direction != text_direction) { |
1241 if (trailing_text_direction == base::i18n::LEFT_TO_RIGHT) | 1245 if (trailing_text_direction == base::i18n::LEFT_TO_RIGHT) |
1242 new_text += base::i18n::kLeftToRightMark; | 1246 new_text += base::i18n::kLeftToRightMark; |
1243 else | 1247 else |
1244 new_text += base::i18n::kRightToLeftMark; | 1248 new_text += base::i18n::kRightToLeftMark; |
1245 } | 1249 } |
1246 render_text->SetText(new_text); | 1250 render_text->SetText(new_text); |
1247 } | 1251 } |
1248 | 1252 |
1249 // We check the width of the whole desired string at once to ensure we | 1253 // We check the width of the whole desired string at once to ensure we |
1250 // handle kerning/ligatures/etc. correctly. | 1254 // handle kerning/ligatures/etc. correctly. |
1251 const int guess_width = render_text->GetContentWidth(); | 1255 const int guess_width = render_text->GetContentWidth(); |
1252 if (guess_width == display_rect_.width()) | 1256 if (guess_width == available_width) |
1253 break; | 1257 break; |
1254 if (guess_width > display_rect_.width()) { | 1258 if (guess_width > available_width) { |
1255 hi = guess - 1; | 1259 hi = guess - 1; |
1256 // Move back if we are on loop terminating condition, and guess is wider | 1260 // Move back on the loop terminating condition when the guess is too wide. |
1257 // than available. | |
1258 if (hi < lo) | 1261 if (hi < lo) |
1259 lo = hi; | 1262 lo = hi; |
1260 } else { | 1263 } else { |
1261 lo = guess + 1; | 1264 lo = guess + 1; |
1262 } | 1265 } |
1263 } | 1266 } |
1264 | 1267 |
1265 return render_text->text(); | 1268 return render_text->text(); |
1266 } | 1269 } |
1267 | 1270 |
1271 base::string16 RenderText::ElideEmail(const base::string16& email, | |
1272 float available_width) { | |
1273 // The returned string will have at least one character besides the ellipsis | |
1274 // on either side of '@'; if that's impossible, a single ellipsis is returned. | |
1275 // If possible, only the username is elided. Otherwise, the domain is elided | |
1276 // in the middle, splitting available width equally with the elided username. | |
1277 // If the username is short enough that it doesn't need half the available | |
1278 // width, the elided domain will occupy that extra width. | |
1279 | |
1280 // Split the email into its local-part (username) and domain-part. The email | |
1281 // spec allows for @ symbols in the username under some special requirements, | |
1282 // but not in the domain part, so splitting at the last @ symbol is safe. | |
1283 const size_t split_index = email.find_last_of('@'); | |
1284 DCHECK_NE(split_index, base::string16::npos); | |
1285 base::string16 username = email.substr(0, split_index); | |
1286 base::string16 domain = email.substr(split_index + 1); | |
1287 DCHECK(!username.empty()); | |
1288 DCHECK(!domain.empty()); | |
1289 | |
1290 // Subtract the @ symbol from the available width as it is mandatory. | |
1291 const base::string16 kAtSignUTF16 = base::ASCIIToUTF16("@"); | |
1292 available_width -= GetStringWidthF(kAtSignUTF16, font_list()); | |
1293 | |
1294 // Check whether eliding the domain is necessary: if eliding the username | |
1295 // is sufficient, the domain will not be elided. | |
1296 const float full_username_width = GetStringWidthF(username, font_list()); | |
1297 const float available_domain_width = available_width - | |
1298 std::min(full_username_width, | |
1299 GetStringWidthF(username.substr(0, 1) + kEllipsisUTF16, font_list())); | |
1300 if (GetStringWidthF(domain, font_list()) > available_domain_width) { | |
1301 // Elide the domain so that it only takes half of the available width. | |
1302 // Should the username not need all the width available in its half, the | |
1303 // domain will occupy the leftover width. | |
1304 // If |desired_domain_width| is greater than |available_domain_width|: the | |
1305 // minimal username elision allowed by the specifications will not fit; thus | |
1306 // |desired_domain_width| must be <= |available_domain_width| at all cost. | |
1307 const float desired_domain_width = | |
1308 std::min<float>(available_domain_width, | |
1309 std::max<float>(available_width - full_username_width, | |
1310 available_width / 2)); | |
1311 domain = Elide(domain, desired_domain_width, ELIDE_MIDDLE); | |
1312 // Failing to elide the domain such that at least one character remains | |
1313 // (other than the ellipsis itself) remains: return a single ellipsis. | |
1314 if (domain.length() <= 1U) | |
1315 return base::string16(kEllipsisUTF16); | |
1316 } | |
1317 | |
1318 // Fit the username in the remaining width (at this point the elided username | |
1319 // is guaranteed to fit with at least one character remaining given all the | |
1320 // precautions taken earlier). | |
1321 available_width -= GetStringWidthF(domain, font_list()); | |
1322 username = Elide(username, available_width, ELIDE_TAIL); | |
1323 return username + kAtSignUTF16 + domain; | |
1324 } | |
1325 | |
1268 void RenderText::UpdateCachedBoundsAndOffset() { | 1326 void RenderText::UpdateCachedBoundsAndOffset() { |
1269 if (cached_bounds_and_offset_valid_) | 1327 if (cached_bounds_and_offset_valid_) |
1270 return; | 1328 return; |
1271 | 1329 |
1272 // TODO(ckocagil): Add support for scrolling multiline text. | 1330 // TODO(ckocagil): Add support for scrolling multiline text. |
1273 | 1331 |
1274 // First, set the valid flag true to calculate the current cursor bounds using | 1332 // First, set the valid flag true to calculate the current cursor bounds using |
1275 // the stale |display_offset_|. Applying |delta_offset| at the end of this | 1333 // the stale |display_offset_|. Applying |delta_offset| at the end of this |
1276 // function will set |cursor_bounds_| and |display_offset_| to correct values. | 1334 // function will set |cursor_bounds_| and |display_offset_| to correct values. |
1277 cached_bounds_and_offset_valid_ = true; | 1335 cached_bounds_and_offset_valid_ = true; |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1315 cursor_bounds_ += delta_offset; | 1373 cursor_bounds_ += delta_offset; |
1316 } | 1374 } |
1317 | 1375 |
1318 void RenderText::DrawSelection(Canvas* canvas) { | 1376 void RenderText::DrawSelection(Canvas* canvas) { |
1319 const std::vector<Rect> sel = GetSubstringBounds(selection()); | 1377 const std::vector<Rect> sel = GetSubstringBounds(selection()); |
1320 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) | 1378 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) |
1321 canvas->FillRect(*i, selection_background_focused_color_); | 1379 canvas->FillRect(*i, selection_background_focused_color_); |
1322 } | 1380 } |
1323 | 1381 |
1324 } // namespace gfx | 1382 } // namespace gfx |
OLD | NEW |