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

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

Issue 7458014: Implement Uniscribe RenderText for Windows. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix RenderText::RightEndSelectionModel. Created 9 years, 4 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 | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 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 8
9 #include "base/i18n/break_iterator.h" 9 #include "base/i18n/break_iterator.h"
10 #include "base/logging.h" 10 #include "base/logging.h"
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after
157 } 157 }
158 } 158 }
159 style_ranges_.back().range.set_end(text_.length()); 159 style_ranges_.back().range.set_end(text_.length());
160 } 160 }
161 #ifndef NDEBUG 161 #ifndef NDEBUG
162 CheckStyleRanges(style_ranges_, text_.length()); 162 CheckStyleRanges(style_ranges_, text_.length());
163 #endif 163 #endif
164 cached_bounds_and_offset_valid_ = false; 164 cached_bounds_and_offset_valid_ = false;
165 } 165 }
166 166
167 void RenderText::SetSelectionModel(const SelectionModel& sel) { 167 void RenderText::ToggleInsertMode() {
168 size_t start = sel.selection_start(); 168 insert_mode_ = !insert_mode_;
169 size_t end = sel.selection_end();
170 selection_model_.set_selection_start(std::min(start, text().length()));
171 selection_model_.set_selection_end(std::min(end, text().length()));
172 selection_model_.set_caret_pos(std::min(sel.caret_pos(), text().length()));
173 selection_model_.set_caret_placement(sel.caret_placement());
174
175 cached_bounds_and_offset_valid_ = false; 169 cached_bounds_and_offset_valid_ = false;
176 } 170 }
177 171
178 void RenderText::SetDisplayRect(const Rect& r) { 172 void RenderText::SetDisplayRect(const Rect& r) {
179 display_rect_ = r; 173 display_rect_ = r;
180 cached_bounds_and_offset_valid_ = false; 174 cached_bounds_and_offset_valid_ = false;
181 } 175 }
182 176
183 size_t RenderText::GetCursorPosition() const { 177 size_t RenderText::GetCursorPosition() const {
184 return selection_model_.selection_end(); 178 return selection_model_.selection_end();
185 } 179 }
186 180
187 void RenderText::SetCursorPosition(const size_t position) { 181 void RenderText::SetCursorPosition(size_t position) {
188 SelectionModel sel(selection_model()); 182 MoveCursorTo(position, false);
189 sel.set_selection_start(position);
190 sel.set_selection_end(position);
191 sel.set_caret_pos(GetIndexOfPreviousGrapheme(position));
192 sel.set_caret_placement(SelectionModel::TRAILING);
193 SetSelectionModel(sel);
194 } 183 }
195 184
196 void RenderText::MoveCursorLeft(BreakType break_type, bool select) { 185 void RenderText::MoveCursorLeft(BreakType break_type, bool select) {
197 SelectionModel position(selection_model()); 186 SelectionModel position(selection_model());
198 position.set_selection_start(GetCursorPosition()); 187 position.set_selection_start(GetCursorPosition());
199 // Cancelling a selection moves to the edge of the selection. 188 // Cancelling a selection moves to the edge of the selection.
200 if (break_type != LINE_BREAK && !EmptySelection() && !select) { 189 if (break_type != LINE_BREAK && !EmptySelection() && !select) {
201 // Use the selection start if it is left of the selection end. 190 // Use the selection start if it is left of the selection end.
202 SelectionModel selection_start(GetSelectionStart(), GetSelectionStart(), 191 SelectionModel selection_start(GetSelectionStart(), GetSelectionStart(),
203 SelectionModel::LEADING); 192 SelectionModel::LEADING);
(...skipping 26 matching lines...) Expand all
230 if (break_type == WORD_BREAK) 219 if (break_type == WORD_BREAK)
231 position = GetRightSelectionModel(position, break_type); 220 position = GetRightSelectionModel(position, break_type);
232 } else { 221 } else {
233 position = GetRightSelectionModel(position, break_type); 222 position = GetRightSelectionModel(position, break_type);
234 } 223 }
235 if (select) 224 if (select)
236 position.set_selection_start(GetSelectionStart()); 225 position.set_selection_start(GetSelectionStart());
237 MoveCursorTo(position); 226 MoveCursorTo(position);
238 } 227 }
239 228
240 bool RenderText::MoveCursorTo(const SelectionModel& selection) { 229 bool RenderText::MoveCursorTo(const SelectionModel& selection_model) {
241 bool changed = !selection.Equals(selection_model_); 230 SelectionModel sel(selection_model);
242 SetSelectionModel(selection); 231 size_t text_length = text().length();
232 // Enforce valid selection model components.
233 if (sel.selection_start() > text_length)
234 sel.set_selection_start(text_length);
235 if (sel.selection_end() > text_length)
236 sel.set_selection_end(text_length);
237 // The current model only supports caret positions at valid character indices.
238 if (text_length == 0) {
239 sel.set_caret_pos(0);
240 sel.set_caret_placement(SelectionModel::LEADING);
241 } else if (sel.caret_pos() >= text_length) {
242 SelectionModel end = base::i18n::IsRTL() ? LeftEndSelectionModel() :
xji 2011/08/25 05:58:38 change base::i18n::IsRTL() to GetTextDirection(
msw 2011/08/26 16:26:25 Done.
243 RightEndSelectionModel();
244 sel.set_caret_pos(end.caret_pos());
245 sel.set_caret_placement(end.caret_placement());
246 }
247 bool changed = !sel.Equals(selection_model_);
248 SetSelectionModel(sel);
243 return changed; 249 return changed;
244 } 250 }
245 251
246 bool RenderText::MoveCursorTo(const Point& point, bool select) { 252 bool RenderText::MoveCursorTo(const Point& point, bool select) {
247 SelectionModel selection = FindCursorPosition(point); 253 SelectionModel selection = FindCursorPosition(point);
248 if (select) 254 if (select)
249 selection.set_selection_start(GetSelectionStart()); 255 selection.set_selection_start(GetSelectionStart());
250 return MoveCursorTo(selection); 256 return MoveCursorTo(selection);
251 } 257 }
252 258
253 bool RenderText::IsPointInSelection(const Point& point) { 259 bool RenderText::IsPointInSelection(const Point& point) {
260 if (EmptySelection())
261 return false;
254 // TODO(xji): should this check whether the point is inside the visual 262 // TODO(xji): should this check whether the point is inside the visual
255 // selection bounds? In case of "abcFED", if "ED" is selected, |point| points 263 // selection bounds? In case of "abcFED", if "ED" is selected, |point| points
256 // to the right half of 'c', is the point in selection? 264 // to the right half of 'c', is the point in selection?
257 size_t pos = FindCursorPosition(point).selection_end(); 265 size_t pos = FindCursorPosition(point).selection_end();
258 return (pos >= MinOfSelection() && pos < MaxOfSelection()); 266 return (pos >= MinOfSelection() && pos < MaxOfSelection());
259 } 267 }
260 268
261 void RenderText::ClearSelection() { 269 void RenderText::ClearSelection() {
262 SelectionModel sel(selection_model()); 270 SelectionModel sel(selection_model());
263 sel.set_selection_start(GetCursorPosition()); 271 sel.set_selection_start(GetCursorPosition());
264 SetSelectionModel(sel); 272 SetSelectionModel(sel);
265 } 273 }
266 274
267 void RenderText::SelectAll() { 275 void RenderText::SelectAll() {
268 SelectionModel sel(0, text().length(), 276 SelectionModel sel(RightEndSelectionModel());
269 text().length(), SelectionModel::LEADING); 277 sel.set_selection_start(LeftEndSelectionModel().selection_start());
270 SetSelectionModel(sel); 278 SetSelectionModel(sel);
271 } 279 }
272 280
273 // TODO(xji): it does not work for languages do not use space as word breaker, 281 // TODO(xji): it does not work for languages do not use space as word breaker,
274 // such as Chinese. Should use BreakIterator. 282 // such as Chinese. Should use BreakIterator.
275 void RenderText::SelectWord() { 283 void RenderText::SelectWord() {
276 size_t selection_start = GetSelectionStart(); 284 size_t selection_start = GetSelectionStart();
277 size_t cursor_position = GetCursorPosition(); 285 size_t cursor_position = GetCursorPosition();
278 // First we setup selection_start_ and selection_end_. There are so many cases 286 // First we setup selection_start_ and selection_end_. There are so many cases
279 // because we try to emulate what select-word looks like in a gtk textfield. 287 // because we try to emulate what select-word looks like in a gtk textfield.
(...skipping 21 matching lines...) Expand all
301 } 309 }
302 310
303 // Now we move selection_end_ to end of selection. Selection boundary 311 // Now we move selection_end_ to end of selection. Selection boundary
304 // is defined as the position where we have alpha-num character on one side 312 // is defined as the position where we have alpha-num character on one side
305 // and non-alpha-num char on the other side. 313 // and non-alpha-num char on the other side.
306 for (; cursor_position < text().length(); cursor_position++) { 314 for (; cursor_position < text().length(); cursor_position++) {
307 if (IsPositionAtWordSelectionBoundary(cursor_position)) 315 if (IsPositionAtWordSelectionBoundary(cursor_position))
308 break; 316 break;
309 } 317 }
310 318
311 SelectionModel sel(selection_model()); 319 MoveCursorTo(selection_start, false);
312 sel.set_selection_start(selection_start); 320 MoveCursorTo(cursor_position, true);
313 sel.set_selection_end(cursor_position);
314 sel.set_caret_pos(GetIndexOfPreviousGrapheme(cursor_position));
315 sel.set_caret_placement(SelectionModel::TRAILING);
316 SetSelectionModel(sel);
317 } 321 }
318 322
319 const ui::Range& RenderText::GetCompositionRange() const { 323 const ui::Range& RenderText::GetCompositionRange() const {
320 return composition_range_; 324 return composition_range_;
321 } 325 }
322 326
323 void RenderText::SetCompositionRange(const ui::Range& composition_range) { 327 void RenderText::SetCompositionRange(const ui::Range& composition_range) {
324 CHECK(!composition_range.IsValid() || 328 CHECK(!composition_range.IsValid() ||
325 ui::Range(0, text_.length()).Contains(composition_range)); 329 ui::Range(0, text_.length()).Contains(composition_range));
326 composition_range_.set_end(composition_range.end()); 330 composition_range_.set_end(composition_range.end());
(...skipping 16 matching lines...) Expand all
343 347
344 void RenderText::ApplyDefaultStyle() { 348 void RenderText::ApplyDefaultStyle() {
345 style_ranges_.clear(); 349 style_ranges_.clear();
346 StyleRange style = StyleRange(default_style_); 350 StyleRange style = StyleRange(default_style_);
347 style.range.set_end(text_.length()); 351 style.range.set_end(text_.length());
348 style_ranges_.push_back(style); 352 style_ranges_.push_back(style);
349 cached_bounds_and_offset_valid_ = false; 353 cached_bounds_and_offset_valid_ = false;
350 } 354 }
351 355
352 base::i18n::TextDirection RenderText::GetTextDirection() const { 356 base::i18n::TextDirection RenderText::GetTextDirection() const {
353 // TODO(msw): Bidi implementation, intended to replace the functionality added 357 if (base::i18n::IsRTL())
354 // in crrev.com/91881 (discussed in codereview.chromium.org/7324011). 358 return base::i18n::RIGHT_TO_LEFT;
355 return base::i18n::LEFT_TO_RIGHT; 359 return base::i18n::LEFT_TO_RIGHT;
356 } 360 }
357 361
358 int RenderText::GetStringWidth() { 362 int RenderText::GetStringWidth() {
359 return default_style_.font.GetStringWidth(text()); 363 return default_style_.font.GetStringWidth(text());
360 } 364 }
361 365
362 void RenderText::Draw(Canvas* canvas) { 366 void RenderText::Draw(Canvas* canvas) {
363 // Clip the canvas to the text display area. 367 // Clip the canvas to the text display area.
364 canvas->ClipRectInt(display_rect_.x(), display_rect_.y(), 368 canvas->ClipRectInt(display_rect_.x(), display_rect_.y(),
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
436 } else if (pivot == x) { 440 } else if (pivot == x) {
437 return SelectionModel(pivot_pos); 441 return SelectionModel(pivot_pos);
438 } else { 442 } else {
439 right = pivot; 443 right = pivot;
440 right_pos = pivot_pos; 444 right_pos = pivot_pos;
441 } 445 }
442 } 446 }
443 return SelectionModel(left_pos); 447 return SelectionModel(left_pos);
444 } 448 }
445 449
446 std::vector<Rect> RenderText::GetSubstringBounds(size_t from, size_t to) {
447 size_t start = std::min(from, to);
448 size_t end = std::max(from, to);
449 const Font& font = default_style_.font;
450 int start_x = font.GetStringWidth(text().substr(0, start));
451 int end_x = font.GetStringWidth(text().substr(0, end));
452 Rect rect(start_x, 0, end_x - start_x, font.GetHeight());
453 rect.Offset(display_rect_.origin());
454 rect.Offset(GetUpdatedDisplayOffset());
455 // Center the rect vertically in |display_rect_|.
456 rect.Offset(Point(0, (display_rect_.height() - rect.height()) / 2));
457 return std::vector<Rect>(1, rect);
458 }
459
460 Rect RenderText::GetCursorBounds(const SelectionModel& selection, 450 Rect RenderText::GetCursorBounds(const SelectionModel& selection,
461 bool insert_mode) { 451 bool insert_mode) {
462 size_t from = selection.selection_end(); 452 size_t from = selection.selection_end();
463 size_t to = insert_mode ? from : std::min(text_.length(), from + 1); 453 size_t to = insert_mode ? from : std::min(text_.length(), from + 1);
464 return GetSubstringBounds(from, to)[0]; 454 return GetSubstringBounds(from, to)[0];
465 } 455 }
466 456
467 const Rect& RenderText::GetUpdatedCursorBounds() { 457 const Rect& RenderText::GetUpdatedCursorBounds() {
468 UpdateCachedBoundsAndOffset(); 458 UpdateCachedBoundsAndOffset();
469 return cursor_bounds_; 459 return cursor_bounds_;
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after
542 if (!success) 532 if (!success)
543 return current; 533 return current;
544 while (iter.Advance()) { 534 while (iter.Advance()) {
545 pos = iter.pos(); 535 pos = iter.pos();
546 if (iter.IsWord() && pos > current.selection_end()) 536 if (iter.IsWord() && pos > current.selection_end())
547 break; 537 break;
548 } 538 }
549 return SelectionModel(pos, pos, SelectionModel::LEADING); 539 return SelectionModel(pos, pos, SelectionModel::LEADING);
550 } 540 }
551 541
542 SelectionModel RenderText::LeftEndSelectionModel() {
543 return SelectionModel(0, 0, SelectionModel::LEADING);
544 }
545
546 SelectionModel RenderText::RightEndSelectionModel() {
547 size_t cursor = text().length();
548 size_t caret_pos = GetIndexOfPreviousGrapheme(cursor);
549 SelectionModel::CaretPlacement placement = (caret_pos == cursor) ?
550 SelectionModel::LEADING : SelectionModel::TRAILING;
551 return SelectionModel(cursor, caret_pos, placement);
552 }
553
552 size_t RenderText::GetIndexOfPreviousGrapheme(size_t position) { 554 size_t RenderText::GetIndexOfPreviousGrapheme(size_t position) {
553 // TODO(msw): Handle complex script. 555 // TODO(msw): Handle complex script.
554 return std::max(static_cast<int>(position - 1), static_cast<int>(0)); 556 return std::max(static_cast<long>(position - 1), static_cast<long>(0));
557 }
558
559 std::vector<Rect> RenderText::GetSubstringBounds(size_t from, size_t to) {
560 size_t start = std::min(from, to);
561 size_t end = std::max(from, to);
562 const Font& font = default_style_.font;
563 int start_x = font.GetStringWidth(text().substr(0, start));
564 int end_x = font.GetStringWidth(text().substr(0, end));
565 Rect rect(start_x, 0, end_x - start_x, font.GetHeight());
566 rect.Offset(display_rect_.origin());
567 rect.Offset(GetUpdatedDisplayOffset());
568 // Center the rect vertically in |display_rect_|.
569 rect.Offset(Point(0, (display_rect_.height() - rect.height()) / 2));
570 return std::vector<Rect>(1, rect);
555 } 571 }
556 572
557 void RenderText::ApplyCompositionAndSelectionStyles( 573 void RenderText::ApplyCompositionAndSelectionStyles(
558 StyleRanges* style_ranges) const { 574 StyleRanges* style_ranges) const {
559 // TODO(msw): This pattern ought to be reconsidered; what about composition 575 // TODO(msw): This pattern ought to be reconsidered; what about composition
560 // and selection overlaps, retain existing local style features? 576 // and selection overlaps, retain existing local style features?
561 // Apply a composition style override to a copy of the style ranges. 577 // Apply a composition style override to a copy of the style ranges.
562 if (composition_range_.IsValid() && !composition_range_.is_empty()) { 578 if (composition_range_.IsValid() && !composition_range_.is_empty()) {
563 StyleRange composition_style(default_style_); 579 StyleRange composition_style(default_style_);
564 composition_style.underline = true; 580 composition_style.underline = true;
565 composition_style.range.set_start(composition_range_.start()); 581 composition_style.range.set_start(composition_range_.start());
566 composition_style.range.set_end(composition_range_.end()); 582 composition_style.range.set_end(composition_range_.end());
567 ApplyStyleRangeImpl(style_ranges, composition_style); 583 ApplyStyleRangeImpl(style_ranges, composition_style);
568 } 584 }
569 // Apply a selection style override to a copy of the style ranges. 585 // Apply a selection style override to a copy of the style ranges.
570 if (!EmptySelection()) { 586 if (!EmptySelection()) {
571 StyleRange selection_style(default_style_); 587 StyleRange selection_style(default_style_);
572 selection_style.foreground = kSelectedTextColor; 588 selection_style.foreground = kSelectedTextColor;
573 selection_style.range.set_start(MinOfSelection()); 589 selection_style.range.set_start(MinOfSelection());
574 selection_style.range.set_end(MaxOfSelection()); 590 selection_style.range.set_end(MaxOfSelection());
575 ApplyStyleRangeImpl(style_ranges, selection_style); 591 ApplyStyleRangeImpl(style_ranges, selection_style);
576 } 592 }
577 } 593 }
578 594
595 Point RenderText::ToTextPoint(const Point& point) {
596 Point p(point.Subtract(display_rect().origin()));
597 p = p.Subtract(GetUpdatedDisplayOffset());
598 if (base::i18n::IsRTL())
599 p.Offset(GetStringWidth() - display_rect().width() + 1, 0);
600 return p;
601 }
602
603 Point RenderText::ToViewPoint(const Point& point) {
604 Point p(point.Add(display_rect().origin()));
605 p = p.Add(GetUpdatedDisplayOffset());
606 if (base::i18n::IsRTL())
607 p.Offset(display_rect().width() - GetStringWidth() - 1, 0);
608 return p;
609 }
610
611 void RenderText::SetSelectionModel(const SelectionModel& selection_model) {
612 DCHECK_LE(selection_model.selection_start(), text().length());
613 selection_model_.set_selection_start(selection_model.selection_start());
614 DCHECK_LE(selection_model.selection_end(), text().length());
615 selection_model_.set_selection_end(selection_model.selection_end());
616 DCHECK_LT(selection_model.caret_pos(),
617 std::max(text().length(), static_cast<size_t>(1)));
618 selection_model_.set_caret_pos(selection_model.caret_pos());
619 selection_model_.set_caret_placement(selection_model.caret_placement());
620
621 cached_bounds_and_offset_valid_ = false;
622 }
623
624 void RenderText::MoveCursorTo(size_t position, bool select) {
625 size_t cursor = std::min(position, text().length());
626 size_t caret_pos = GetIndexOfPreviousGrapheme(cursor);
627 SelectionModel::CaretPlacement placement = (caret_pos == cursor) ?
628 SelectionModel::LEADING : SelectionModel::TRAILING;
629 size_t selection_start = select ? GetSelectionStart() : cursor;
630 SelectionModel sel(selection_start, cursor, caret_pos, placement);
631 SetSelectionModel(sel);
632 }
633
579 bool RenderText::IsPositionAtWordSelectionBoundary(size_t pos) { 634 bool RenderText::IsPositionAtWordSelectionBoundary(size_t pos) {
580 return pos == 0 || (u_isalnum(text()[pos - 1]) && !u_isalnum(text()[pos])) || 635 return pos == 0 || (u_isalnum(text()[pos - 1]) && !u_isalnum(text()[pos])) ||
581 (!u_isalnum(text()[pos - 1]) && u_isalnum(text()[pos])); 636 (!u_isalnum(text()[pos - 1]) && u_isalnum(text()[pos]));
582 } 637 }
583 638
584 void RenderText::UpdateCachedBoundsAndOffset() { 639 void RenderText::UpdateCachedBoundsAndOffset() {
585 if (cached_bounds_and_offset_valid_) 640 if (cached_bounds_and_offset_valid_)
586 return; 641 return;
587 // First, set the valid flag true to calculate the current cursor bounds using 642 // First, set the valid flag true to calculate the current cursor bounds using
588 // the stale |display_offset_|. Applying |delta_offset| at the end of this 643 // the stale |display_offset_|. Applying |delta_offset| at the end of this
589 // function will set |cursor_bounds_| and |display_offset_| to correct values. 644 // function will set |cursor_bounds_| and |display_offset_| to correct values.
590 cached_bounds_and_offset_valid_ = true; 645 cached_bounds_and_offset_valid_ = true;
591 cursor_bounds_ = GetCursorBounds(selection_model_, insert_mode_); 646 cursor_bounds_ = GetCursorBounds(selection_model_, insert_mode_);
592 cursor_bounds_.set_width(std::max(cursor_bounds_.width(), 1));
593 // Update |display_offset_| to ensure the current cursor is visible. 647 // Update |display_offset_| to ensure the current cursor is visible.
594 int display_width = display_rect_.width(); 648 int display_width = display_rect_.width();
595 int string_width = GetStringWidth(); 649 int string_width = GetStringWidth();
596 int delta_offset = 0; 650 int delta_offset = 0;
597 if (string_width < display_width) { 651 if (string_width < display_width) {
598 // Show all text whenever the text fits to the size. 652 // Show all text whenever the text fits to the size.
599 delta_offset = -display_offset_.x(); 653 delta_offset = -display_offset_.x();
600 } else if (cursor_bounds_.right() > display_rect_.right()) { 654 } else if (cursor_bounds_.right() > display_rect_.right()) {
601 // Pan to show the cursor when it overflows to the right, 655 // Pan to show the cursor when it overflows to the right,
602 delta_offset = display_rect_.right() - cursor_bounds_.right(); 656 delta_offset = display_rect_.right() - cursor_bounds_.right();
603 } else if (cursor_bounds_.x() < display_rect_.x()) { 657 } else if (cursor_bounds_.x() < display_rect_.x()) {
604 // Pan to show the cursor when it overflows to the left. 658 // Pan to show the cursor when it overflows to the left.
605 delta_offset = display_rect_.x() - cursor_bounds_.x(); 659 delta_offset = display_rect_.x() - cursor_bounds_.x();
606 } 660 }
607 display_offset_.Offset(delta_offset, 0); 661 display_offset_.Offset(delta_offset, 0);
608 cursor_bounds_.Offset(delta_offset, 0); 662 cursor_bounds_.Offset(delta_offset, 0);
609 } 663 }
610 664
611 } // namespace gfx 665 } // namespace gfx
OLDNEW
« no previous file with comments | « ui/gfx/render_text.h ('k') | ui/gfx/render_text_win.h » ('j') | ui/gfx/render_text_win.cc » ('J')

Powered by Google App Engine
This is Rietveld 408576698