[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 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 |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "ui/gfx/canvas_skia.h" |
| 6 | |
| 7 | #include "base/i18n/rtl.h" |
| 8 | #include "base/logging.h" |
| 9 | #include "base/memory/scoped_ptr.h" |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 10 | #include "ui/base/range/range.h" |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 11 | #include "ui/base/text/text_elider.h" |
| 12 | #include "ui/gfx/font.h" |
| 13 | #include "ui/gfx/font_list.h" |
| 14 | #include "ui/gfx/rect.h" |
| 15 | #include "ui/gfx/render_text.h" |
| 16 | #include "ui/gfx/skia_util.h" |
| 17 | |
| 18 | namespace { |
| 19 | |
| 20 | // Based on |flags| and |text| content, returns whether text should be |
| 21 | // rendered right-to-left. |
| 22 | bool IsTextRTL(int flags, const string16& text) { |
| 23 | if (flags & gfx::Canvas::FORCE_RTL_DIRECTIONALITY) |
| 24 | return true; |
| 25 | if (flags & gfx::Canvas::FORCE_LTR_DIRECTIONALITY) |
| 26 | return false; |
| 27 | return base::i18n::IsRTL() && base::i18n::StringContainsStrongRTLChars(text); |
| 28 | } |
| 29 | |
| 30 | // Checks each pixel immediately adjacent to the given pixel in the bitmap. If |
| 31 | // any of them are not the halo color, returns true. This defines the halo of |
| 32 | // pixels that will appear around the text. Note that we have to check each |
| 33 | // pixel against both the halo color and transparent since |DrawStringWithHalo| |
| 34 | // will modify the bitmap as it goes, and cleared pixels shouldn't count as |
| 35 | // changed. |
| 36 | bool PixelShouldGetHalo(const SkBitmap& bitmap, |
| 37 | int x, int y, |
| 38 | SkColor halo_color) { |
| 39 | if (x > 0 && |
| 40 | *bitmap.getAddr32(x - 1, y) != halo_color && |
| 41 | *bitmap.getAddr32(x - 1, y) != 0) |
| 42 | return true; // Touched pixel to the left. |
| 43 | if (x < bitmap.width() - 1 && |
| 44 | *bitmap.getAddr32(x + 1, y) != halo_color && |
| 45 | *bitmap.getAddr32(x + 1, y) != 0) |
| 46 | return true; // Touched pixel to the right. |
| 47 | if (y > 0 && |
| 48 | *bitmap.getAddr32(x, y - 1) != halo_color && |
| 49 | *bitmap.getAddr32(x, y - 1) != 0) |
| 50 | return true; // Touched pixel above. |
| 51 | if (y < bitmap.height() - 1 && |
| 52 | *bitmap.getAddr32(x, y + 1) != halo_color && |
| 53 | *bitmap.getAddr32(x, y + 1) != 0) |
| 54 | return true; // Touched pixel below. |
| 55 | return false; |
| 56 | } |
| 57 | |
| 58 | // Apply vertical alignment per |flags|. Returns y-coordinate delta. |
| 59 | int VAlignText(const gfx::Font& font, |
| 60 | int line_count, |
| 61 | int flags, |
| 62 | int available_height) { |
| 63 | const int text_size = font.GetFontSize(); |
| 64 | |
| 65 | if (flags & gfx::Canvas::TEXT_VALIGN_TOP) |
| 66 | return text_size; |
| 67 | |
| 68 | if (flags & gfx::Canvas::TEXT_VALIGN_BOTTOM) { |
| 69 | // Note: The -1 was chosen empirically to match the existing GDI code. |
| 70 | int offset = available_height + text_size - font.GetHeight() - 1; |
| 71 | if (line_count > 1) |
| 72 | offset -= (line_count * text_size); |
| 73 | return offset; |
| 74 | } |
| 75 | |
| 76 | // Default case: TEXT_VALIGN_MIDDLE. |
| 77 | // Note: The +1 below and the -2 further down were chosen empirically to match |
| 78 | // the alignment and rounding in the existing GDI code. |
| 79 | int double_offset = available_height + text_size + 1; |
| 80 | if (line_count > 1) |
| 81 | double_offset -= (line_count * text_size); |
| 82 | return double_offset / 2 - 2; |
| 83 | } |
| 84 | |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 85 | // Strips accelerator character prefixes in |text| if needed, based on |flags|. |
| 86 | // Returns a range in |text| to underline or ui::Range::InvalidRange() if |
| 87 | // underlining is not needed. |
| 88 | ui::Range StripAcceleratorChars(int flags, string16* text) { |
| 89 | if (flags & (gfx::Canvas::SHOW_PREFIX | gfx::Canvas::HIDE_PREFIX)) { |
| 90 | int char_pos = -1; |
| 91 | int char_span = 0; |
| 92 | *text = gfx::RemoveAcceleratorChar(*text, '&', &char_pos, &char_span); |
| 93 | if ((flags & gfx::Canvas::SHOW_PREFIX) && char_pos != -1) |
| 94 | return ui::Range(char_pos, char_pos + char_span); |
| 95 | } |
| 96 | return ui::Range::InvalidRange(); |
| 97 | } |
| 98 | |
| 99 | // Elides |text| and adjusts |range| appropriately. If eliding causes |range| |
| 100 | // to no longer point to the same character in |text|, |range| is made invalid. |
| 101 | void ElideTextAndAdjustRange(const gfx::Font& font, |
| 102 | int width, |
| 103 | string16* text, |
| 104 | ui::Range* range) { |
| 105 | const char16 start_char = (range->IsValid() ? text->at(range->start()) : 0); |
| 106 | *text = ui::ElideText(*text, font, width, ui::ELIDE_AT_END); |
| 107 | if (!range->IsValid()) |
| 108 | return; |
| 109 | if (range->start() >= text->length() || |
| 110 | text->at(range->start()) != start_char) { |
| 111 | *range = ui::Range::InvalidRange(); |
| 112 | } |
| 113 | } |
| 114 | |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 115 | // Updates |render_text| from the specified parameters. |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 116 | void UpdateRenderText(const gfx::Rect& rect, |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 117 | const string16& text, |
| 118 | const gfx::Font& font, |
| 119 | int flags, |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 120 | SkColor color, |
| 121 | gfx::RenderText* render_text) { |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 122 | render_text->SetFontList(gfx::FontList(font)); |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 123 | render_text->SetText(text); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 124 | render_text->SetCursorEnabled(false); |
| 125 | |
| 126 | gfx::Rect display_rect = rect; |
| 127 | display_rect.Offset(0, -font.GetFontSize()); |
| 128 | display_rect.set_height(font.GetHeight()); |
| 129 | render_text->SetDisplayRect(display_rect); |
| 130 | |
| 131 | if (flags & gfx::Canvas::TEXT_ALIGN_RIGHT) |
| 132 | render_text->SetHorizontalAlignment(gfx::ALIGN_RIGHT); |
| 133 | else if (flags & gfx::Canvas::TEXT_ALIGN_CENTER) |
| 134 | render_text->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| 135 | else |
| 136 | render_text->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| 137 | |
| 138 | gfx::StyleRange style; |
| 139 | style.foreground = color; |
| 140 | style.font_style = font.GetStyle(); |
| 141 | if (font.GetStyle() & gfx::Font::UNDERLINED) |
| 142 | style.underline = true; |
| 143 | render_text->set_default_style(style); |
| 144 | render_text->ApplyDefaultStyle(); |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 145 | } |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 146 | |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 147 | // Adds an underline style to |render_text| over |range|. |
| 148 | void ApplyUnderlineStyle(const ui::Range& range, gfx::RenderText* render_text) { |
| 149 | gfx::StyleRange style = render_text->default_style(); |
| 150 | if (range.IsValid() && !style.underline) { |
| 151 | style.range = range; |
| 152 | style.underline = true; |
| 153 | render_text->ApplyStyleRange(style); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 154 | } |
| 155 | } |
| 156 | |
| 157 | } // anonymous namespace |
| 158 | |
| 159 | namespace gfx { |
| 160 | |
| 161 | // static |
| 162 | void CanvasSkia::SizeStringInt(const string16& text, |
| 163 | const gfx::Font& font, |
| 164 | int* width, int* height, |
| 165 | int flags) { |
| 166 | DCHECK_GE(*width, 0); |
| 167 | DCHECK_GE(*height, 0); |
| 168 | |
| 169 | if ((flags & MULTI_LINE) && *width != 0) { |
| 170 | ui::WordWrapBehavior wrap_behavior = ui::TRUNCATE_LONG_WORDS; |
| 171 | if (flags & CHARACTER_BREAK) |
| 172 | wrap_behavior = ui::WRAP_LONG_WORDS; |
| 173 | else if (!(flags & NO_ELLIPSIS)) |
| 174 | wrap_behavior = ui::ELIDE_LONG_WORDS; |
| 175 | |
| 176 | gfx::Rect rect(*width, INT_MAX); |
| 177 | std::vector<string16> strings; |
| 178 | ui::ElideRectangleText(text, font, rect.width(), rect.height(), |
| 179 | wrap_behavior, &strings); |
| 180 | scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 181 | UpdateRenderText(rect, string16(), font, flags, 0, render_text.get()); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 182 | |
| 183 | int h = 0; |
| 184 | int w = 0; |
| 185 | for (size_t i = 0; i < strings.size(); ++i) { |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 186 | StripAcceleratorChars(flags, &strings[i]); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 187 | render_text->SetText(strings[i]); |
| 188 | w = std::max(w, render_text->GetStringWidth()); |
| 189 | h += font.GetHeight(); |
| 190 | } |
| 191 | *width = w; |
| 192 | *height = h; |
| 193 | } else { |
| 194 | // If the string is too long, the call by |RenderTextWin| to |ScriptShape()| |
| 195 | // will inexplicably fail with result E_INVALIDARG. Guard against this. |
| 196 | const size_t kMaxRenderTextLength = 5000; |
| 197 | if (text.length() >= kMaxRenderTextLength) { |
| 198 | *width = text.length() * font.GetAverageCharacterWidth(); |
| 199 | } else { |
| 200 | scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); |
| 201 | gfx::Rect rect(*width, *height); |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 202 | string16 adjusted_text = text; |
| 203 | StripAcceleratorChars(flags, &adjusted_text); |
| 204 | UpdateRenderText(rect, adjusted_text, font, flags, 0, render_text.get()); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 205 | *width = render_text->GetStringWidth(); |
| 206 | } |
| 207 | *height = font.GetHeight(); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | void CanvasSkia::DrawStringInt(const string16& text, |
| 212 | const gfx::Font& font, |
| 213 | const SkColor& color, |
| 214 | int x, int y, int w, int h, |
| 215 | int flags) { |
| 216 | if (!IntersectsClipRectInt(x, y, w, h)) |
| 217 | return; |
| 218 | |
| 219 | // TODO(asvitkine): On Windows, MULTI_LINE implies top alignment. |
| 220 | // http://crbug.com/107357 |
| 221 | if (flags & MULTI_LINE) { |
| 222 | flags &= ~(TEXT_VALIGN_MIDDLE | TEXT_VALIGN_BOTTOM); |
| 223 | flags |= TEXT_VALIGN_TOP; |
| 224 | } |
| 225 | |
| 226 | gfx::Rect rect(x, y, w, h); |
| 227 | canvas_->save(SkCanvas::kClip_SaveFlag); |
| 228 | ClipRect(rect); |
| 229 | |
| 230 | string16 adjusted_text = text; |
| 231 | if (IsTextRTL(flags, text)) |
| 232 | base::i18n::AdjustStringForLocaleDirection(&adjusted_text); |
| 233 | |
| 234 | scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); |
| 235 | |
| 236 | if (flags & MULTI_LINE) { |
| 237 | ui::WordWrapBehavior wrap_behavior = ui::IGNORE_LONG_WORDS; |
| 238 | if (flags & CHARACTER_BREAK) |
| 239 | wrap_behavior = ui::WRAP_LONG_WORDS; |
| 240 | else if (!(flags & NO_ELLIPSIS)) |
| 241 | wrap_behavior = ui::ELIDE_LONG_WORDS; |
| 242 | |
| 243 | std::vector<string16> strings; |
| 244 | ui::ElideRectangleText(adjusted_text, font, w, h, wrap_behavior, |
| 245 | &strings); |
| 246 | |
| 247 | rect.Offset(0, VAlignText(font, strings.size(), flags, h)); |
| 248 | for (size_t i = 0; i < strings.size(); i++) { |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 249 | ui::Range range = StripAcceleratorChars(flags, &strings[i]); |
| 250 | UpdateRenderText(rect, strings[i], font, flags, color, render_text.get()); |
| 251 | ApplyUnderlineStyle(range, render_text.get()); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 252 | render_text->Draw(this); |
| 253 | rect.Offset(0, font.GetHeight()); |
| 254 | } |
| 255 | } else { |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 256 | ui::Range range = StripAcceleratorChars(flags, &adjusted_text); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 257 | if (!(flags & NO_ELLIPSIS)) |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 258 | ElideTextAndAdjustRange(font, w, &adjusted_text, &range); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 259 | |
| 260 | rect.Offset(0, VAlignText(font, 1, flags, h)); |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 261 | UpdateRenderText(rect, adjusted_text, font, flags, color, |
| 262 | render_text.get()); |
| 263 | ApplyUnderlineStyle(range, render_text.get()); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 264 | render_text->Draw(this); |
| 265 | } |
| 266 | |
| 267 | canvas_->restore(); |
| 268 | } |
| 269 | |
| 270 | void CanvasSkia::DrawStringWithHalo(const string16& text, |
| 271 | const gfx::Font& font, |
| 272 | const SkColor& text_color, |
| 273 | const SkColor& halo_color_in, |
| 274 | int x, int y, int w, int h, |
| 275 | int flags) { |
| 276 | // Some callers will have semitransparent halo colors, which we don't handle |
| 277 | // (since the resulting image can have 1-bit transparency only). |
| 278 | SkColor halo_color = halo_color_in | 0xFF000000; |
| 279 | |
| 280 | // Create a temporary buffer filled with the halo color. It must leave room |
| 281 | // for the 1-pixel border around the text. |
| 282 | gfx::Size size(w + 2, h + 2); |
| 283 | CanvasSkia text_canvas(size, true); |
| 284 | SkPaint bkgnd_paint; |
| 285 | bkgnd_paint.setColor(halo_color); |
| 286 | text_canvas.DrawRect(gfx::Rect(size), bkgnd_paint); |
| 287 | |
| 288 | // Draw the text into the temporary buffer. This will have correct |
| 289 | // ClearType since the background color is the same as the halo color. |
| 290 | text_canvas.DrawStringInt(text, font, text_color, 1, 1, w, h, flags); |
| 291 | |
| 292 | uint32_t halo_premul = SkPreMultiplyColor(halo_color); |
| 293 | SkBitmap& text_bitmap = const_cast<SkBitmap&>( |
| 294 | skia::GetTopDevice(*text_canvas.sk_canvas())->accessBitmap(true)); |
| 295 | |
| 296 | for (int cur_y = 0; cur_y < h + 2; cur_y++) { |
| 297 | uint32_t* text_row = text_bitmap.getAddr32(0, cur_y); |
| 298 | for (int cur_x = 0; cur_x < w + 2; cur_x++) { |
| 299 | if (text_row[cur_x] == halo_premul) { |
| 300 | // This pixel was not touched by the text routines. See if it borders |
| 301 | // a touched pixel in any of the 4 directions (not diagonally). |
| 302 | if (!PixelShouldGetHalo(text_bitmap, cur_x, cur_y, halo_premul)) |
| 303 | text_row[cur_x] = 0; // Make transparent. |
| 304 | } else { |
| 305 | text_row[cur_x] |= 0xff << SK_A32_SHIFT; // Make opaque. |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | // Draw the halo bitmap with blur. |
| 311 | DrawBitmapInt(text_bitmap, x - 1, y - 1); |
| 312 | } |
| 313 | |
| 314 | void CanvasSkia::DrawFadeTruncatingString( |
| 315 | const string16& text, |
| 316 | CanvasSkia::TruncateFadeMode truncate_mode, |
| 317 | size_t desired_characters_to_truncate_from_head, |
| 318 | const gfx::Font& font, |
| 319 | const SkColor& color, |
| 320 | const gfx::Rect& display_rect) { |
| 321 | int flags = NO_ELLIPSIS; |
| 322 | |
| 323 | // If the whole string fits in the destination then just draw it directly. |
| 324 | if (GetStringWidth(text, font) <= display_rect.width()) { |
| 325 | DrawStringInt(text, font, color, display_rect.x(), display_rect.y(), |
| 326 | display_rect.width(), display_rect.height(), flags); |
| 327 | return; |
| 328 | } |
| 329 | |
| 330 | scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); |
| 331 | string16 clipped_text = text; |
| 332 | const bool is_rtl = IsTextRTL(flags, text); |
| 333 | if (is_rtl) |
| 334 | base::i18n::AdjustStringForLocaleDirection(&clipped_text); |
| 335 | |
| 336 | switch (truncate_mode) { |
| 337 | case TruncateFadeTail: |
| 338 | render_text->set_fade_tail(true); |
| 339 | if (is_rtl) |
| 340 | flags |= TEXT_ALIGN_RIGHT; |
| 341 | break; |
| 342 | case TruncateFadeHead: |
| 343 | render_text->set_fade_head(true); |
| 344 | if (!is_rtl) |
| 345 | flags |= TEXT_ALIGN_RIGHT; |
| 346 | break; |
| 347 | case TruncateFadeHeadAndTail: |
| 348 | DCHECK_GT(desired_characters_to_truncate_from_head, 0u); |
| 349 | // Due to the fade effect the first character is hard to see. |
| 350 | // We want to make sure that the first character starting at |
| 351 | // |desired_characters_to_truncate_from_head| is readable so we reduce |
| 352 | // the offset by a little bit. |
| 353 | desired_characters_to_truncate_from_head = |
| 354 | std::max<int>(0, desired_characters_to_truncate_from_head - 2); |
| 355 | |
| 356 | if (desired_characters_to_truncate_from_head) { |
| 357 | // Make sure to clip the text at a UTF16 boundary. |
| 358 | U16_SET_CP_LIMIT(text.data(), 0, |
| 359 | desired_characters_to_truncate_from_head, |
| 360 | text.length()); |
| 361 | clipped_text = text.substr(desired_characters_to_truncate_from_head); |
| 362 | } |
| 363 | |
| 364 | render_text->set_fade_tail(true); |
| 365 | render_text->set_fade_head(true); |
| 366 | break; |
| 367 | } |
| 368 | |
[email protected] | 5bf0faf | 2012-01-31 03:02:52 | [diff] [blame] | 369 | gfx::Rect rect = display_rect; |
| 370 | rect.Offset(0, VAlignText(font, 1, flags, display_rect.height())); |
| 371 | UpdateRenderText(rect, clipped_text, font, flags, color, render_text.get()); |
[email protected] | d18e5031 | 2012-01-25 17:49:35 | [diff] [blame] | 372 | |
| 373 | canvas_->save(SkCanvas::kClip_SaveFlag); |
| 374 | ClipRect(display_rect); |
| 375 | render_text->Draw(this); |
| 376 | canvas_->restore(); |
| 377 | } |
| 378 | |
| 379 | } // namespace gfx |