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 830 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
866 } | 865 } |
867 | 866 |
868 SelectionModel RenderText::GetSelectionModelForSelectionStart() { | 867 SelectionModel RenderText::GetSelectionModelForSelectionStart() { |
869 const Range& sel = selection(); | 868 const Range& sel = selection(); |
870 if (sel.is_empty()) | 869 if (sel.is_empty()) |
871 return selection_model_; | 870 return selection_model_; |
872 return SelectionModel(sel.start(), | 871 return SelectionModel(sel.start(), |
873 sel.is_reversed() ? CURSOR_BACKWARD : CURSOR_FORWARD); | 872 sel.is_reversed() ? CURSOR_BACKWARD : CURSOR_FORWARD); |
874 } | 873 } |
875 | 874 |
| 875 base::string16 RenderText::Elide(const base::string16& text, |
| 876 float available_width, |
| 877 ElideBehavior behavior) { |
| 878 if (text.empty() || behavior == FADE_TAIL) |
| 879 return text; |
| 880 if (available_width <= 0) |
| 881 return base::string16(); |
| 882 if (behavior == ELIDE_EMAIL) |
| 883 return ElideEmail(text, available_width); |
| 884 |
| 885 // Create a RenderText copy with attributes that affect the rendering width. |
| 886 scoped_ptr<RenderText> render_text(CreateInstance()); |
| 887 render_text->SetFontList(font_list_); |
| 888 render_text->SetDirectionalityMode(directionality_mode_); |
| 889 render_text->SetCursorEnabled(cursor_enabled_); |
| 890 render_text->styles_ = styles_; |
| 891 render_text->colors_ = colors_; |
| 892 render_text->SetText(text); |
| 893 const int current_text_pixel_width = render_text->GetContentWidth(); |
| 894 |
| 895 const base::string16 ellipsis = base::string16(kEllipsisUTF16); |
| 896 const bool insert_ellipsis = (behavior != TRUNCATE); |
| 897 const bool elide_in_middle = (behavior == ELIDE_MIDDLE); |
| 898 const bool elide_at_beginning = (behavior == ELIDE_HEAD); |
| 899 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); |
| 900 |
| 901 // Pango will return 0 width for absurdly long strings. Cut the string in |
| 902 // half and try again. |
| 903 // This is caused by an int overflow in Pango (specifically, in |
| 904 // pango_glyph_string_extents_range). It's actually more subtle than just |
| 905 // returning 0, since on super absurdly long strings, the int can wrap and |
| 906 // return positive numbers again. Detecting that is probably not worth it |
| 907 // (eliding way too much from a ridiculous string is probably still |
| 908 // ridiculous), but we should check other widths for bogus values as well. |
| 909 if (current_text_pixel_width <= 0 && !text.empty()) |
| 910 return Elide(slicer.CutString(text.length() / 2, insert_ellipsis), |
| 911 available_width, behavior); |
| 912 |
| 913 if (current_text_pixel_width <= available_width) |
| 914 return text; |
| 915 |
| 916 render_text->SetText(ellipsis); |
| 917 const int ellipsis_width = render_text->GetContentWidth(); |
| 918 |
| 919 if (insert_ellipsis && (ellipsis_width > available_width)) |
| 920 return base::string16(); |
| 921 |
| 922 // Use binary search to compute the elided text. |
| 923 size_t lo = 0; |
| 924 size_t hi = text.length() - 1; |
| 925 const base::i18n::TextDirection text_direction = GetTextDirection(); |
| 926 for (size_t guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { |
| 927 // Restore styles and colors. They will be truncated to size by SetText. |
| 928 render_text->styles_ = styles_; |
| 929 render_text->colors_ = colors_; |
| 930 base::string16 new_text = |
| 931 slicer.CutString(guess, insert_ellipsis && behavior != ELIDE_TAIL); |
| 932 render_text->SetText(new_text); |
| 933 |
| 934 // This has to be an additional step so that the ellipsis is rendered with |
| 935 // same style as trailing part of the text. |
| 936 if (insert_ellipsis && behavior == ELIDE_TAIL) { |
| 937 // When ellipsis follows text whose directionality is not the same as that |
| 938 // of the whole text, it will be rendered with the directionality of the |
| 939 // whole text. Since we want ellipsis to indicate continuation of the |
| 940 // preceding text, we force the directionality of ellipsis to be same as |
| 941 // the preceding text using LTR or RTL markers. |
| 942 base::i18n::TextDirection trailing_text_direction = |
| 943 base::i18n::GetLastStrongCharacterDirection(new_text); |
| 944 new_text.append(ellipsis); |
| 945 if (trailing_text_direction != text_direction) { |
| 946 if (trailing_text_direction == base::i18n::LEFT_TO_RIGHT) |
| 947 new_text += base::i18n::kLeftToRightMark; |
| 948 else |
| 949 new_text += base::i18n::kRightToLeftMark; |
| 950 } |
| 951 render_text->SetText(new_text); |
| 952 } |
| 953 |
| 954 // We check the width of the whole desired string at once to ensure we |
| 955 // handle kerning/ligatures/etc. correctly. |
| 956 const int guess_width = render_text->GetContentWidth(); |
| 957 if (guess_width == available_width) |
| 958 break; |
| 959 if (guess_width > available_width) { |
| 960 hi = guess - 1; |
| 961 // Move back if we are on loop terminating condition, and guess is wider |
| 962 // than available. |
| 963 if (hi < lo) |
| 964 lo = hi; |
| 965 } else { |
| 966 lo = guess + 1; |
| 967 } |
| 968 } |
| 969 |
| 970 return render_text->text(); |
| 971 } |
| 972 |
876 RenderText::RenderText() | 973 RenderText::RenderText() |
877 : horizontal_alignment_(base::i18n::IsRTL() ? ALIGN_RIGHT : ALIGN_LEFT), | 974 : horizontal_alignment_(base::i18n::IsRTL() ? ALIGN_RIGHT : ALIGN_LEFT), |
878 directionality_mode_(DIRECTIONALITY_FROM_TEXT), | 975 directionality_mode_(DIRECTIONALITY_FROM_TEXT), |
879 text_direction_(base::i18n::UNKNOWN_DIRECTION), | 976 text_direction_(base::i18n::UNKNOWN_DIRECTION), |
880 cursor_enabled_(true), | 977 cursor_enabled_(true), |
881 cursor_visible_(false), | 978 cursor_visible_(false), |
882 insert_mode_(true), | 979 insert_mode_(true), |
883 cursor_color_(kDefaultColor), | 980 cursor_color_(kDefaultColor), |
884 selection_color_(kDefaultColor), | 981 selection_color_(kDefaultColor), |
885 selection_background_focused_color_(kDefaultSelectionBackgroundColor), | 982 selection_background_focused_color_(kDefaultSelectionBackgroundColor), |
(...skipping 276 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1162 icu::StringCharacterIterator iter(text.c_str()); | 1259 icu::StringCharacterIterator iter(text.c_str()); |
1163 iter.setIndex32(truncate_length_ - 1); | 1260 iter.setIndex32(truncate_length_ - 1); |
1164 layout_text_.assign(text.substr(0, iter.getIndex()) + kEllipsisUTF16); | 1261 layout_text_.assign(text.substr(0, iter.getIndex()) + kEllipsisUTF16); |
1165 } | 1262 } |
1166 | 1263 |
1167 if (elide_behavior_ != TRUNCATE && elide_behavior_ != FADE_TAIL && | 1264 if (elide_behavior_ != TRUNCATE && elide_behavior_ != FADE_TAIL && |
1168 display_rect_.width() > 0 && !layout_text_.empty() && | 1265 display_rect_.width() > 0 && !layout_text_.empty() && |
1169 GetContentWidth() > display_rect_.width()) { | 1266 GetContentWidth() > display_rect_.width()) { |
1170 // This doesn't trim styles so ellipsis may get rendered as a different | 1267 // This doesn't trim styles so ellipsis may get rendered as a different |
1171 // style than the preceding text. See crbug.com/327850. | 1268 // style than the preceding text. See crbug.com/327850. |
1172 layout_text_.assign(ElideText(layout_text_)); | 1269 layout_text_.assign( |
| 1270 Elide(layout_text_, display_rect_.width(), elide_behavior_)); |
1173 } | 1271 } |
1174 | 1272 |
1175 ResetLayout(); | 1273 ResetLayout(); |
1176 } | 1274 } |
1177 | 1275 |
1178 // TODO(skanuj): Fix code duplication with ElideText in ui/gfx/text_elider.cc | 1276 base::string16 RenderText::ElideEmail(const base::string16& email, |
1179 // See crbug.com/327846 | 1277 float available_width) { |
1180 base::string16 RenderText::ElideText(const base::string16& text) { | 1278 // The returned string will have at least one character besides the ellipses |
1181 const bool insert_ellipsis = (elide_behavior_ != TRUNCATE); | 1279 // on either side of '@'; if that's impossible a single ellipsis is returned. |
1182 // Create a RenderText copy with attributes that affect the rendering width. | 1280 // If possible, only the username is elided. Otherwise, the domain is elided |
1183 scoped_ptr<RenderText> render_text(CreateInstance()); | 1281 // in the middle, splitting available width equally with the elided username. |
1184 render_text->SetFontList(font_list_); | 1282 // If the username is short enough that it doesn't need half the available |
1185 render_text->SetDirectionalityMode(directionality_mode_); | 1283 // width, the elided domain will occupy that extra width. |
1186 render_text->SetCursorEnabled(cursor_enabled_); | |
1187 | 1284 |
1188 render_text->styles_ = styles_; | 1285 // Split the email into its local-part (username) and domain-part. The email |
1189 render_text->colors_ = colors_; | 1286 // spec allows for @ symbols in the username under some special requirements, |
1190 render_text->SetText(text); | 1287 // but not in the domain part, so splitting at the last @ symbol is safe. |
1191 const int current_text_pixel_width = render_text->GetContentWidth(); | 1288 const size_t split_index = email.find_last_of('@'); |
| 1289 DCHECK_NE(split_index, base::string16::npos); |
| 1290 base::string16 username = email.substr(0, split_index); |
| 1291 base::string16 domain = email.substr(split_index + 1); |
| 1292 DCHECK(!username.empty()); |
| 1293 DCHECK(!domain.empty()); |
1192 | 1294 |
1193 const base::string16 ellipsis = base::string16(kEllipsisUTF16); | 1295 // Subtract the @ symbol from the available width as it is mandatory. |
1194 const bool elide_in_middle = false; | 1296 const base::string16 kAtSignUTF16 = base::ASCIIToUTF16("@"); |
1195 const bool elide_at_beginning = false; | 1297 available_width -= GetStringWidthF(kAtSignUTF16, font_list()); |
1196 StringSlicer slicer(text, ellipsis, elide_in_middle, elide_at_beginning); | |
1197 | 1298 |
1198 // Pango will return 0 width for absurdly long strings. Cut the string in | 1299 // Check whether eliding the domain is necessary: if eliding the username |
1199 // half and try again. | 1300 // is sufficient, the domain will not be elided. |
1200 // This is caused by an int overflow in Pango (specifically, in | 1301 const float full_username_width = GetStringWidthF(username, font_list()); |
1201 // pango_glyph_string_extents_range). It's actually more subtle than just | 1302 const float available_domain_width = available_width - |
1202 // returning 0, since on super absurdly long strings, the int can wrap and | 1303 std::min(full_username_width, |
1203 // return positive numbers again. Detecting that is probably not worth it | 1304 GetStringWidthF(username.substr(0, 1) + kEllipsisUTF16, font_list())); |
1204 // (eliding way too much from a ridiculous string is probably still | 1305 if (GetStringWidthF(domain, font_list()) > available_domain_width) { |
1205 // ridiculous), but we should check other widths for bogus values as well. | 1306 // Elide the domain so that it only takes half of the available width. |
1206 if (current_text_pixel_width <= 0 && !text.empty()) | 1307 // Should the username not need all the width available in its half, the |
1207 return ElideText(slicer.CutString(text.length() / 2, insert_ellipsis)); | 1308 // domain will occupy the leftover width. |
1208 | 1309 // If |desired_domain_width| is greater than |available_domain_width|: the |
1209 if (current_text_pixel_width <= display_rect_.width()) | 1310 // minimal username elision allowed by the specifications will not fit; thus |
1210 return text; | 1311 // |desired_domain_width| must be <= |available_domain_width| at all cost. |
1211 | 1312 const float desired_domain_width = |
1212 render_text->SetText(base::string16()); | 1313 std::min<float>(available_domain_width, |
1213 render_text->SetText(ellipsis); | 1314 std::max<float>(available_width - full_username_width, |
1214 const int ellipsis_width = render_text->GetContentWidth(); | 1315 available_width / 2)); |
1215 | 1316 domain = Elide(domain, desired_domain_width, ELIDE_MIDDLE); |
1216 if (insert_ellipsis && (ellipsis_width >= display_rect_.width())) | 1317 // Failing to elide the domain such that at least one character remains |
1217 return base::string16(); | 1318 // (other than the ellipsis itself) remains: return a single ellipsis. |
1218 | 1319 if (domain.length() <= 1U) |
1219 // Use binary search to compute the elided text. | 1320 return base::string16(kEllipsisUTF16); |
1220 size_t lo = 0; | |
1221 size_t hi = text.length() - 1; | |
1222 const base::i18n::TextDirection text_direction = GetTextDirection(); | |
1223 for (size_t guess = (lo + hi) / 2; lo <= hi; guess = (lo + hi) / 2) { | |
1224 // Restore styles and colors. They will be truncated to size by SetText. | |
1225 render_text->styles_ = styles_; | |
1226 render_text->colors_ = colors_; | |
1227 base::string16 new_text = slicer.CutString(guess, false); | |
1228 render_text->SetText(new_text); | |
1229 | |
1230 // This has to be an additional step so that the ellipsis is rendered with | |
1231 // same style as trailing part of the text. | |
1232 if (insert_ellipsis) { | |
1233 // When ellipsis follows text whose directionality is not the same as that | |
1234 // of the whole text, it will be rendered with the directionality of the | |
1235 // whole text. Since we want ellipsis to indicate continuation of the | |
1236 // preceding text, we force the directionality of ellipsis to be same as | |
1237 // the preceding text using LTR or RTL markers. | |
1238 base::i18n::TextDirection trailing_text_direction = | |
1239 base::i18n::GetLastStrongCharacterDirection(new_text); | |
1240 new_text.append(ellipsis); | |
1241 if (trailing_text_direction != text_direction) { | |
1242 if (trailing_text_direction == base::i18n::LEFT_TO_RIGHT) | |
1243 new_text += base::i18n::kLeftToRightMark; | |
1244 else | |
1245 new_text += base::i18n::kRightToLeftMark; | |
1246 } | |
1247 render_text->SetText(new_text); | |
1248 } | |
1249 | |
1250 // We check the width of the whole desired string at once to ensure we | |
1251 // handle kerning/ligatures/etc. correctly. | |
1252 const int guess_width = render_text->GetContentWidth(); | |
1253 if (guess_width == display_rect_.width()) | |
1254 break; | |
1255 if (guess_width > display_rect_.width()) { | |
1256 hi = guess - 1; | |
1257 // Move back if we are on loop terminating condition, and guess is wider | |
1258 // than available. | |
1259 if (hi < lo) | |
1260 lo = hi; | |
1261 } else { | |
1262 lo = guess + 1; | |
1263 } | |
1264 } | 1321 } |
1265 | 1322 |
1266 return render_text->text(); | 1323 // Fit the username in the remaining width (at this point the elided username |
| 1324 // is guaranteed to fit with at least one character remaining given all the |
| 1325 // precautions taken earlier). |
| 1326 available_width -= GetStringWidthF(domain, font_list()); |
| 1327 username = Elide(username, available_width, ELIDE_TAIL); |
| 1328 return username + kAtSignUTF16 + domain; |
1267 } | 1329 } |
1268 | 1330 |
1269 void RenderText::UpdateCachedBoundsAndOffset() { | 1331 void RenderText::UpdateCachedBoundsAndOffset() { |
1270 if (cached_bounds_and_offset_valid_) | 1332 if (cached_bounds_and_offset_valid_) |
1271 return; | 1333 return; |
1272 | 1334 |
1273 // TODO(ckocagil): Add support for scrolling multiline text. | 1335 // TODO(ckocagil): Add support for scrolling multiline text. |
1274 | 1336 |
1275 // First, set the valid flag true to calculate the current cursor bounds using | 1337 // First, set the valid flag true to calculate the current cursor bounds using |
1276 // the stale |display_offset_|. Applying |delta_offset| at the end of this | 1338 // the stale |display_offset_|. Applying |delta_offset| at the end of this |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1316 cursor_bounds_ += delta_offset; | 1378 cursor_bounds_ += delta_offset; |
1317 } | 1379 } |
1318 | 1380 |
1319 void RenderText::DrawSelection(Canvas* canvas) { | 1381 void RenderText::DrawSelection(Canvas* canvas) { |
1320 const std::vector<Rect> sel = GetSubstringBounds(selection()); | 1382 const std::vector<Rect> sel = GetSubstringBounds(selection()); |
1321 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) | 1383 for (std::vector<Rect>::const_iterator i = sel.begin(); i < sel.end(); ++i) |
1322 canvas->FillRect(*i, selection_background_focused_color_); | 1384 canvas->FillRect(*i, selection_background_focused_color_); |
1323 } | 1385 } |
1324 | 1386 |
1325 } // namespace gfx | 1387 } // namespace gfx |
OLD | NEW |