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

Side by Side Diff: Source/core/rendering/RenderText.cpp

Issue 104813005: Explicitly set text direction for TextRuns (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Patch for landing Created 6 years, 11 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 /* 1 /*
2 * (C) 1999 Lars Knoll (knoll@kde.org) 2 * (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 2000 Dirk Mueller (mueller@kde.org) 3 * (C) 2000 Dirk Mueller (mueller@kde.org)
4 * Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved. 4 * Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
5 * Copyright (C) 2006 Andrew Wellington (proton@wiretapped.net) 5 * Copyright (C) 2006 Andrew Wellington (proton@wiretapped.net)
6 * Copyright (C) 2006 Graham Dennis (graham.dennis@gmail.com) 6 * Copyright (C) 2006 Graham Dennis (graham.dennis@gmail.com)
7 * 7 *
8 * This library is free software; you can redistribute it and/or 8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public 9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either 10 * License as published by the Free Software Foundation; either
(...skipping 22 matching lines...) Expand all
33 #include "core/frame/Settings.h" 33 #include "core/frame/Settings.h"
34 #include "core/rendering/AbstractInlineTextBox.h" 34 #include "core/rendering/AbstractInlineTextBox.h"
35 #include "core/rendering/EllipsisBox.h" 35 #include "core/rendering/EllipsisBox.h"
36 #include "core/rendering/InlineTextBox.h" 36 #include "core/rendering/InlineTextBox.h"
37 #include "core/rendering/RenderBlock.h" 37 #include "core/rendering/RenderBlock.h"
38 #include "core/rendering/RenderCombineText.h" 38 #include "core/rendering/RenderCombineText.h"
39 #include "core/rendering/RenderLayer.h" 39 #include "core/rendering/RenderLayer.h"
40 #include "core/rendering/RenderView.h" 40 #include "core/rendering/RenderView.h"
41 #include "core/rendering/break_lines.h" 41 #include "core/rendering/break_lines.h"
42 #include "platform/geometry/FloatQuad.h" 42 #include "platform/geometry/FloatQuad.h"
43 #include "platform/text/BidiResolver.h"
43 #include "platform/text/TextBreakIterator.h" 44 #include "platform/text/TextBreakIterator.h"
45 #include "platform/text/TextRunIterator.h"
44 #include "wtf/text/StringBuffer.h" 46 #include "wtf/text/StringBuffer.h"
45 #include "wtf/text/StringBuilder.h" 47 #include "wtf/text/StringBuilder.h"
46 #include "wtf/unicode/CharacterNames.h" 48 #include "wtf/unicode/CharacterNames.h"
47 49
48 using namespace std; 50 using namespace std;
49 using namespace WTF; 51 using namespace WTF;
50 using namespace Unicode; 52 using namespace Unicode;
51 53
52 namespace WebCore { 54 namespace WebCore {
53 55
(...skipping 659 matching lines...) Expand 10 before | Expand all | Expand 10 after
713 left = max(left, leftEdge); 715 left = max(left, leftEdge);
714 left = min(left, rootRight - caretWidth); 716 left = min(left, rootRight - caretWidth);
715 } else { 717 } else {
716 left = min(left, rightEdge - caretWidthRightOfOffset); 718 left = min(left, rightEdge - caretWidthRightOfOffset);
717 left = max(left, rootLeft); 719 left = max(left, rootLeft);
718 } 720 }
719 721
720 return style()->isHorizontalWritingMode() ? IntRect(left, top, caretWidth, h eight) : IntRect(top, left, height, caretWidth); 722 return style()->isHorizontalWritingMode() ? IntRect(left, top, caretWidth, h eight) : IntRect(top, left, height, caretWidth);
721 } 723 }
722 724
723 ALWAYS_INLINE float RenderText::widthFromCache(const Font& f, int start, int len , float xPos, HashSet<const SimpleFontData*>* fallbackFonts, GlyphOverflow* glyp hOverflow) const 725 ALWAYS_INLINE float RenderText::widthFromCache(const Font& f, int start, int len , float xPos, TextDirection textDirection, HashSet<const SimpleFontData*>* fallb ackFonts, GlyphOverflow* glyphOverflow) const
724 { 726 {
725 if (style()->hasTextCombine() && isCombineText()) { 727 if (style()->hasTextCombine() && isCombineText()) {
726 const RenderCombineText* combineText = toRenderCombineText(this); 728 const RenderCombineText* combineText = toRenderCombineText(this);
727 if (combineText->isCombined()) 729 if (combineText->isCombined())
728 return combineText->combinedTextWidth(f); 730 return combineText->combinedTextWidth(f);
729 } 731 }
730 732
731 if (f.isFixedPitch() && !f.isSmallCaps() && m_isAllASCII && (!glyphOverflow || !glyphOverflow->computeBounds)) { 733 if (f.isFixedPitch() && !f.isSmallCaps() && m_isAllASCII && (!glyphOverflow || !glyphOverflow->computeBounds)) {
732 float monospaceCharacterWidth = f.spaceWidth(); 734 float monospaceCharacterWidth = f.spaceWidth();
733 float w = 0; 735 float w = 0;
(...skipping 19 matching lines...) Expand all
753 } else { 755 } else {
754 w += monospaceCharacterWidth; 756 w += monospaceCharacterWidth;
755 isSpace = false; 757 isSpace = false;
756 } 758 }
757 if (isSpace && i > start) 759 if (isSpace && i > start)
758 w += f.wordSpacing(); 760 w += f.wordSpacing();
759 } 761 }
760 return w; 762 return w;
761 } 763 }
762 764
763 TextRun run = RenderBlockFlow::constructTextRun(const_cast<RenderText*>(this ), f, this, start, len, style()); 765 TextRun run = RenderBlockFlow::constructTextRun(const_cast<RenderText*>(this ), f, this, start, len, style(), textDirection);
764 run.setCharactersLength(textLength() - start); 766 run.setCharactersLength(textLength() - start);
765 ASSERT(run.charactersLength() >= run.length()); 767 ASSERT(run.charactersLength() >= run.length());
766 768
767 run.setCharacterScanForCodePath(!canUseSimpleFontCodePath()); 769 run.setCharacterScanForCodePath(!canUseSimpleFontCodePath());
768 run.setTabSize(!style()->collapseWhiteSpace(), style()->tabSize()); 770 run.setTabSize(!style()->collapseWhiteSpace(), style()->tabSize());
769 run.setXPos(xPos); 771 run.setXPos(xPos);
770 return f.width(run, fallbackFonts, glyphOverflow); 772 return f.width(run, fallbackFonts, glyphOverflow);
771 } 773 }
772 774
773 void RenderText::trimmedPrefWidths(float leadWidth, 775 void RenderText::trimmedPrefWidths(float leadWidth,
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
832 const Font& f = style()->font(); // FIXME: This ignores first-line. 834 const Font& f = style()->font(); // FIXME: This ignores first-line.
833 bool firstLine = true; 835 bool firstLine = true;
834 firstLineMaxWidth = maxWidth; 836 firstLineMaxWidth = maxWidth;
835 lastLineMaxWidth = maxWidth; 837 lastLineMaxWidth = maxWidth;
836 for (int i = 0; i < len; i++) { 838 for (int i = 0; i < len; i++) {
837 int linelen = 0; 839 int linelen = 0;
838 while (i + linelen < len && text[i + linelen] != '\n') 840 while (i + linelen < len && text[i + linelen] != '\n')
839 linelen++; 841 linelen++;
840 842
841 if (linelen) { 843 if (linelen) {
842 lastLineMaxWidth = widthFromCache(f, i, linelen, leadWidth + las tLineMaxWidth, 0, 0); 844 lastLineMaxWidth = widthFromCache(f, i, linelen, leadWidth + las tLineMaxWidth, LTR, 0, 0);
843 if (firstLine) { 845 if (firstLine) {
844 firstLine = false; 846 firstLine = false;
845 leadWidth = 0; 847 leadWidth = 0;
846 firstLineMaxWidth = lastLineMaxWidth; 848 firstLineMaxWidth = lastLineMaxWidth;
847 } 849 }
848 i += linelen; 850 i += linelen;
849 } else if (firstLine) { 851 } else if (firstLine) {
850 firstLineMaxWidth = 0; 852 firstLineMaxWidth = 0;
851 firstLine = false; 853 firstLine = false;
852 leadWidth = 0; 854 leadWidth = 0;
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
930 // space, then subtract its width. 932 // space, then subtract its width.
931 float wordTrailingSpaceWidth = f.typesettingFeatures() & Kerning ? f.width(R enderBlockFlow::constructTextRun(this, f, &space, 1, styleToUse)) + wordSpacing : 0; 933 float wordTrailingSpaceWidth = f.typesettingFeatures() & Kerning ? f.width(R enderBlockFlow::constructTextRun(this, f, &space, 1, styleToUse)) + wordSpacing : 0;
932 934
933 // If automatic hyphenation is allowed, we keep track of the width of the wi dest word (or word 935 // If automatic hyphenation is allowed, we keep track of the width of the wi dest word (or word
934 // fragment) encountered so far, and only try hyphenating words that are wid er. 936 // fragment) encountered so far, and only try hyphenating words that are wid er.
935 float maxWordWidth = numeric_limits<float>::max(); 937 float maxWordWidth = numeric_limits<float>::max();
936 int firstGlyphLeftOverflow = -1; 938 int firstGlyphLeftOverflow = -1;
937 939
938 bool breakAll = (styleToUse->wordBreak() == BreakAllWordBreak || styleToUse- >wordBreak() == BreakWordBreak) && styleToUse->autoWrap(); 940 bool breakAll = (styleToUse->wordBreak() == BreakAllWordBreak || styleToUse- >wordBreak() == BreakWordBreak) && styleToUse->autoWrap();
939 941
942 TextRun textRun(text());
943 BidiResolver<TextRunIterator, BidiCharacterRun> bidiResolver;
944 bidiResolver.setStatus(BidiStatus(textRun.direction(), textRun.directionalOv erride()));
945 bidiResolver.setPositionIgnoringNestedIsolates(TextRunIterator(&textRun, 0)) ;
946 bool hardLineBreak = false;
947 bool reorderRuns = false;
948 bidiResolver.createBidiRunsForLine(TextRunIterator(&textRun, textRun.length( )), NoVisualOverride, hardLineBreak, reorderRuns);
949
950 BidiRunList<BidiCharacterRun>& bidiRuns = bidiResolver.runs();
951 BidiCharacterRun* run = bidiRuns.firstRun();
940 for (int i = 0; i < len; i++) { 952 for (int i = 0; i < len; i++) {
941 UChar c = uncheckedCharacterAt(i); 953 UChar c = uncheckedCharacterAt(i);
942 954
955 while (i > run->stop())
956 run = run->next();
957
958 ASSERT(run);
959 ASSERT(i >= run->start() && i <= run->stop());
960 TextDirection textDirection = run->direction();
961
943 bool previousCharacterIsSpace = isSpace; 962 bool previousCharacterIsSpace = isSpace;
944
945 bool isNewline = false; 963 bool isNewline = false;
946 if (c == '\n') { 964 if (c == '\n') {
947 if (styleToUse->preserveNewline()) { 965 if (styleToUse->preserveNewline()) {
948 m_hasBreak = true; 966 m_hasBreak = true;
949 isNewline = true; 967 isNewline = true;
950 isSpace = false; 968 isSpace = false;
951 } else 969 } else
952 isSpace = true; 970 isSpace = true;
953 } else if (c == '\t') { 971 } else if (c == '\t') {
954 if (!styleToUse->collapseWhiteSpace()) { 972 if (!styleToUse->collapseWhiteSpace()) {
(...skipping 17 matching lines...) Expand all
972 990
973 if (ignoringSpaces && !isSpace) 991 if (ignoringSpaces && !isSpace)
974 ignoringSpaces = false; 992 ignoringSpaces = false;
975 993
976 // Ignore spaces and soft hyphens 994 // Ignore spaces and soft hyphens
977 if (ignoringSpaces) { 995 if (ignoringSpaces) {
978 ASSERT(lastWordBoundary == i); 996 ASSERT(lastWordBoundary == i);
979 lastWordBoundary++; 997 lastWordBoundary++;
980 continue; 998 continue;
981 } else if (c == softHyphen) { 999 } else if (c == softHyphen) {
982 currMaxWidth += widthFromCache(f, lastWordBoundary, i - lastWordBoun dary, leadWidth + currMaxWidth, &fallbackFonts, &glyphOverflow); 1000 currMaxWidth += widthFromCache(f, lastWordBoundary, i - lastWordBoun dary, leadWidth + currMaxWidth, textDirection, &fallbackFonts, &glyphOverflow);
983 if (firstGlyphLeftOverflow < 0) 1001 if (firstGlyphLeftOverflow < 0)
984 firstGlyphLeftOverflow = glyphOverflow.left; 1002 firstGlyphLeftOverflow = glyphOverflow.left;
985 lastWordBoundary = i + 1; 1003 lastWordBoundary = i + 1;
986 continue; 1004 continue;
987 } 1005 }
988 1006
989 bool hasBreak = breakAll || isBreakable(breakIterator, i, nextBreakable) ; 1007 bool hasBreak = breakAll || isBreakable(breakIterator, i, nextBreakable) ;
990 bool betweenWords = true; 1008 bool betweenWords = true;
991 int j = i; 1009 int j = i;
992 while (c != '\n' && c != ' ' && c != '\t' && (c != softHyphen)) { 1010 while (c != '\n' && c != ' ' && c != '\t' && (c != softHyphen)) {
993 j++; 1011 j++;
994 if (j == len) 1012 if (j == len)
995 break; 1013 break;
996 c = uncheckedCharacterAt(j); 1014 c = uncheckedCharacterAt(j);
997 if (isBreakable(breakIterator, j, nextBreakable) && characterAt(j - 1) != softHyphen) 1015 if (isBreakable(breakIterator, j, nextBreakable) && characterAt(j - 1) != softHyphen)
998 break; 1016 break;
999 if (breakAll) { 1017 if (breakAll) {
1000 betweenWords = false; 1018 betweenWords = false;
1001 break; 1019 break;
1002 } 1020 }
1003 } 1021 }
1004 1022
1005 int wordLen = j - i; 1023 int wordLen = j - i;
1006 if (wordLen) { 1024 if (wordLen) {
1007 bool isSpace = (j < len) && c == ' '; 1025 bool isSpace = (j < len) && c == ' ';
1008 float w; 1026 float w;
1009 if (wordTrailingSpaceWidth && isSpace) 1027 if (wordTrailingSpaceWidth && isSpace)
1010 w = widthFromCache(f, i, wordLen + 1, leadWidth + currMaxWidth, &fallbackFonts, &glyphOverflow) - wordTrailingSpaceWidth; 1028 w = widthFromCache(f, i, wordLen + 1, leadWidth + currMaxWidth, textDirection, &fallbackFonts, &glyphOverflow) - wordTrailingSpaceWidth;
1011 else { 1029 else {
1012 w = widthFromCache(f, i, wordLen, leadWidth + currMaxWidth, &fal lbackFonts, &glyphOverflow); 1030 w = widthFromCache(f, i, wordLen, leadWidth + currMaxWidth, text Direction, &fallbackFonts, &glyphOverflow);
1013 if (c == softHyphen) 1031 if (c == softHyphen)
1014 currMinWidth += hyphenWidth(this, f); 1032 currMinWidth += hyphenWidth(this, f);
1015 } 1033 }
1016 1034
1017 maxWordWidth = max(maxWordWidth, w); 1035 maxWordWidth = max(maxWordWidth, w);
1018 1036
1019 if (firstGlyphLeftOverflow < 0) 1037 if (firstGlyphLeftOverflow < 0)
1020 firstGlyphLeftOverflow = glyphOverflow.left; 1038 firstGlyphLeftOverflow = glyphOverflow.left;
1021 currMinWidth += w; 1039 currMinWidth += w;
1022 if (betweenWords) { 1040 if (betweenWords) {
1023 if (lastWordBoundary == i) 1041 if (lastWordBoundary == i)
1024 currMaxWidth += w; 1042 currMaxWidth += w;
1025 else 1043 else
1026 currMaxWidth += widthFromCache(f, lastWordBoundary, j - last WordBoundary, leadWidth + currMaxWidth, &fallbackFonts, &glyphOverflow); 1044 currMaxWidth += widthFromCache(f, lastWordBoundary, j - last WordBoundary, leadWidth + currMaxWidth, textDirection, &fallbackFonts, &glyphOve rflow);
1027 lastWordBoundary = j; 1045 lastWordBoundary = j;
1028 } 1046 }
1029 1047
1030 bool isCollapsibleWhiteSpace = (j < len) && styleToUse->isCollapsibl eWhiteSpace(c); 1048 bool isCollapsibleWhiteSpace = (j < len) && styleToUse->isCollapsibl eWhiteSpace(c);
1031 if (j < len && styleToUse->autoWrap()) 1049 if (j < len && styleToUse->autoWrap())
1032 m_hasBreakableChar = true; 1050 m_hasBreakableChar = true;
1033 1051
1034 // Add in wordSpacing to our currMaxWidth, but not if this is the la st word on a line or the 1052 // Add in wordSpacing to our currMaxWidth, but not if this is the la st word on a line or the
1035 // last word in the run. 1053 // last word in the run.
1036 if (wordSpacing && (isSpace || isCollapsibleWhiteSpace) && !contains OnlyWhitespace(j, len-j)) 1054 if (wordSpacing && (isSpace || isCollapsibleWhiteSpace) && !contains OnlyWhitespace(j, len-j))
(...skipping 401 matching lines...) Expand 10 before | Expand all | Expand 10 after
1438 m_lastTextBox = s->prevTextBox(); 1456 m_lastTextBox = s->prevTextBox();
1439 else 1457 else
1440 s->nextTextBox()->setPreviousTextBox(s->prevTextBox()); 1458 s->nextTextBox()->setPreviousTextBox(s->prevTextBox());
1441 s->destroy(); 1459 s->destroy();
1442 return; 1460 return;
1443 } 1461 }
1444 1462
1445 m_containsReversedText |= !s->isLeftToRightDirection(); 1463 m_containsReversedText |= !s->isLeftToRightDirection();
1446 } 1464 }
1447 1465
1448 float RenderText::width(unsigned from, unsigned len, float xPos, bool firstLine, HashSet<const SimpleFontData*>* fallbackFonts, GlyphOverflow* glyphOverflow) co nst 1466 float RenderText::width(unsigned from, unsigned len, float xPos, TextDirection t extDirection, bool firstLine, HashSet<const SimpleFontData*>* fallbackFonts, Gly phOverflow* glyphOverflow) const
1449 { 1467 {
1450 if (from >= textLength()) 1468 if (from >= textLength())
1451 return 0; 1469 return 0;
1452 1470
1453 if (from + len > textLength()) 1471 if (from + len > textLength())
1454 len = textLength() - from; 1472 len = textLength() - from;
1455 1473
1456 return width(from, len, style(firstLine)->font(), xPos, fallbackFonts, glyph Overflow); 1474 return width(from, len, style(firstLine)->font(), xPos, textDirection, fallb ackFonts, glyphOverflow);
1457 } 1475 }
1458 1476
1459 float RenderText::width(unsigned from, unsigned len, const Font& f, float xPos, HashSet<const SimpleFontData*>* fallbackFonts, GlyphOverflow* glyphOverflow) con st 1477 float RenderText::width(unsigned from, unsigned len, const Font& f, float xPos, TextDirection textDirection, HashSet<const SimpleFontData*>* fallbackFonts, Glyp hOverflow* glyphOverflow) const
1460 { 1478 {
1461 ASSERT(from + len <= textLength()); 1479 ASSERT(from + len <= textLength());
1462 if (!textLength()) 1480 if (!textLength())
1463 return 0; 1481 return 0;
1464 1482
1465 float w; 1483 float w;
1466 if (&f == &style()->font()) { 1484 if (&f == &style()->font()) {
1467 if (!style()->preserveNewline() && !from && len == textLength() && (!gly phOverflow || !glyphOverflow->computeBounds)) { 1485 if (!style()->preserveNewline() && !from && len == textLength() && (!gly phOverflow || !glyphOverflow->computeBounds)) {
1468 if (fallbackFonts) { 1486 if (fallbackFonts) {
1469 ASSERT(glyphOverflow); 1487 ASSERT(glyphOverflow);
1470 if (preferredLogicalWidthsDirty() || !m_knownToHaveNoOverflowAnd NoFallbackFonts) { 1488 if (preferredLogicalWidthsDirty() || !m_knownToHaveNoOverflowAnd NoFallbackFonts) {
1471 const_cast<RenderText*>(this)->computePreferredLogicalWidths (0, *fallbackFonts, *glyphOverflow); 1489 const_cast<RenderText*>(this)->computePreferredLogicalWidths (0, *fallbackFonts, *glyphOverflow);
1472 if (fallbackFonts->isEmpty() && !glyphOverflow->left && !gly phOverflow->right && !glyphOverflow->top && !glyphOverflow->bottom) 1490 if (fallbackFonts->isEmpty() && !glyphOverflow->left && !gly phOverflow->right && !glyphOverflow->top && !glyphOverflow->bottom)
1473 m_knownToHaveNoOverflowAndNoFallbackFonts = true; 1491 m_knownToHaveNoOverflowAndNoFallbackFonts = true;
1474 } 1492 }
1475 w = m_maxWidth; 1493 w = m_maxWidth;
1476 } else 1494 } else
1477 w = maxLogicalWidth(); 1495 w = maxLogicalWidth();
1478 } else 1496 } else {
1479 w = widthFromCache(f, from, len, xPos, fallbackFonts, glyphOverflow) ; 1497 w = widthFromCache(f, from, len, xPos, textDirection, fallbackFonts, glyphOverflow);
1498 }
1480 } else { 1499 } else {
1481 TextRun run = RenderBlockFlow::constructTextRun(const_cast<RenderText*>( this), f, this, from, len, style()); 1500 TextRun run = RenderBlockFlow::constructTextRun(const_cast<RenderText*>( this), f, this, from, len, style(), textDirection);
1482 run.setCharactersLength(textLength() - from); 1501 run.setCharactersLength(textLength() - from);
1483 ASSERT(run.charactersLength() >= run.length()); 1502 ASSERT(run.charactersLength() >= run.length());
1484 1503
1485 run.setCharacterScanForCodePath(!canUseSimpleFontCodePath()); 1504 run.setCharacterScanForCodePath(!canUseSimpleFontCodePath());
1486 run.setTabSize(!style()->collapseWhiteSpace(), style()->tabSize()); 1505 run.setTabSize(!style()->collapseWhiteSpace(), style()->tabSize());
1487 run.setXPos(xPos); 1506 run.setXPos(xPos);
1488 w = f.width(run, fallbackFonts, glyphOverflow); 1507 w = f.width(run, fallbackFonts, glyphOverflow);
1489 } 1508 }
1490 1509
1491 return w; 1510 return w;
(...skipping 349 matching lines...) Expand 10 before | Expand all | Expand 10 after
1841 } 1860 }
1842 secureTextTimer->restartWithNewText(lastTypedCharacterOffset); 1861 secureTextTimer->restartWithNewText(lastTypedCharacterOffset);
1843 } 1862 }
1844 1863
1845 PassRefPtr<AbstractInlineTextBox> RenderText::firstAbstractInlineTextBox() 1864 PassRefPtr<AbstractInlineTextBox> RenderText::firstAbstractInlineTextBox()
1846 { 1865 {
1847 return AbstractInlineTextBox::getOrCreate(this, m_firstTextBox); 1866 return AbstractInlineTextBox::getOrCreate(this, m_firstTextBox);
1848 } 1867 }
1849 1868
1850 } // namespace WebCore 1869 } // namespace WebCore
OLDNEW
« no previous file with comments | « Source/core/rendering/RenderText.h ('k') | Source/core/rendering/line/BreakingContextInlineHeaders.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698