| 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_win.h" | 5 #include "ui/gfx/render_text_win.h" |
| 6 | 6 |
| 7 #include <algorithm> | 7 #include <algorithm> |
| 8 | 8 |
| 9 #include "base/i18n/break_iterator.h" | 9 #include "base/i18n/break_iterator.h" |
| 10 #include "base/i18n/rtl.h" | 10 #include "base/i18n/rtl.h" |
| 11 #include "base/logging.h" | 11 #include "base/logging.h" |
| 12 #include "base/strings/string_util.h" | 12 #include "base/strings/string_util.h" |
| 13 #include "base/strings/utf_string_conversions.h" | 13 #include "base/strings/utf_string_conversions.h" |
| 14 #include "base/win/windows_version.h" | 14 #include "base/win/windows_version.h" |
| 15 #include "ui/base/text/utf16_indexing.h" | 15 #include "ui/base/text/utf16_indexing.h" |
| 16 #include "ui/gfx/canvas.h" | 16 #include "ui/gfx/canvas.h" |
| 17 #include "ui/gfx/font_fallback_win.h" | 17 #include "ui/gfx/font_fallback_win.h" |
| 18 #include "ui/gfx/font_smoothing_win.h" | 18 #include "ui/gfx/font_smoothing_win.h" |
| 19 #include "ui/gfx/platform_font_win.h" | 19 #include "ui/gfx/platform_font_win.h" |
| 20 | 20 |
| 21 namespace gfx { | 21 namespace gfx { |
| 22 | 22 |
| 23 namespace { | 23 namespace { |
| 24 | 24 |
| 25 // The maximum supported number of Uniscribe runs; a SCRIPT_ITEM is 8 bytes. | 25 // The maximum length of text supported for Uniscribe layout and display. |
| 26 // TODO(msw): Review memory use/failure? Max string length? Alternate approach? | 26 // This empirically chosen value should prevent major performance degradations. |
| 27 const int kGuessItems = 100; | 27 // TODO(msw): Support longer text, partial layout/painting, etc. |
| 28 const int kMaxItems = 10000; | 28 const size_t kMaxUniscribeTextLength = 10000; |
| 29 | 29 |
| 30 // The maximum supported number of Uniscribe glyphs; a glyph is 1 word. | 30 // The initial guess and maximum supported number of runs; arbitrary values. |
| 31 // TODO(msw): Review memory use/failure? Max string length? Alternate approach? | 31 // TODO(msw): Support more runs, determine a better initial guess, etc. |
| 32 const int kMaxGlyphs = 100000; | 32 const int kGuessRuns = 100; |
| 33 const size_t kMaxRuns = 10000; |
| 34 |
| 35 // The maximum number of glyphs per run; ScriptShape fails on larger values. |
| 36 const size_t kMaxGlyphs = 65535; |
| 33 | 37 |
| 34 // Callback to |EnumEnhMetaFile()| to intercept font creation. | 38 // Callback to |EnumEnhMetaFile()| to intercept font creation. |
| 35 int CALLBACK MetaFileEnumProc(HDC hdc, | 39 int CALLBACK MetaFileEnumProc(HDC hdc, |
| 36 HANDLETABLE* table, | 40 HANDLETABLE* table, |
| 37 CONST ENHMETARECORD* record, | 41 CONST ENHMETARECORD* record, |
| 38 int table_entries, | 42 int table_entries, |
| 39 LPARAM log_font) { | 43 LPARAM log_font) { |
| 40 if (record->iType == EMR_EXTCREATEFONTINDIRECTW) { | 44 if (record->iType == EMR_EXTCREATEFONTINDIRECTW) { |
| 41 const EMREXTCREATEFONTINDIRECTW* create_font_record = | 45 const EMREXTCREATEFONTINDIRECTW* create_font_record = |
| 42 reinterpret_cast<const EMREXTCREATEFONTINDIRECTW*>(record); | 46 reinterpret_cast<const EMREXTCREATEFONTINDIRECTW*>(record); |
| (...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 174 // static | 178 // static |
| 175 HDC RenderTextWin::cached_hdc_ = NULL; | 179 HDC RenderTextWin::cached_hdc_ = NULL; |
| 176 | 180 |
| 177 // static | 181 // static |
| 178 std::map<std::string, Font> RenderTextWin::successful_substitute_fonts_; | 182 std::map<std::string, Font> RenderTextWin::successful_substitute_fonts_; |
| 179 | 183 |
| 180 RenderTextWin::RenderTextWin() | 184 RenderTextWin::RenderTextWin() |
| 181 : RenderText(), | 185 : RenderText(), |
| 182 common_baseline_(0), | 186 common_baseline_(0), |
| 183 needs_layout_(false) { | 187 needs_layout_(false) { |
| 188 set_truncate_length(kMaxUniscribeTextLength); |
| 189 |
| 184 memset(&script_control_, 0, sizeof(script_control_)); | 190 memset(&script_control_, 0, sizeof(script_control_)); |
| 185 memset(&script_state_, 0, sizeof(script_state_)); | 191 memset(&script_state_, 0, sizeof(script_state_)); |
| 186 | 192 |
| 187 MoveCursorTo(EdgeSelectionModel(CURSOR_LEFT)); | 193 MoveCursorTo(EdgeSelectionModel(CURSOR_LEFT)); |
| 188 } | 194 } |
| 189 | 195 |
| 190 RenderTextWin::~RenderTextWin() { | 196 RenderTextWin::~RenderTextWin() { |
| 191 } | 197 } |
| 192 | 198 |
| 193 Size RenderTextWin::GetStringSize() { | 199 Size RenderTextWin::GetStringSize() { |
| 194 EnsureLayout(); | 200 EnsureLayout(); |
| 195 return string_size_; | 201 return string_size_; |
| 196 } | 202 } |
| 197 | 203 |
| 198 int RenderTextWin::GetBaseline() { | 204 int RenderTextWin::GetBaseline() { |
| 199 EnsureLayout(); | 205 EnsureLayout(); |
| 200 return common_baseline_; | 206 return common_baseline_; |
| 201 } | 207 } |
| 202 | 208 |
| 203 SelectionModel RenderTextWin::FindCursorPosition(const Point& point) { | 209 SelectionModel RenderTextWin::FindCursorPosition(const Point& point) { |
| 204 if (text().empty()) | 210 if (text().empty()) |
| 205 return SelectionModel(); | 211 return SelectionModel(); |
| 206 | 212 |
| 207 EnsureLayout(); | 213 EnsureLayout(); |
| 208 // Find the run that contains the point and adjust the argument location. | 214 // Find the run that contains the point and adjust the argument location. |
| 209 int x = ToTextPoint(point).x(); | 215 int x = ToTextPoint(point).x(); |
| 210 size_t run_index = GetRunContainingXCoord(x); | 216 size_t run_index = GetRunContainingXCoord(x); |
| 211 if (run_index == runs_.size()) | 217 if (run_index >= runs_.size()) |
| 212 return EdgeSelectionModel((x < 0) ? CURSOR_LEFT : CURSOR_RIGHT); | 218 return EdgeSelectionModel((x < 0) ? CURSOR_LEFT : CURSOR_RIGHT); |
| 213 internal::TextRun* run = runs_[run_index]; | 219 internal::TextRun* run = runs_[run_index]; |
| 214 | 220 |
| 215 int position = 0, trailing = 0; | 221 int position = 0, trailing = 0; |
| 216 HRESULT hr = ScriptXtoCP(x - run->preceding_run_widths, | 222 HRESULT hr = ScriptXtoCP(x - run->preceding_run_widths, |
| 217 run->range.length(), | 223 run->range.length(), |
| 218 run->glyph_count, | 224 run->glyph_count, |
| 219 run->logical_clusters.get(), | 225 run->logical_clusters.get(), |
| 220 run->visible_attributes.get(), | 226 run->visible_attributes.get(), |
| 221 run->advance_widths.get(), | 227 run->advance_widths.get(), |
| (...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 338 RenderText::SetSelectionModel(model); | 344 RenderText::SetSelectionModel(model); |
| 339 // TODO(xji|msw): The text selection color is applied in ItemizeLogicalText(). | 345 // TODO(xji|msw): The text selection color is applied in ItemizeLogicalText(). |
| 340 // So, the layout must be updated in order to draw the proper selection range. | 346 // So, the layout must be updated in order to draw the proper selection range. |
| 341 // Colors should be applied in DrawVisualText(), as done by RenderTextLinux. | 347 // Colors should be applied in DrawVisualText(), as done by RenderTextLinux. |
| 342 ResetLayout(); | 348 ResetLayout(); |
| 343 } | 349 } |
| 344 | 350 |
| 345 ui::Range RenderTextWin::GetGlyphBounds(size_t index) { | 351 ui::Range RenderTextWin::GetGlyphBounds(size_t index) { |
| 346 const size_t run_index = | 352 const size_t run_index = |
| 347 GetRunContainingCaret(SelectionModel(index, CURSOR_FORWARD)); | 353 GetRunContainingCaret(SelectionModel(index, CURSOR_FORWARD)); |
| 348 DCHECK_LT(run_index, runs_.size()); | 354 // Return edge bounds if the index is invalid or beyond the layout text size. |
| 355 if (run_index >= runs_.size()) |
| 356 return ui::Range(string_size_.width()); |
| 349 internal::TextRun* run = runs_[run_index]; | 357 internal::TextRun* run = runs_[run_index]; |
| 350 const size_t layout_index = TextIndexToLayoutIndex(index); | 358 const size_t layout_index = TextIndexToLayoutIndex(index); |
| 351 return ui::Range(GetGlyphXBoundary(run, layout_index, false), | 359 return ui::Range(GetGlyphXBoundary(run, layout_index, false), |
| 352 GetGlyphXBoundary(run, layout_index, true)); | 360 GetGlyphXBoundary(run, layout_index, true)); |
| 353 } | 361 } |
| 354 | 362 |
| 355 std::vector<Rect> RenderTextWin::GetSubstringBounds(const ui::Range& range) { | 363 std::vector<Rect> RenderTextWin::GetSubstringBounds(const ui::Range& range) { |
| 356 DCHECK(!needs_layout_); | 364 DCHECK(!needs_layout_); |
| 357 DCHECK(ui::Range(0, text().length()).Contains(range)); | 365 DCHECK(ui::Range(0, text().length()).Contains(range)); |
| 358 ui::Range layout_range(TextIndexToLayoutIndex(range.start()), | 366 ui::Range layout_range(TextIndexToLayoutIndex(range.start()), |
| (...skipping 20 matching lines...) Expand all Loading... |
| 379 rect.Union(bounds.back()); | 387 rect.Union(bounds.back()); |
| 380 bounds.pop_back(); | 388 bounds.pop_back(); |
| 381 } | 389 } |
| 382 bounds.push_back(rect); | 390 bounds.push_back(rect); |
| 383 } | 391 } |
| 384 } | 392 } |
| 385 return bounds; | 393 return bounds; |
| 386 } | 394 } |
| 387 | 395 |
| 388 size_t RenderTextWin::TextIndexToLayoutIndex(size_t index) const { | 396 size_t RenderTextWin::TextIndexToLayoutIndex(size_t index) const { |
| 389 if (!obscured()) | |
| 390 return index; | |
| 391 | |
| 392 DCHECK_LE(index, text().length()); | 397 DCHECK_LE(index, text().length()); |
| 393 const ptrdiff_t offset = ui::UTF16IndexToOffset(text(), 0, index); | 398 ptrdiff_t i = obscured() ? ui::UTF16IndexToOffset(text(), 0, index) : index; |
| 394 DCHECK_GE(offset, 0); | 399 CHECK_GE(i, 0); |
| 395 DCHECK_LE(static_cast<size_t>(offset), GetLayoutText().length()); | 400 // Clamp layout indices to the length of the text actually used for layout. |
| 396 return static_cast<size_t>(offset); | 401 return std::min<size_t>(GetLayoutText().length(), i); |
| 397 } | 402 } |
| 398 | 403 |
| 399 size_t RenderTextWin::LayoutIndexToTextIndex(size_t index) const { | 404 size_t RenderTextWin::LayoutIndexToTextIndex(size_t index) const { |
| 400 if (!obscured()) | 405 if (!obscured()) |
| 401 return index; | 406 return index; |
| 402 | 407 |
| 403 DCHECK_LE(index, GetLayoutText().length()); | 408 DCHECK_LE(index, GetLayoutText().length()); |
| 404 const size_t text_index = ui::UTF16OffsetToIndex(text(), 0, index); | 409 const size_t text_index = ui::UTF16OffsetToIndex(text(), 0, index); |
| 405 DCHECK_LE(text_index, text().length()); | 410 DCHECK_LE(text_index, text().length()); |
| 406 return text_index; | 411 return text_index; |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 489 // Set Uniscribe's base text direction. | 494 // Set Uniscribe's base text direction. |
| 490 script_state_.uBidiLevel = | 495 script_state_.uBidiLevel = |
| 491 (GetTextDirection() == base::i18n::RIGHT_TO_LEFT) ? 1 : 0; | 496 (GetTextDirection() == base::i18n::RIGHT_TO_LEFT) ? 1 : 0; |
| 492 | 497 |
| 493 if (text().empty()) | 498 if (text().empty()) |
| 494 return; | 499 return; |
| 495 | 500 |
| 496 HRESULT hr = E_OUTOFMEMORY; | 501 HRESULT hr = E_OUTOFMEMORY; |
| 497 int script_items_count = 0; | 502 int script_items_count = 0; |
| 498 std::vector<SCRIPT_ITEM> script_items; | 503 std::vector<SCRIPT_ITEM> script_items; |
| 499 const size_t text_length = GetLayoutText().length(); | 504 const size_t layout_text_length = GetLayoutText().length(); |
| 500 for (size_t n = kGuessItems; hr == E_OUTOFMEMORY && n < kMaxItems; n *= 2) { | 505 // Ensure that |kMaxRuns| is attempted and the loop terminates afterward. |
| 506 for (size_t runs = kGuessRuns; hr == E_OUTOFMEMORY && runs <= kMaxRuns; |
| 507 runs = std::max(runs + 1, std::min(runs * 2, kMaxRuns))) { |
| 501 // Derive the array of Uniscribe script items from the logical text. | 508 // Derive the array of Uniscribe script items from the logical text. |
| 502 // ScriptItemize always adds a terminal array item so that the length of the | 509 // ScriptItemize always adds a terminal array item so that the length of |
| 503 // last item can be derived from the terminal SCRIPT_ITEM::iCharPos. | 510 // the last item can be derived from the terminal SCRIPT_ITEM::iCharPos. |
| 504 script_items.resize(n); | 511 script_items.resize(runs); |
| 505 hr = ScriptItemize(GetLayoutText().c_str(), | 512 hr = ScriptItemize(GetLayoutText().c_str(), layout_text_length, |
| 506 text_length, | 513 runs - 1, &script_control_, &script_state_, |
| 507 n - 1, | 514 &script_items[0], &script_items_count); |
| 508 &script_control_, | |
| 509 &script_state_, | |
| 510 &script_items[0], | |
| 511 &script_items_count); | |
| 512 } | 515 } |
| 513 DCHECK(SUCCEEDED(hr)); | 516 DCHECK(SUCCEEDED(hr)); |
| 514 | 517 if (!SUCCEEDED(hr) || script_items_count <= 0) |
| 515 if (script_items_count <= 0) | |
| 516 return; | 518 return; |
| 517 | 519 |
| 518 // Temporarily apply composition underlines and selection colors. | 520 // Temporarily apply composition underlines and selection colors. |
| 519 ApplyCompositionAndSelectionStyles(); | 521 ApplyCompositionAndSelectionStyles(); |
| 520 | 522 |
| 521 // Build the list of runs from the script items and ranged colors/styles. | 523 // Build the list of runs from the script items and ranged colors/styles. |
| 522 // TODO(msw): Only break for bold/italic, not color etc. See TextRun comment. | 524 // TODO(msw): Only break for bold/italic, not color etc. See TextRun comment. |
| 523 internal::StyleIterator style(colors(), styles()); | 525 internal::StyleIterator style(colors(), styles()); |
| 524 SCRIPT_ITEM* script_item = &script_items[0]; | 526 SCRIPT_ITEM* script_item = &script_items[0]; |
| 525 const size_t layout_text_length = GetLayoutText().length(); | 527 const size_t max_run_length = kMaxGlyphs / 2; |
| 526 for (size_t run_break = 0; run_break < layout_text_length;) { | 528 for (size_t run_break = 0; run_break < layout_text_length;) { |
| 527 internal::TextRun* run = new internal::TextRun(); | 529 internal::TextRun* run = new internal::TextRun(); |
| 528 run->range.set_start(run_break); | 530 run->range.set_start(run_break); |
| 529 run->font = GetFont(); | 531 run->font = GetFont(); |
| 530 run->font_style = (style.style(BOLD) ? Font::BOLD : 0) | | 532 run->font_style = (style.style(BOLD) ? Font::BOLD : 0) | |
| 531 (style.style(ITALIC) ? Font::ITALIC : 0); | 533 (style.style(ITALIC) ? Font::ITALIC : 0); |
| 532 DeriveFontIfNecessary(run->font.GetFontSize(), run->font.GetHeight(), | 534 DeriveFontIfNecessary(run->font.GetFontSize(), run->font.GetHeight(), |
| 533 run->font_style, &run->font); | 535 run->font_style, &run->font); |
| 534 run->foreground = style.color(); | 536 run->foreground = style.color(); |
| 535 run->strike = style.style(STRIKE); | 537 run->strike = style.style(STRIKE); |
| 536 run->diagonal_strike = style.style(DIAGONAL_STRIKE); | 538 run->diagonal_strike = style.style(DIAGONAL_STRIKE); |
| 537 run->underline = style.style(UNDERLINE); | 539 run->underline = style.style(UNDERLINE); |
| 538 run->script_analysis = script_item->a; | 540 run->script_analysis = script_item->a; |
| 539 | 541 |
| 540 // Find the next break and advance the iterators as needed. | 542 // Find the next break and advance the iterators as needed. |
| 541 const size_t script_item_break = (script_item + 1)->iCharPos; | 543 const size_t script_item_break = (script_item + 1)->iCharPos; |
| 542 run_break = std::min(script_item_break, | 544 run_break = std::min(script_item_break, |
| 543 TextIndexToLayoutIndex(style.GetRange().end())); | 545 TextIndexToLayoutIndex(style.GetRange().end())); |
| 546 // Clamp run lengths to avoid exceeding the maximum supported glyph count. |
| 547 if ((run_break - run->range.start()) > max_run_length) |
| 548 run_break = run->range.start() + max_run_length; |
| 544 style.UpdatePosition(LayoutIndexToTextIndex(run_break)); | 549 style.UpdatePosition(LayoutIndexToTextIndex(run_break)); |
| 545 if (script_item_break == run_break) | 550 if (script_item_break == run_break) |
| 546 script_item++; | 551 script_item++; |
| 547 run->range.set_end(run_break); | 552 run->range.set_end(run_break); |
| 548 runs_.push_back(run); | 553 runs_.push_back(run); |
| 549 } | 554 } |
| 550 | 555 |
| 551 // Undo the temporarily applied composition underlines and selection colors. | 556 // Undo the temporarily applied composition underlines and selection colors. |
| 552 UndoCompositionAndSelectionStyles(); | 557 UndoCompositionAndSelectionStyles(); |
| 553 } | 558 } |
| (...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 733 DeriveFontIfNecessary(font_size, font_height, run->font_style, &run->font); | 738 DeriveFontIfNecessary(font_size, font_height, run->font_style, &run->font); |
| 734 ScriptFreeCache(&run->script_cache); | 739 ScriptFreeCache(&run->script_cache); |
| 735 } | 740 } |
| 736 | 741 |
| 737 // Select the font desired for glyph generation. | 742 // Select the font desired for glyph generation. |
| 738 SelectObject(cached_hdc_, run->font.GetNativeFont()); | 743 SelectObject(cached_hdc_, run->font.GetNativeFont()); |
| 739 | 744 |
| 740 HRESULT hr = E_OUTOFMEMORY; | 745 HRESULT hr = E_OUTOFMEMORY; |
| 741 const size_t run_length = run->range.length(); | 746 const size_t run_length = run->range.length(); |
| 742 const wchar_t* run_text = &(GetLayoutText()[run->range.start()]); | 747 const wchar_t* run_text = &(GetLayoutText()[run->range.start()]); |
| 743 // Max glyph guess: http://msdn.microsoft.com/en-us/library/dd368564.aspx | 748 // Guess the expected number of glyphs from the length of the run. |
| 749 // MSDN suggests this at http://msdn.microsoft.com/en-us/library/dd368564.aspx |
| 744 size_t max_glyphs = static_cast<size_t>(1.5 * run_length + 16); | 750 size_t max_glyphs = static_cast<size_t>(1.5 * run_length + 16); |
| 745 while (hr == E_OUTOFMEMORY && max_glyphs < kMaxGlyphs) { | 751 while (hr == E_OUTOFMEMORY && max_glyphs <= kMaxGlyphs) { |
| 746 run->glyph_count = 0; | 752 run->glyph_count = 0; |
| 747 run->glyphs.reset(new WORD[max_glyphs]); | 753 run->glyphs.reset(new WORD[max_glyphs]); |
| 748 run->visible_attributes.reset(new SCRIPT_VISATTR[max_glyphs]); | 754 run->visible_attributes.reset(new SCRIPT_VISATTR[max_glyphs]); |
| 749 hr = ScriptShape(cached_hdc_, | 755 hr = ScriptShape(cached_hdc_, &run->script_cache, run_text, run_length, |
| 750 &run->script_cache, | 756 max_glyphs, &run->script_analysis, run->glyphs.get(), |
| 751 run_text, | 757 run->logical_clusters.get(), run->visible_attributes.get(), |
| 752 run_length, | |
| 753 max_glyphs, | |
| 754 &run->script_analysis, | |
| 755 run->glyphs.get(), | |
| 756 run->logical_clusters.get(), | |
| 757 run->visible_attributes.get(), | |
| 758 &run->glyph_count); | 758 &run->glyph_count); |
| 759 max_glyphs *= 2; | 759 // Ensure that |kMaxGlyphs| is attempted and the loop terminates afterward. |
| 760 max_glyphs = std::max(max_glyphs + 1, std::min(max_glyphs * 2, kMaxGlyphs)); |
| 760 } | 761 } |
| 761 return hr; | 762 return hr; |
| 762 } | 763 } |
| 763 | 764 |
| 764 int RenderTextWin::CountCharsWithMissingGlyphs(internal::TextRun* run) const { | 765 int RenderTextWin::CountCharsWithMissingGlyphs(internal::TextRun* run) const { |
| 765 int chars_not_missing_glyphs = 0; | 766 int chars_not_missing_glyphs = 0; |
| 766 SCRIPT_FONTPROPERTIES properties; | 767 SCRIPT_FONTPROPERTIES properties; |
| 767 memset(&properties, 0, sizeof(properties)); | 768 memset(&properties, 0, sizeof(properties)); |
| 768 properties.cBytes = sizeof(properties); | 769 properties.cBytes = sizeof(properties); |
| 769 ScriptGetFontProperties(cached_hdc_, &run->script_cache, &properties); | 770 ScriptGetFontProperties(cached_hdc_, &run->script_cache, &properties); |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 828 size_t position = LayoutIndexToTextIndex(run->range.end()); | 829 size_t position = LayoutIndexToTextIndex(run->range.end()); |
| 829 position = IndexOfAdjacentGrapheme(position, CURSOR_BACKWARD); | 830 position = IndexOfAdjacentGrapheme(position, CURSOR_BACKWARD); |
| 830 return SelectionModel(position, CURSOR_FORWARD); | 831 return SelectionModel(position, CURSOR_FORWARD); |
| 831 } | 832 } |
| 832 | 833 |
| 833 RenderText* RenderText::CreateInstance() { | 834 RenderText* RenderText::CreateInstance() { |
| 834 return new RenderTextWin; | 835 return new RenderTextWin; |
| 835 } | 836 } |
| 836 | 837 |
| 837 } // namespace gfx | 838 } // namespace gfx |
| OLD | NEW |