Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(557)

Side by Side Diff: ui/gfx/render_text_harfbuzz.cc

Issue 924773002: Fix multiline behaviors for RTL text. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: fix Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 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_harfbuzz.h" 5 #include "ui/gfx/render_text_harfbuzz.h"
6 6
7 #include <limits> 7 #include <limits>
8 8
9 #include "base/i18n/bidi_line_iterator.h" 9 #include "base/i18n/bidi_line_iterator.h"
10 #include "base/i18n/break_iterator.h" 10 #include "base/i18n/break_iterator.h"
(...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after
216 // TODO(ckocagil): Expose the interface of this class in the header and test 216 // TODO(ckocagil): Expose the interface of this class in the header and test
217 // this class directly. 217 // this class directly.
218 class HarfBuzzLineBreaker { 218 class HarfBuzzLineBreaker {
219 public: 219 public:
220 HarfBuzzLineBreaker(size_t max_width, 220 HarfBuzzLineBreaker(size_t max_width,
221 int min_baseline, 221 int min_baseline,
222 float min_height, 222 float min_height,
223 bool multiline, 223 bool multiline,
224 const base::string16& text, 224 const base::string16& text,
225 const BreakList<size_t>* words, 225 const BreakList<size_t>* words,
226 const ScopedVector<internal::TextRunHarfBuzz>& runs) 226 const internal::TextRunList& run_list)
227 : max_width_((max_width == 0) ? SK_ScalarMax : SkIntToScalar(max_width)), 227 : max_width_((max_width == 0) ? SK_ScalarMax : SkIntToScalar(max_width)),
228 min_baseline_(min_baseline), 228 min_baseline_(min_baseline),
229 min_height_(min_height), 229 min_height_(min_height),
230 multiline_(multiline), 230 multiline_(multiline),
231 text_(text), 231 text_(text),
232 words_(words), 232 words_(words),
233 runs_(runs), 233 run_list_(run_list),
234 text_x_(0), 234 text_x_(0),
235 line_x_(0), 235 line_x_(0),
236 max_descent_(0), 236 max_descent_(0),
237 max_ascent_(0) { 237 max_ascent_(0) {
238 DCHECK_EQ(multiline_, (words_ != nullptr)); 238 DCHECK_EQ(multiline_, (words_ != nullptr));
239 AdvanceLine(); 239 AdvanceLine();
240 } 240 }
241 241
242 // Breaks the run at given |run_index| into Line structs. 242 // Breaks the run at given |run_index| into Line structs.
243 void AddRun(int run_index) { 243 void AddRun(int run_index) {
244 const internal::TextRunHarfBuzz* run = runs_[run_index]; 244 const internal::TextRunHarfBuzz* run = run_list_.runs()[run_index];
245 base::char16 first_char = text_[run->range.start()]; 245 base::char16 first_char = text_[run->range.start()];
246 if (multiline_ && first_char == '\n') { 246 if (multiline_ && first_char == '\n') {
247 AdvanceLine(); 247 AdvanceLine();
248 } else if (multiline_ && (line_x_ + SkFloatToScalar(run->width)) > 248 } else if (multiline_ && (line_x_ + SkFloatToScalar(run->width)) >
249 max_width_) { 249 max_width_) {
250 BreakRun(run_index); 250 BreakRun(run_index);
251 } else { 251 } else {
252 AddSegment(run_index, run->range, run->width); 252 AddSegment(run_index, run->range, run->width);
253 } 253 }
254 } 254 }
(...skipping 13 matching lines...) Expand all
268 typedef std::pair<size_t, size_t> SegmentHandle; 268 typedef std::pair<size_t, size_t> SegmentHandle;
269 269
270 internal::LineSegment* SegmentFromHandle(const SegmentHandle& handle) { 270 internal::LineSegment* SegmentFromHandle(const SegmentHandle& handle) {
271 return &lines_[handle.first].segments[handle.second]; 271 return &lines_[handle.first].segments[handle.second];
272 } 272 }
273 273
274 // Breaks a run into segments that fit in the last line in |lines_| and adds 274 // Breaks a run into segments that fit in the last line in |lines_| and adds
275 // them. Adds a new Line to the back of |lines_| whenever a new segment can't 275 // them. Adds a new Line to the back of |lines_| whenever a new segment can't
276 // be added without the Line's width exceeding |max_width_|. 276 // be added without the Line's width exceeding |max_width_|.
277 void BreakRun(int run_index) { 277 void BreakRun(int run_index) {
278 const internal::TextRunHarfBuzz& run = *runs_[run_index]; 278 const internal::TextRunHarfBuzz& run = *(run_list_.runs()[run_index]);
279 SkScalar width = 0; 279 SkScalar width = 0;
280 size_t next_char = run.range.start(); 280 size_t next_char = run.range.start();
281 281
282 // Break the run until it fits the current line. 282 // Break the run until it fits the current line.
283 while (next_char < run.range.end()) { 283 while (next_char < run.range.end()) {
284 const size_t current_char = next_char; 284 const size_t current_char = next_char;
285 const bool skip_line = 285 const bool skip_line =
286 BreakRunAtWidth(run, current_char, &width, &next_char); 286 BreakRunAtWidth(run, current_char, &width, &next_char);
287 AddSegment(run_index, Range(current_char, next_char), 287 AddSegment(run_index, Range(current_char, next_char),
288 SkScalarToFloat(width)); 288 SkScalarToFloat(width));
(...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after
368 x += segment_width; 368 x += segment_width;
369 } 369 }
370 rtl_segments_.clear(); 370 rtl_segments_.clear();
371 } 371 }
372 372
373 // Finishes the size calculations of the last Line in |lines_|. Adds a new 373 // Finishes the size calculations of the last Line in |lines_|. Adds a new
374 // Line to the back of |lines_|. 374 // Line to the back of |lines_|.
375 void AdvanceLine() { 375 void AdvanceLine() {
376 if (!lines_.empty()) { 376 if (!lines_.empty()) {
377 internal::Line* line = &lines_.back(); 377 internal::Line* line = &lines_.back();
378 std::sort(line->segments.begin(), line->segments.end(),
379 [this](const internal::LineSegment& s1,
380 const internal::LineSegment& s2) -> bool {
381 return run_list_.logical_to_visual(s1.run) <
382 run_list_.logical_to_visual(s2.run);
383 });
378 line->size.set_height(std::max(min_height_, max_descent_ + max_ascent_)); 384 line->size.set_height(std::max(min_height_, max_descent_ + max_ascent_));
379 line->baseline = 385 line->baseline =
380 std::max(min_baseline_, SkScalarRoundToInt(max_ascent_)); 386 std::max(min_baseline_, SkScalarRoundToInt(max_ascent_));
381 line->preceding_heights = std::ceil(total_size_.height()); 387 line->preceding_heights = std::ceil(total_size_.height());
382 total_size_.set_height(total_size_.height() + line->size.height()); 388 total_size_.set_height(total_size_.height() + line->size.height());
383 total_size_.set_width(std::max(total_size_.width(), line->size.width())); 389 total_size_.set_width(std::max(total_size_.width(), line->size.width()));
384 } 390 }
385 max_descent_ = 0; 391 max_descent_ = 0;
386 max_ascent_ = 0; 392 max_ascent_ = 0;
387 line_x_ = 0; 393 line_x_ = 0;
388 lines_.push_back(internal::Line()); 394 lines_.push_back(internal::Line());
389 } 395 }
390 396
391 // Adds a new segment with the given properties to |lines_.back()|. 397 // Adds a new segment with the given properties to |lines_.back()|.
392 void AddSegment(int run_index, Range char_range, float width) { 398 void AddSegment(int run_index, Range char_range, float width) {
393 if (char_range.is_empty()) { 399 if (char_range.is_empty()) {
394 DCHECK_EQ(0, width); 400 DCHECK_EQ(0, width);
395 return; 401 return;
396 } 402 }
397 const internal::TextRunHarfBuzz& run = *runs_[run_index]; 403 const internal::TextRunHarfBuzz& run = *(run_list_.runs()[run_index]);
398 404
399 internal::LineSegment segment; 405 internal::LineSegment segment;
400 segment.run = run_index; 406 segment.run = run_index;
401 segment.char_range = char_range; 407 segment.char_range = char_range;
402 segment.x_range = Range( 408 segment.x_range = Range(
403 SkScalarCeilToInt(text_x_), 409 SkScalarCeilToInt(text_x_),
404 SkScalarCeilToInt(text_x_ + SkFloatToScalar(width))); 410 SkScalarCeilToInt(text_x_ + SkFloatToScalar(width)));
405 segment.width = width; 411 segment.width = width;
406 412
407 internal::Line* line = &lines_.back(); 413 internal::Line* line = &lines_.back();
(...skipping 22 matching lines...) Expand all
430 text_x_ += SkFloatToScalar(width); 436 text_x_ += SkFloatToScalar(width);
431 line_x_ += SkFloatToScalar(width); 437 line_x_ += SkFloatToScalar(width);
432 } 438 }
433 439
434 const SkScalar max_width_; 440 const SkScalar max_width_;
435 const int min_baseline_; 441 const int min_baseline_;
436 const float min_height_; 442 const float min_height_;
437 const bool multiline_; 443 const bool multiline_;
438 const base::string16& text_; 444 const base::string16& text_;
439 const BreakList<size_t>* const words_; 445 const BreakList<size_t>* const words_;
440 const ScopedVector<internal::TextRunHarfBuzz>& runs_; 446 const internal::TextRunList& run_list_;
441 447
442 // Stores the resulting lines. 448 // Stores the resulting lines.
443 std::vector<internal::Line> lines_; 449 std::vector<internal::Line> lines_;
444 450
445 // Text space and line space x coordinates of the next segment to be added. 451 // Text space and line space x coordinates of the next segment to be added.
446 SkScalar text_x_; 452 SkScalar text_x_;
447 SkScalar line_x_; 453 SkScalar line_x_;
448 454
449 float max_descent_; 455 float max_descent_;
450 float max_ascent_; 456 float max_ascent_;
(...skipping 512 matching lines...) Expand 10 before | Expand all | Expand 10 after
963 if (lines().empty()) { 969 if (lines().empty()) {
964 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed. 970 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
965 tracked_objects::ScopedTracker tracking_profile2( 971 tracked_objects::ScopedTracker tracking_profile2(
966 FROM_HERE_WITH_EXPLICIT_FUNCTION( 972 FROM_HERE_WITH_EXPLICIT_FUNCTION(
967 "431326 RenderTextHarfBuzz::EnsureLayout2")); 973 "431326 RenderTextHarfBuzz::EnsureLayout2"));
968 974
969 internal::TextRunList* run_list = GetRunList(); 975 internal::TextRunList* run_list = GetRunList();
970 HarfBuzzLineBreaker line_breaker( 976 HarfBuzzLineBreaker line_breaker(
971 display_rect().width(), font_list().GetBaseline(), 977 display_rect().width(), font_list().GetBaseline(),
972 std::max(font_list().GetHeight(), min_line_height()), multiline(), 978 std::max(font_list().GetHeight(), min_line_height()), multiline(),
973 GetDisplayText(), multiline() ? &GetLineBreaks() : nullptr, 979 GetDisplayText(), multiline() ? &GetLineBreaks() : nullptr, *run_list);
974 run_list->runs());
975 980
976 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed. 981 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
977 tracked_objects::ScopedTracker tracking_profile3( 982 tracked_objects::ScopedTracker tracking_profile3(
978 FROM_HERE_WITH_EXPLICIT_FUNCTION( 983 FROM_HERE_WITH_EXPLICIT_FUNCTION(
979 "431326 RenderTextHarfBuzz::EnsureLayout3")); 984 "431326 RenderTextHarfBuzz::EnsureLayout3"));
980 985
981 for (size_t i = 0; i < run_list->size(); ++i) 986 for (size_t i = 0; i < run_list->size(); ++i)
982 line_breaker.AddRun(run_list->visual_to_logical(i)); 987 line_breaker.AddRun(i);
983
984 std::vector<internal::Line> lines; 988 std::vector<internal::Line> lines;
985 line_breaker.Finalize(&lines, &total_size_); 989 line_breaker.Finalize(&lines, &total_size_);
986 set_lines(&lines); 990 set_lines(&lines);
987 } 991 }
988 } 992 }
989 993
990 void RenderTextHarfBuzz::DrawVisualText(Canvas* canvas) { 994 void RenderTextHarfBuzz::DrawVisualText(Canvas* canvas) {
995 internal::SkiaTextRenderer renderer(canvas);
996 DrawVisualTextInternal(&renderer);
997 }
998
999 void RenderTextHarfBuzz::DrawVisualTextInternal(
1000 internal::SkiaTextRenderer* renderer) {
991 DCHECK(!update_layout_run_list_); 1001 DCHECK(!update_layout_run_list_);
992 DCHECK(!update_display_run_list_); 1002 DCHECK(!update_display_run_list_);
993 DCHECK(!update_display_text_); 1003 DCHECK(!update_display_text_);
994 if (lines().empty()) 1004 if (lines().empty())
995 return; 1005 return;
996 1006
997 internal::SkiaTextRenderer renderer(canvas); 1007 ApplyFadeEffects(renderer);
998 ApplyFadeEffects(&renderer); 1008 ApplyTextShadows(renderer);
999 ApplyTextShadows(&renderer);
1000 ApplyCompositionAndSelectionStyles(); 1009 ApplyCompositionAndSelectionStyles();
1001 1010
1002 internal::TextRunList* run_list = GetRunList(); 1011 internal::TextRunList* run_list = GetRunList();
1003 for (size_t i = 0; i < lines().size(); ++i) { 1012 for (size_t i = 0; i < lines().size(); ++i) {
1004 const internal::Line& line = lines()[i]; 1013 const internal::Line& line = lines()[i];
1005 const Vector2d origin = GetLineOffset(i) + Vector2d(0, line.baseline); 1014 const Vector2d origin = GetLineOffset(i) + Vector2d(0, line.baseline);
1006 SkScalar preceding_segment_widths = 0; 1015 SkScalar preceding_segment_widths = 0;
1007 for (const internal::LineSegment& segment : line.segments) { 1016 for (const internal::LineSegment& segment : line.segments) {
1008 const internal::TextRunHarfBuzz& run = *run_list->runs()[segment.run]; 1017 const internal::TextRunHarfBuzz& run = *run_list->runs()[segment.run];
1009 renderer.SetTypeface(run.skia_face.get()); 1018 renderer->SetTypeface(run.skia_face.get());
1010 renderer.SetTextSize(SkIntToScalar(run.font_size)); 1019 renderer->SetTextSize(SkIntToScalar(run.font_size));
1011 renderer.SetFontRenderParams(run.render_params, 1020 renderer->SetFontRenderParams(run.render_params,
1012 background_is_transparent()); 1021 background_is_transparent());
1013 Range glyphs_range = run.CharRangeToGlyphRange(segment.char_range); 1022 Range glyphs_range = run.CharRangeToGlyphRange(segment.char_range);
1014 scoped_ptr<SkPoint[]> positions(new SkPoint[glyphs_range.length()]); 1023 scoped_ptr<SkPoint[]> positions(new SkPoint[glyphs_range.length()]);
1015 SkScalar offset_x = 1024 SkScalar offset_x =
1016 preceding_segment_widths - run.positions[glyphs_range.start()].x(); 1025 preceding_segment_widths - run.positions[glyphs_range.start()].x();
1017 for (size_t j = 0; j < glyphs_range.length(); ++j) { 1026 for (size_t j = 0; j < glyphs_range.length(); ++j) {
1018 positions[j] = run.positions[(glyphs_range.is_reversed()) ? 1027 positions[j] = run.positions[(glyphs_range.is_reversed()) ?
1019 (glyphs_range.start() - j) : 1028 (glyphs_range.start() - j) :
1020 (glyphs_range.start() + j)]; 1029 (glyphs_range.start() + j)];
1021 positions[j].offset(SkIntToScalar(origin.x()) + offset_x, 1030 positions[j].offset(SkIntToScalar(origin.x()) + offset_x,
1022 SkIntToScalar(origin.y())); 1031 SkIntToScalar(origin.y()));
1023 } 1032 }
1024 for (BreakList<SkColor>::const_iterator it = 1033 for (BreakList<SkColor>::const_iterator it =
1025 colors().GetBreak(segment.char_range.start()); 1034 colors().GetBreak(segment.char_range.start());
1026 it != colors().breaks().end() && 1035 it != colors().breaks().end() &&
1027 it->first < segment.char_range.end(); 1036 it->first < segment.char_range.end();
1028 ++it) { 1037 ++it) {
1029 const Range intersection = 1038 const Range intersection =
1030 colors().GetRange(it).Intersect(segment.char_range); 1039 colors().GetRange(it).Intersect(segment.char_range);
1031 const Range colored_glyphs = run.CharRangeToGlyphRange(intersection); 1040 const Range colored_glyphs = run.CharRangeToGlyphRange(intersection);
1032 // The range may be empty if a portion of a multi-character grapheme is 1041 // The range may be empty if a portion of a multi-character grapheme is
1033 // selected, yielding two colors for a single glyph. For now, this just 1042 // selected, yielding two colors for a single glyph. For now, this just
1034 // paints the glyph with a single style, but it should paint it twice, 1043 // paints the glyph with a single style, but it should paint it twice,
1035 // clipped according to selection bounds. See http://crbug.com/366786 1044 // clipped according to selection bounds. See http://crbug.com/366786
1036 if (colored_glyphs.is_empty()) 1045 if (colored_glyphs.is_empty())
1037 continue; 1046 continue;
1038 1047
1039 renderer.SetForegroundColor(it->second); 1048 renderer->SetForegroundColor(it->second);
1040 renderer.DrawPosText( 1049 renderer->DrawPosText(
1041 &positions[colored_glyphs.start() - glyphs_range.start()], 1050 &positions[colored_glyphs.start() - glyphs_range.start()],
1042 &run.glyphs[colored_glyphs.start()], colored_glyphs.length()); 1051 &run.glyphs[colored_glyphs.start()], colored_glyphs.length());
1043 int start_x = SkScalarRoundToInt( 1052 int start_x = SkScalarRoundToInt(
1044 positions[colored_glyphs.start() - glyphs_range.start()].x()); 1053 positions[colored_glyphs.start() - glyphs_range.start()].x());
1045 int end_x = SkScalarRoundToInt( 1054 int end_x = SkScalarRoundToInt(
1046 (colored_glyphs.end() == glyphs_range.end()) 1055 (colored_glyphs.end() == glyphs_range.end())
1047 ? (SkFloatToScalar(segment.width) + preceding_segment_widths + 1056 ? (SkFloatToScalar(segment.width) + preceding_segment_widths +
1048 SkIntToScalar(origin.x())) 1057 SkIntToScalar(origin.x()))
1049 : positions[colored_glyphs.end() - glyphs_range.start()].x()); 1058 : positions[colored_glyphs.end() - glyphs_range.start()].x());
1050 renderer.DrawDecorations(start_x, origin.y(), end_x - start_x, 1059 renderer->DrawDecorations(start_x, origin.y(), end_x - start_x,
1051 run.underline, run.strike, 1060 run.underline, run.strike,
1052 run.diagonal_strike); 1061 run.diagonal_strike);
1053 } 1062 }
1054 preceding_segment_widths += SkFloatToScalar(segment.width); 1063 preceding_segment_widths += SkFloatToScalar(segment.width);
1055 } 1064 }
1056 } 1065 }
1057 1066
1058 renderer.EndDiagonalStrike(); 1067 renderer->EndDiagonalStrike();
1059 1068
1060 UndoCompositionAndSelectionStyles(); 1069 UndoCompositionAndSelectionStyles();
1061 } 1070 }
1062 1071
1063 size_t RenderTextHarfBuzz::GetRunContainingCaret( 1072 size_t RenderTextHarfBuzz::GetRunContainingCaret(
1064 const SelectionModel& caret) { 1073 const SelectionModel& caret) {
1065 DCHECK(!update_display_run_list_); 1074 DCHECK(!update_display_run_list_);
1066 size_t layout_position = TextIndexToDisplayIndex(caret.caret_pos()); 1075 size_t layout_position = TextIndexToDisplayIndex(caret.caret_pos());
1067 LogicalCursorDirection affinity = caret.caret_affinity(); 1076 LogicalCursorDirection affinity = caret.caret_affinity();
1068 internal::TextRunList* run_list = GetRunList(); 1077 internal::TextRunList* run_list = GetRunList();
(...skipping 301 matching lines...) Expand 10 before | Expand all | Expand 10 after
1370 "431326 RenderTextHarfBuzz::ShapeRunWithFont3")); 1379 "431326 RenderTextHarfBuzz::ShapeRunWithFont3"));
1371 1380
1372 for (size_t i = 0; i < run->glyph_count; ++i) { 1381 for (size_t i = 0; i < run->glyph_count; ++i) {
1373 DCHECK_LE(infos[i].codepoint, std::numeric_limits<uint16>::max()); 1382 DCHECK_LE(infos[i].codepoint, std::numeric_limits<uint16>::max());
1374 run->glyphs[i] = static_cast<uint16>(infos[i].codepoint); 1383 run->glyphs[i] = static_cast<uint16>(infos[i].codepoint);
1375 run->glyph_to_char[i] = infos[i].cluster; 1384 run->glyph_to_char[i] = infos[i].cluster;
1376 const SkScalar x_offset = SkFixedToScalar(hb_positions[i].x_offset); 1385 const SkScalar x_offset = SkFixedToScalar(hb_positions[i].x_offset);
1377 const SkScalar y_offset = SkFixedToScalar(hb_positions[i].y_offset); 1386 const SkScalar y_offset = SkFixedToScalar(hb_positions[i].y_offset);
1378 run->positions[i].set(run->width + x_offset, -y_offset); 1387 run->positions[i].set(run->width + x_offset, -y_offset);
1379 run->width += (glyph_width_for_test_ > 0) 1388 run->width += (glyph_width_for_test_ > 0)
1380 ? SkIntToScalar(glyph_width_for_test_) 1389 ? glyph_width_for_test_
1381 : SkFixedToScalar(hb_positions[i].x_advance); 1390 : SkFixedToFloat(hb_positions[i].x_advance);
1382 // Round run widths if subpixel positioning is off to match native behavior. 1391 // Round run widths if subpixel positioning is off to match native behavior.
1383 if (!run->render_params.subpixel_positioning) 1392 if (!run->render_params.subpixel_positioning)
1384 run->width = std::floor(run->width + 0.5f); 1393 run->width = std::floor(run->width + 0.5f);
1385 } 1394 }
1386 1395
1387 hb_buffer_destroy(buffer); 1396 hb_buffer_destroy(buffer);
1388 hb_font_destroy(harfbuzz_font); 1397 hb_font_destroy(harfbuzz_font);
1389 return true; 1398 return true;
1390 } 1399 }
1391 1400
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
1457 DCHECK(!update_layout_run_list_); 1466 DCHECK(!update_layout_run_list_);
1458 DCHECK(!update_display_run_list_); 1467 DCHECK(!update_display_run_list_);
1459 return text_elided() ? display_run_list_.get() : &layout_run_list_; 1468 return text_elided() ? display_run_list_.get() : &layout_run_list_;
1460 } 1469 }
1461 1470
1462 const internal::TextRunList* RenderTextHarfBuzz::GetRunList() const { 1471 const internal::TextRunList* RenderTextHarfBuzz::GetRunList() const {
1463 return const_cast<RenderTextHarfBuzz*>(this)->GetRunList(); 1472 return const_cast<RenderTextHarfBuzz*>(this)->GetRunList();
1464 } 1473 }
1465 1474
1466 } // namespace gfx 1475 } // namespace gfx
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698