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/i18n/break_iterator.h" | 10 #include "base/i18n/break_iterator.h" |
11 #include "base/logging.h" | 11 #include "base/logging.h" |
12 #include "base/stl_util.h" | 12 #include "base/stl_util.h" |
13 #include "base/strings/utf_string_conversions.h" | |
13 #include "third_party/icu/source/common/unicode/rbbi.h" | 14 #include "third_party/icu/source/common/unicode/rbbi.h" |
14 #include "third_party/icu/source/common/unicode/utf16.h" | 15 #include "third_party/icu/source/common/unicode/utf16.h" |
15 #include "third_party/skia/include/core/SkTypeface.h" | 16 #include "third_party/skia/include/core/SkTypeface.h" |
16 #include "third_party/skia/include/effects/SkGradientShader.h" | 17 #include "third_party/skia/include/effects/SkGradientShader.h" |
17 #include "ui/gfx/canvas.h" | 18 #include "ui/gfx/canvas.h" |
18 #include "ui/gfx/insets.h" | 19 #include "ui/gfx/insets.h" |
19 #include "ui/gfx/skia_util.h" | 20 #include "ui/gfx/skia_util.h" |
20 #include "ui/gfx/text_constants.h" | 21 #include "ui/gfx/text_constants.h" |
21 #include "ui/gfx/text_elider.h" | 22 #include "ui/gfx/text_elider.h" |
23 #include "ui/gfx/text_utils.h" | |
22 #include "ui/gfx/utf16_indexing.h" | 24 #include "ui/gfx/utf16_indexing.h" |
23 | 25 |
24 namespace gfx { | 26 namespace gfx { |
25 | 27 |
26 namespace { | 28 namespace { |
27 | 29 |
28 // All chars are replaced by this char when the password style is set. | 30 // All chars are replaced by this char when the password style is set. |
29 // TODO(benrg): GTK uses the first of U+25CF, U+2022, U+2731, U+273A, '*' | 31 // TODO(benrg): GTK uses the first of U+25CF, U+2022, U+2731, U+273A, '*' |
30 // that's available in the font (find_invisible_char() in gtkentry.c). | 32 // that's available in the font (find_invisible_char() in gtkentry.c). |
31 const base::char16 kPasswordReplacementChar = '*'; | 33 const base::char16 kPasswordReplacementChar = '*'; |
(...skipping 331 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
363 // Reset selection model. SetText should always followed by SetSelectionModel | 365 // Reset selection model. SetText should always followed by SetSelectionModel |
364 // or SetCursorPosition in upper layer. | 366 // or SetCursorPosition in upper layer. |
365 SetSelectionModel(SelectionModel()); | 367 SetSelectionModel(SelectionModel()); |
366 | 368 |
367 // Invalidate the cached text direction if it depends on the text contents. | 369 // Invalidate the cached text direction if it depends on the text contents. |
368 if (directionality_mode_ == DIRECTIONALITY_FROM_TEXT) | 370 if (directionality_mode_ == DIRECTIONALITY_FROM_TEXT) |
369 text_direction_ = base::i18n::UNKNOWN_DIRECTION; | 371 text_direction_ = base::i18n::UNKNOWN_DIRECTION; |
370 | 372 |
371 obscured_reveal_index_ = -1; | 373 obscured_reveal_index_ = -1; |
372 UpdateLayoutText(); | 374 UpdateLayoutText(); |
373 ResetLayout(); | |
374 } | 375 } |
375 | 376 |
376 void RenderText::SetHorizontalAlignment(HorizontalAlignment alignment) { | 377 void RenderText::SetHorizontalAlignment(HorizontalAlignment alignment) { |
377 if (horizontal_alignment_ != alignment) { | 378 if (horizontal_alignment_ != alignment) { |
378 horizontal_alignment_ = alignment; | 379 horizontal_alignment_ = alignment; |
379 display_offset_ = Vector2d(); | 380 display_offset_ = Vector2d(); |
380 cached_bounds_and_offset_valid_ = false; | 381 cached_bounds_and_offset_valid_ = false; |
381 } | 382 } |
382 } | 383 } |
383 | 384 |
(...skipping 25 matching lines...) Expand all Loading... | |
409 insert_mode_ = !insert_mode_; | 410 insert_mode_ = !insert_mode_; |
410 cached_bounds_and_offset_valid_ = false; | 411 cached_bounds_and_offset_valid_ = false; |
411 } | 412 } |
412 | 413 |
413 void RenderText::SetObscured(bool obscured) { | 414 void RenderText::SetObscured(bool obscured) { |
414 if (obscured != obscured_) { | 415 if (obscured != obscured_) { |
415 obscured_ = obscured; | 416 obscured_ = obscured; |
416 obscured_reveal_index_ = -1; | 417 obscured_reveal_index_ = -1; |
417 cached_bounds_and_offset_valid_ = false; | 418 cached_bounds_and_offset_valid_ = false; |
418 UpdateLayoutText(); | 419 UpdateLayoutText(); |
419 ResetLayout(); | |
420 } | 420 } |
421 } | 421 } |
422 | 422 |
423 void RenderText::SetObscuredRevealIndex(int index) { | 423 void RenderText::SetObscuredRevealIndex(int index) { |
424 if (obscured_reveal_index_ == index) | 424 if (obscured_reveal_index_ == index) |
425 return; | 425 return; |
426 | 426 |
427 obscured_reveal_index_ = index; | 427 obscured_reveal_index_ = index; |
428 cached_bounds_and_offset_valid_ = false; | 428 cached_bounds_and_offset_valid_ = false; |
429 UpdateLayoutText(); | 429 UpdateLayoutText(); |
430 ResetLayout(); | |
431 } | 430 } |
432 | 431 |
433 void RenderText::SetMultiline(bool multiline) { | 432 void RenderText::SetMultiline(bool multiline) { |
434 if (multiline != multiline_) { | 433 if (multiline != multiline_) { |
435 multiline_ = multiline; | 434 multiline_ = multiline; |
436 cached_bounds_and_offset_valid_ = false; | 435 cached_bounds_and_offset_valid_ = false; |
437 lines_.clear(); | 436 lines_.clear(); |
438 } | 437 } |
439 } | 438 } |
440 | 439 |
440 void RenderText::SetElideBehavior(ElideBehavior elide_behavior) { | |
441 // TODO(skanuj) : Add a test for triggering layout change. | |
442 if (elide_behavior_ != elide_behavior) { | |
443 elide_behavior_ = elide_behavior; | |
444 UpdateLayoutText(); | |
445 } | |
446 } | |
447 | |
441 void RenderText::SetDisplayRect(const Rect& r) { | 448 void RenderText::SetDisplayRect(const Rect& r) { |
442 display_rect_ = r; | 449 if (r != display_rect_) { |
443 baseline_ = kInvalidBaseline; | 450 display_rect_ = r; |
444 cached_bounds_and_offset_valid_ = false; | 451 baseline_ = kInvalidBaseline; |
445 lines_.clear(); | 452 cached_bounds_and_offset_valid_ = false; |
453 lines_.clear(); | |
454 if (elide_behavior_ != gfx::NO_ELIDE) | |
455 UpdateLayoutText(); | |
456 } | |
446 } | 457 } |
447 | 458 |
448 void RenderText::SetCursorPosition(size_t position) { | 459 void RenderText::SetCursorPosition(size_t position) { |
449 MoveCursorTo(position, false); | 460 MoveCursorTo(position, false); |
450 } | 461 } |
451 | 462 |
452 void RenderText::MoveCursor(BreakType break_type, | 463 void RenderText::MoveCursor(BreakType break_type, |
453 VisualCursorDirection direction, | 464 VisualCursorDirection direction, |
454 bool select) { | 465 bool select) { |
455 SelectionModel position(cursor_position(), selection_model_.caret_affinity()); | 466 SelectionModel position(cursor_position(), selection_model_.caret_affinity()); |
(...skipping 372 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
828 selection_color_(kDefaultColor), | 839 selection_color_(kDefaultColor), |
829 selection_background_focused_color_(kDefaultSelectionBackgroundColor), | 840 selection_background_focused_color_(kDefaultSelectionBackgroundColor), |
830 focused_(false), | 841 focused_(false), |
831 composition_range_(Range::InvalidRange()), | 842 composition_range_(Range::InvalidRange()), |
832 colors_(kDefaultColor), | 843 colors_(kDefaultColor), |
833 styles_(NUM_TEXT_STYLES), | 844 styles_(NUM_TEXT_STYLES), |
834 composition_and_selection_styles_applied_(false), | 845 composition_and_selection_styles_applied_(false), |
835 obscured_(false), | 846 obscured_(false), |
836 obscured_reveal_index_(-1), | 847 obscured_reveal_index_(-1), |
837 truncate_length_(0), | 848 truncate_length_(0), |
849 elide_behavior_(NO_ELIDE), | |
838 multiline_(false), | 850 multiline_(false), |
839 fade_head_(false), | 851 fade_head_(false), |
840 fade_tail_(false), | 852 fade_tail_(false), |
841 background_is_transparent_(false), | 853 background_is_transparent_(false), |
842 clip_to_display_rect_(true), | 854 clip_to_display_rect_(true), |
843 baseline_(kInvalidBaseline), | 855 baseline_(kInvalidBaseline), |
844 cached_bounds_and_offset_valid_(false) { | 856 cached_bounds_and_offset_valid_(false) { |
845 } | 857 } |
846 | 858 |
847 const Vector2d& RenderText::GetUpdatedDisplayOffset() { | 859 const Vector2d& RenderText::GetUpdatedDisplayOffset() { |
(...skipping 259 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1107 U16_NEXT(text_.data(), end, text_.length(), unused_char); | 1119 U16_NEXT(text_.data(), end, text_.length(), unused_char); |
1108 | 1120 |
1109 // Gets the index in |layout_text_| to be replaced. | 1121 // Gets the index in |layout_text_| to be replaced. |
1110 const size_t cp_start = | 1122 const size_t cp_start = |
1111 static_cast<size_t>(gfx::UTF16IndexToOffset(text_, 0, start)); | 1123 static_cast<size_t>(gfx::UTF16IndexToOffset(text_, 0, start)); |
1112 if (layout_text_.length() > cp_start) | 1124 if (layout_text_.length() > cp_start) |
1113 layout_text_.replace(cp_start, 1, text_.substr(start, end - start)); | 1125 layout_text_.replace(cp_start, 1, text_.substr(start, end - start)); |
1114 } | 1126 } |
1115 } | 1127 } |
1116 | 1128 |
1117 const base::string16& text = obscured_ ? layout_text_ : text_; | 1129 const base::string16& text = GetLayoutText(); |
1118 if (truncate_length_ > 0 && truncate_length_ < text.length()) { | 1130 if (truncate_length_ > 0 && truncate_length_ < text.length()) { |
1119 // Truncate the text at a valid character break and append an ellipsis. | 1131 // Truncate the text at a valid character break and append an ellipsis. |
1120 icu::StringCharacterIterator iter(text.c_str()); | 1132 icu::StringCharacterIterator iter(text.c_str()); |
1121 iter.setIndex32(truncate_length_ - 1); | 1133 iter.setIndex32(truncate_length_ - 1); |
1122 layout_text_.assign(text.substr(0, iter.getIndex()) + gfx::kEllipsisUTF16); | 1134 layout_text_.assign(text.substr(0, iter.getIndex()) + gfx::kEllipsisUTF16); |
1123 } | 1135 } |
1136 | |
1137 if (elide_behavior_ != NO_ELIDE && display_rect_.width() > 0 && | |
1138 GetContentWidth() > display_rect_.width()) { | |
1139 base::string16 elided_text = ElideText(GetLayoutText()); | |
1140 | |
1141 // This doesn't trim styles so ellipsis may get rendered as a different | |
1142 // style than the preceding text. See crbug.com/327850. | |
1143 layout_text_.assign(elided_text); | |
1144 } | |
1145 ResetLayout(); | |
1146 } | |
1147 | |
1148 // TODO(skanuj): Fix code duplication with ElideText in ui/gfx/text_elider.cc | |
1149 // See crbug.com/327846 | |
1150 base::string16 RenderText::ElideText(const base::string16& text) { | |
1151 if (text.empty()) | |
msw
2013/12/27 22:54:33
nit: consider an early return (text or empty) for
Anuj
2013/12/30 22:46:58
That check exists on line 1137. I think this is fi
msw
2013/12/30 23:17:59
Please don't delete my original comments when repl
Anuj
2013/12/31 00:04:03
I think that comments are insufficient context, an
msw
2013/12/31 00:24:52
ok, lol
Peter Kasting
2013/12/31 00:27:18
FWIW, it also kinda drives me nuts when people rem
| |
1152 return text; | |
1153 | |
1154 const bool insert_ellipsis = (elide_behavior_ != TRUNCATE_AT_END); | |
1155 // Create a RenderText copy with attributes that affect the rendering width. | |
1156 scoped_ptr<RenderText> render_text(CreateInstance()); | |
1157 render_text->SetFontList(font_list_); | |
1158 render_text->SetDirectionalityMode(directionality_mode_); | |
1159 render_text->SetCursorEnabled(cursor_enabled_); | |
1160 | |
1161 render_text->styles_ = styles_; | |
1162 render_text->colors_ = colors_; | |
1163 render_text->SetText(text); | |
1164 const int current_text_pixel_width = render_text->GetContentWidth(); | |
1165 | |
1166 const base::string16 ellipsis = base::string16(gfx::kEllipsisUTF16); | |
1167 const bool elide_in_middle = false; | |
1168 StringSlicer slicer(text, ellipsis, elide_in_middle); | |
1169 | |
1170 // Pango will return 0 width for absurdly long strings. Cut the string in | |
1171 // half and try again. | |
1172 // This is caused by an int overflow in Pango (specifically, in | |
1173 // pango_glyph_string_extents_range). It's actually more subtle than just | |
1174 // returning 0, since on super absurdly long strings, the int can wrap and | |
1175 // return positive numbers again. Detecting that is probably not worth it | |
1176 // (eliding way too much from a ridiculous string is probably still | |
1177 // ridiculous), but we should check other widths for bogus values as well. | |
1178 if (current_text_pixel_width <= 0 && !text.empty()) | |
1179 return ElideText(slicer.CutString(text.length() / 2, insert_ellipsis)); | |
1180 | |
1181 if (current_text_pixel_width <= display_rect_.width()) | |
1182 return text; | |
1183 | |
1184 render_text->SetText(base::string16()); | |
1185 render_text->SetText(ellipsis); | |
1186 const int ellipsis_width = render_text->GetContentWidth(); | |
1187 | |
1188 if (insert_ellipsis && (ellipsis_width >= display_rect_.width())) | |
1189 return base::string16(); | |
1190 | |
msw
2013/12/27 22:54:33
nit: remove the second blank line here.
Anuj
2013/12/30 22:46:58
Done.
| |
1191 | |
1192 // Use binary search to compute the elided text. | |
1193 size_t lo = 0; | |
1194 size_t hi = text.length() - 1; | |
1195 for (size_t guess = (lo + hi) / 2; (lo <= hi) && (guess < text.length()); | |
msw
2013/12/27 22:54:33
When would guess reasonably exceed the text length
Anuj
2013/12/30 22:46:58
Given guess will be used as an index of text, I th
| |
1196 guess = (lo + hi) / 2) { | |
1197 // Restore styles and colors. They will be truncated to size by SetText. | |
1198 render_text->styles_ = styles_; | |
1199 render_text->colors_ = colors_; | |
1200 base::string16 new_text = slicer.CutString(guess, false); | |
1201 render_text->SetText(new_text); | |
1202 | |
1203 // This has to be an additional step so that the ellipsis is rendered with | |
1204 // same style as trailing part of the text. | |
1205 if (insert_ellipsis) { | |
1206 // When ellipsis follows text whose directionality is not the same as that | |
1207 // of the whole text, it will be rendered with the directionality of the | |
1208 // whole text. Since we want ellipsis to indicate continuation of the | |
1209 // preceding text, we force the directionality of ellipsis to be same as | |
1210 // the preceding text using LTR or RTL markers. | |
1211 base::i18n::TextDirection leading_text_direction = | |
1212 base::i18n::GetFirstStrongCharacterDirection(new_text); | |
1213 base::i18n::TextDirection trailing_text_direction = | |
1214 base::i18n::GetLastStrongCharacterDirection(new_text); | |
1215 new_text.append(ellipsis); | |
1216 if (trailing_text_direction != leading_text_direction) { | |
1217 if (trailing_text_direction == base::i18n::LEFT_TO_RIGHT) | |
1218 new_text += base::i18n::kLeftToRightMark; | |
1219 else | |
1220 new_text += base::i18n::kRightToLeftMark; | |
1221 } | |
1222 render_text->SetText(new_text); | |
1223 } | |
1224 | |
1225 // We check the width of the whole desired string at once to ensure we | |
1226 // handle kerning/ligatures/etc. correctly. | |
1227 const int guess_width = render_text->GetContentWidth(); | |
1228 if (guess_width == display_rect_.width()) | |
1229 break; | |
1230 if (guess_width > display_rect_.width()) { | |
1231 // This shouldn't happen, but lets be paranoid. | |
msw
2013/12/27 22:54:33
nit: let's, but I'd prefer a real fix (or perhaps
Anuj
2013/12/30 22:46:58
I was able to reproduce the issue you described in
msw
2013/12/30 23:17:59
I'm glad the revised ellipsis width checking fixed
Anuj
2013/12/31 00:04:03
Done.
| |
1232 if (guess == 0) | |
1233 return base::string16(); | |
1234 hi = guess - 1; | |
1235 // Move back if we are on loop terminating condition, and guess is wider | |
1236 // than available. | |
1237 if (hi < lo) | |
1238 lo = hi; | |
1239 } else { | |
1240 lo = guess + 1; | |
1241 } | |
1242 } | |
1243 | |
1244 return render_text->text(); | |
1124 } | 1245 } |
1125 | 1246 |
1126 void RenderText::UpdateCachedBoundsAndOffset() { | 1247 void RenderText::UpdateCachedBoundsAndOffset() { |
1127 if (cached_bounds_and_offset_valid_) | 1248 if (cached_bounds_and_offset_valid_) |
1128 return; | 1249 return; |
1129 | 1250 |
1130 // TODO(ckocagil): Add support for scrolling multiline text. | 1251 // TODO(ckocagil): Add support for scrolling multiline text. |
1131 | 1252 |
1132 // First, set the valid flag true to calculate the current cursor bounds using | 1253 // First, set the valid flag true to calculate the current cursor bounds using |
1133 // the stale |display_offset_|. Applying |delta_offset| at the end of this | 1254 // the stale |display_offset_|. Applying |delta_offset| at the end of this |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1172 cursor_bounds_ += delta_offset; | 1293 cursor_bounds_ += delta_offset; |
1173 } | 1294 } |
1174 | 1295 |
1175 void RenderText::DrawSelection(Canvas* canvas) { | 1296 void RenderText::DrawSelection(Canvas* canvas) { |
1176 const std::vector<Rect> sel = GetSubstringBounds(selection()); | 1297 const std::vector<Rect> sel = GetSubstringBounds(selection()); |
1177 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) | 1298 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) |
1178 canvas->FillRect(*i, selection_background_focused_color_); | 1299 canvas->FillRect(*i, selection_background_focused_color_); |
1179 } | 1300 } |
1180 | 1301 |
1181 } // namespace gfx | 1302 } // namespace gfx |
OLD | NEW |