Reviewed by Ryosuke Niwa.
Experiment with moving caret by word in visual order.
https://bugs.webkit.org/show_bug.cgi?id=57336
* editing/selection/move-by-word-visually-expected.txt: Added.
* editing/selection/move-by-word-visually.html: Added.
2011-03-30 Xiaomei Ji <xji@chromium.org>
Reviewed by Ryosuke Niwa.
Experiment with moving caret by word in visual order.
https://bugs.webkit.org/show_bug.cgi?id=57336
Follow Firefox's convention in Windows,
In LTR block, word break visually moves cursor to the left boundary of words,
In RTL block, word break visually moves cursor to the right boundary of words.
This is the 1st version of implementing "move caret by word in visual order".
It only works in the following situation:
1. For a LTR box in a LTR block or a RTL box in RTL block,
when caret is at the left boundary of the box and we are looking for
the word boundary in right.
2. For a LTR or RTL box in a LTR block, when caret is at the left boundary
of the box and we are looking for the word boundary in left and
previous box is a LTR box.
3. For a LTR or RTL box in a RTL block, when the caret is at the right
boundary of the box and we are looking for the word boundary in right and next box is RTL box.
An experimental granularity is introduced, as a side effect, functions having switch statements
to handle those granularities have to add more one case to handle this new granularity.
The experimental granularity is exposed though JS by '-webkit-visual-word".
The overall algorithm is looping through inline boxes visually and looking
for the visually nearest word break position.
Test: editing/selection/move-by-word-visually.html
* editing/SelectionController.cpp:
(WebCore::SelectionController::modifyExtendingRight):
(WebCore::SelectionController::modifyExtendingForward):
(WebCore::SelectionController::modifyMovingRight):
(WebCore::SelectionController::modifyMovingForward):
(WebCore::SelectionController::modifyExtendingLeft):
(WebCore::SelectionController::modifyExtendingBackward):
(WebCore::SelectionController::modifyMovingLeft):
(WebCore::SelectionController::modifyMovingBackward):
* editing/TextGranularity.h:
* editing/VisibleSelection.cpp:
(WebCore::VisibleSelection::setStartAndEndFromBaseAndExtentRespectingGranularity):
* editing/visible_units.cpp:
(WebCore::previousWordBreakInBoxInsideBlockWithSameDirectionality):
(WebCore::wordBoundaryInBox):
(WebCore::wordBoundaryInAdjacentBoxes):
(WebCore::leftWordBoundary):
(WebCore::rightWordBoundary):
(WebCore::leftWordPosition):
(WebCore::rightWordPosition):
* editing/visible_units.h:
* page/DOMSelection.cpp:
(WebCore::DOMSelection::modify):
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@82588
268f45cc-cd09-0410-ab3c-
d52691b4dbfc
+2011-03-30 Xiaomei Ji <xji@chromium.org>
+
+ Reviewed by Ryosuke Niwa.
+
+ Experiment with moving caret by word in visual order.
+ https://bugs.webkit.org/show_bug.cgi?id=57336
+
+ * editing/selection/move-by-word-visually-expected.txt: Added.
+ * editing/selection/move-by-word-visually.html: Added.
+
2011-03-31 Pavel Podivilov <podivilov@chromium.org>
Unreviewed, update chromium test expectations.
--- /dev/null
+
+======== Move By Word ====
+Test 1, LTR:
+Move right by one word
+"abc def hij opq rst"[0, 4, 8, 12, 16, 19] FAIL expected: [4, 8, 12, 16, 19, 19]
+Move left by one word
+"abc def hij opq rst"[16, 16, 12, 8, 4, 0] FAIL expected: [16, 12, 8, 4, 0, 0]
+Test 2, RTL:
+Move left by one word
+"abc def hij opq rst"[0, 15, 11, 7, 3, 19] FAIL expected: [15, 11, 7, 3, 19, 19]
+Move right by one word
+"abc def hij opq rst"[19, 3, 7, 11, 15, 0] FAIL expected: [3, 7, 11, 15, 0, 0]
+Test 3, LTR:
+Move right by one word
+"ZZZ QQQ BBB CCC XXX"[0, 15, 11, 7, 3, 19] FAIL expected: [15, 11, 7, 3, 19, 19]
+Move left by one word
+"ZZZ QQQ BBB CCC XXX"[19, 3, 7, 11, 15, 0] FAIL expected: [3, 7, 11, 15, 0, 0]
+Test 4, RTL:
+Move left by one word
+"ZZZ QQQ BBB CCC XXX"[0, 4, 8, 12, 16, 19] FAIL expected: [4, 8, 12, 16, 19, 19]
+Move right by one word
+"ZZZ QQQ BBB CCC XXX"[16, 16, 12, 8, 4, 0] FAIL expected: [16, 12, 8, 4, 0, 0]
+Test 5, LTR:
+Move right by one word
+"abc def ZQB RIG uvw xyz"[0, 4, 8, 11, 16, 20, 23] FAIL expected: [4, 8, 11, 16, 20, 23, 23]
+Move left by one word
+"abc def ZQB RIG uvw xyz"[20, 20, 16, 11, 4, 4, 0] FAIL expected: [20, 16, 11, 8, 4, 0, 0]
+Test 6, RTL:
+Move left by one word
+"abc def ZQB RIG uvw xyz"[0, 3, 8, 12, 16, 19, 23] FAIL expected: [3, 8, 12, 16, 19, 23, 23]
+Move right by one word
+"abc def ZQB RIG uvw xyz"[12, 19, 12, 12, 8, 3, 0] FAIL expected: [19, 16, 12, 8, 3, 0, 0]
+Test 7, LTR:
+Move right by one word
+"ZQB abc RIG"[0, 4, 8, 11] FAIL expected: [4, 8, 11, 11]
+Move left by one word
+"ZQB abc RIG"[4, 4, 4, 0] FAIL expected: [8, 4, 0, 0]
+Test 8, RTL:
+Move left by one word
+"ZQB abc RIG"[0, 4, 8, 11] FAIL expected: [4, 8, 11, 11]
+Move right by one word
+"ZQB abc RIG"[8, 8, 0, 0] FAIL expected: [8, 4, 0, 0]
+
--- /dev/null
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<style>
+div.test {
+ -webkit-user-modify: read-write;
+ padding: 4px;
+ border: 1px dashed lightblue;
+ margin: 4px 4px 4px 24px;
+ outline: none;
+ font-family: Lucida Grande;
+ counter-increment: test-number;
+}
+div.test:before { content: counter(test-number); position: absolute; left: 8px; font-size: x-small; text-align: right; width: 20px; }
+div.test span { background-color: #def; }
+div.test img { width: 1em; height: 1em; background-color: lightgreen; }
+div.test img + img { background-color: lightblue; }
+div.test div { border: 1px dashed pink; padding: 3px; height: 2em; }
+test_move_by_word {display: none;}
+</style>
+<script>
+var messages = [];
+
+function log(message)
+{
+ messages.push(message);
+}
+
+function flushLog()
+{
+ document.getElementById("console").appendChild(document.createTextNode(messages.join("")));
+}
+
+function caretCoordinates()
+{
+ if (!window.textInputController)
+ return { x: 0, y :0 };
+ var caretRect = textInputController.firstRectForCharacterRange(textInputController.selectedRange()[0], 0);
+ return { x: caretRect[0], y: caretRect[1] };
+}
+
+
+function fold(string)
+{
+ var result = "";
+ for (var i = 0; i < string.length; ++i) {
+ var char = string.charCodeAt(i);
+ if (char >= 0x05d0)
+ char -= 0x058f;
+ else if (char == 10) {
+ result += "\\n";
+ continue;
+ }
+ result += String.fromCharCode(char);
+ }
+ return result;
+}
+
+function logPositions(positions)
+{
+ for (var i = 0; i < positions.length; ++i) {
+ if (i) {
+ if (positions[i].node != positions[i - 1].node)
+ log("]");
+ log(", ");
+ }
+ if (!i || positions[i].node != positions[i - 1].node)
+ log((positions[i].node instanceof Text ? '"' + fold(positions[i].node.data) + '"' : "<" + positions[i].node.tagName + ">") + "[");
+ log(positions[i].offset);
+ }
+ log("]");
+}
+
+var wordBreaks;
+
+function validateData(positions)
+{
+ for (var i = 0; i < wordBreaks.length - 1; ++i) {
+ if (positions[i].offset != wordBreaks[i + 1]) {
+ break;
+ }
+ }
+ if (i != wordBreaks.length - 1 && positions[i] != wordBreaks[i]) {
+ log(" FAIL expected: [");
+ for (var i = 1; i < wordBreaks.length; ++i) {
+ log(wordBreaks[i] + ", ");
+ }
+ log(wordBreaks[wordBreaks.length - 1] + "]");
+ }
+}
+
+function collectWordBreaks(test, searchDirection)
+{
+ if (searchDirection == "right") {
+ if (test.getAttribute("dir") == 'ltr')
+ wordBreaks = test.title.split("|")[0].split(" ");
+ else
+ wordBreaks = test.title.split("|")[1].split(" ");
+ } else {
+ if (test.getAttribute("dir") == 'ltr')
+ wordBreaks = test.title.split("|")[1].split(" ");
+ else
+ wordBreaks = test.title.split("|")[0].split(" ");
+ }
+}
+
+function moveByWord(sel, test, searchDirection, dir)
+{
+ log("Move " + searchDirection + " by one word\n");
+ var prevOffset = sel.anchorOffset;
+ var node = sel.anchorNode;
+ collectWordBreaks(test, searchDirection);
+ sel.setPosition(node, wordBreaks[0]);
+ var positions = [];
+ for (var index = 1; index < wordBreaks.length; ++index) {
+ sel.modify("move", searchDirection, "-webkit-visual-word");
+ positions.push({ node: sel.anchorNode, offset: sel.anchorOffset, point: caretCoordinates() });
+ sel.setPosition(node, wordBreaks[index]);
+ }
+ sel.modify("move", searchDirection, "-webkit-visual-word");
+ positions.push({ node: sel.anchorNode, offset: sel.anchorOffset, point: caretCoordinates() });
+ logPositions(positions);
+ validateData(positions);
+ log("\n");
+}
+
+function moveByWordForEveryPosition(sel, test, dir)
+{
+ // Check ctrl-right-arrow works for every position.
+ sel.setPosition(test, 0);
+ var direction = "right";
+ if (dir == "rtl")
+ direction = "left";
+ moveByWord(sel, test, direction, dir);
+ // Check ctrl-left-arrow works for every position.
+ if (dir == "ltr")
+ direction = "left";
+ else
+ direction = "right";
+ moveByWord(sel, test, direction, dir);
+}
+
+function runMoveLeftRight(tests, unit)
+{
+ var sel = getSelection();
+ for (var i = 0; i < tests.length; ++i) {
+ var positionsMovingRight;
+ var positionsMovingLeft;
+
+ if (tests[i].getAttribute("dir") == 'ltr')
+ {
+ log("Test " + (i + 1) + ", LTR:\n");
+ moveByWordForEveryPosition(sel, tests[i], "ltr");
+ } else {
+ log("Test " + (i + 1) + ", RTL:\n");
+ moveByWordForEveryPosition(sel, tests[i], "rtl");
+ }
+
+ }
+ document.getElementById("testMoveByWord").style.display = "none";
+}
+
+function runTest() {
+ log("\n======== Move By Word ====\n");
+ var tests = document.getElementsByClassName("test_move_by_word");
+ runMoveLeftRight(tests, "word");
+}
+
+onload = function() {
+ try {
+ runTest();
+ } finally {
+ flushLog();
+ }
+};
+
+if (window.layoutTestController)
+ layoutTestController.dumpAsText();
+</script>
+</head>
+<body>
+<div id="testMoveByWord">
+<!-- The numbers put in title are starting word boundaries.
+The numbers printed out in the output are ending word boundaries. -->
+<div dir=ltr class="test_move_by_word" title="0 4 8 12 16 19|19 16 12 8 4 0" contenteditable>abc def hij opq rst</div>
+<div dir=rtl class="test_move_by_word" title="0 15 11 7 3 19|19 3 7 11 15 0" contenteditable>abc def hij opq rst</div>
+<div dir=ltr class="test_move_by_word" title="0 15 11 7 3 19|19 3 7 11 15 0" contenteditable>ששש נננ בבב גגג קקק</div>
+<div dir=rtl class="test_move_by_word" title="0 4 8 12 16 19|19 16 12 8 4 0" contenteditable>ששש נננ בבב גגג קקק</div>
+<div dir=ltr class="test_move_by_word" title="0 4 8 11 16 20 23|23 20 16 11 8 4 0" contenteditable>abc def שנב סטז uvw xyz</div>
+<div dir=rtl class="test_move_by_word" title="0 3 8 12 16 19 23|23 19 16 12 8 3 0" contenteditable>abc def שנב סטז uvw xyz</div>
+<div dir=ltr class="test_move_by_word" title="0 4 8 11|11 8 4 0" contenteditable>שנב abc סטז</div>
+<div dir=rtl class="test_move_by_word" title="0 4 8 11|11 8 4 0" contenteditable>שנב abc סטז</div>
+</div>
+
+<pre id="console"></pre>
+</body>
+</html>
+2011-03-30 Xiaomei Ji <xji@chromium.org>
+
+ Reviewed by Ryosuke Niwa.
+
+ Experiment with moving caret by word in visual order.
+ https://bugs.webkit.org/show_bug.cgi?id=57336
+
+ Follow Firefox's convention in Windows,
+ In LTR block, word break visually moves cursor to the left boundary of words,
+ In RTL block, word break visually moves cursor to the right boundary of words.
+
+ This is the 1st version of implementing "move caret by word in visual order".
+ It only works in the following situation:
+ 1. For a LTR box in a LTR block or a RTL box in RTL block,
+ when caret is at the left boundary of the box and we are looking for
+ the word boundary in right.
+ 2. For a LTR or RTL box in a LTR block, when caret is at the left boundary
+ of the box and we are looking for the word boundary in left and
+ previous box is a LTR box.
+ 3. For a LTR or RTL box in a RTL block, when the caret is at the right
+ boundary of the box and we are looking for the word boundary in right and next box is RTL box.
+
+ An experimental granularity is introduced, as a side effect, functions having switch statements
+ to handle those granularities have to add more one case to handle this new granularity.
+ The experimental granularity is exposed though JS by '-webkit-visual-word".
+
+ The overall algorithm is looping through inline boxes visually and looking
+ for the visually nearest word break position.
+
+ Test: editing/selection/move-by-word-visually.html
+
+ * editing/SelectionController.cpp:
+ (WebCore::SelectionController::modifyExtendingRight):
+ (WebCore::SelectionController::modifyExtendingForward):
+ (WebCore::SelectionController::modifyMovingRight):
+ (WebCore::SelectionController::modifyMovingForward):
+ (WebCore::SelectionController::modifyExtendingLeft):
+ (WebCore::SelectionController::modifyExtendingBackward):
+ (WebCore::SelectionController::modifyMovingLeft):
+ (WebCore::SelectionController::modifyMovingBackward):
+ * editing/TextGranularity.h:
+ * editing/VisibleSelection.cpp:
+ (WebCore::VisibleSelection::setStartAndEndFromBaseAndExtentRespectingGranularity):
+ * editing/visible_units.cpp:
+ (WebCore::previousWordBreakInBoxInsideBlockWithSameDirectionality):
+ (WebCore::wordBoundaryInBox):
+ (WebCore::wordBoundaryInAdjacentBoxes):
+ (WebCore::leftWordBoundary):
+ (WebCore::rightWordBoundary):
+ (WebCore::leftWordPosition):
+ (WebCore::rightWordPosition):
+ * editing/visible_units.h:
+ * page/DOMSelection.cpp:
+ (WebCore::DOMSelection::modify):
+
2011-03-31 Dimitri Glazkov <dglazkov@chromium.org>
Reviewed by Darin Adler.
case DocumentBoundary:
// FIXME: implement all of the above?
pos = modifyExtendingForward(granularity);
+ break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
}
else
pos = endOfDocument(pos);
break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
case LineBoundary:
pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock());
break;
+ case WebKitVisualWordGranularity:
+ pos = rightWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
+ break;
}
return pos;
}
else
pos = endOfDocument(pos);
break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
}
case ParagraphBoundary:
case DocumentBoundary:
pos = modifyExtendingBackward(granularity);
+ break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
}
else
pos = startOfDocument(pos);
break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
}
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true);
break;
case WordGranularity:
+ pos = leftWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
+ break;
case SentenceGranularity:
case LineGranularity:
case ParagraphGranularity:
case LineBoundary:
pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock());
break;
+ case WebKitVisualWordGranularity:
+ pos = leftWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity()));
+ break;
}
return pos;
}
else
pos = startOfDocument(pos);
break;
+ case WebKitVisualWordGranularity:
+ break;
}
return pos;
}
SentenceBoundary,
LineBoundary,
ParagraphBoundary,
- DocumentBoundary
+ DocumentBoundary,
+ // FIXME: this is added temporarily for experiment with visually move
+ // caret by wordGranularity. Once all patches are landed, it should be removed.
+ WebKitVisualWordGranularity
};
}
m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent();
m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent();
break;
+ case WebKitVisualWordGranularity:
+ break;
}
// Make sure we do not have a dangling start or end.
#include "TextBreakIterator.h"
#include "TextIterator.h"
#include "VisiblePosition.h"
+#include "VisibleSelection.h"
#include "htmlediting.h"
#include <wtf/unicode/Unicode.h>
return direction == LTR ? logicalEndOfLine(c) : logicalStartOfLine(c);
}
+static const int invalidOffset = -1;
+
+static VisiblePosition previousWordBreakInBoxInsideBlockWithSameDirectionality(const InlineBox* box, const VisiblePosition& previousWordBreak, int& offsetOfWordBreak)
+{
+ bool hasSeenWordBreakInThisBox = previousWordBreak.isNotNull();
+ // In a LTR block, the word break should be on the left boundary of a word.
+ // In a RTL block, the word break should be on the right boundary of a word.
+ // Because nextWordPosition() returns the word break on the right boundary of the word for LTR text,
+ // we need to use previousWordPosition() to traverse words within the inline boxes from right to left
+ // to find the previous word break (i.e. the first word break on the left). The same applies to RTL text.
+
+ VisiblePosition wordBreak = hasSeenWordBreakInThisBox ? previousWordBreak : Position(box->renderer()->node(), box->caretMaxOffset(), Position::PositionIsOffsetInAnchor);
+
+ // FIXME: handle multi-spaces (http://webkit.org/b/57543).
+
+ wordBreak = previousWordPosition(wordBreak);
+ if (previousWordBreak == wordBreak)
+ return VisiblePosition();
+
+ InlineBox* boxContainingPreviousWordBreak;
+ wordBreak.getInlineBoxAndOffset(boxContainingPreviousWordBreak, offsetOfWordBreak);
+ if (boxContainingPreviousWordBreak != box)
+ return VisiblePosition();
+ return wordBreak;
+}
+
+static VisiblePosition previousWordBreakInBox(const InlineBox* box, int offset, TextDirection blockDirection)
+{
+ int offsetOfWordBreak = 0;
+ VisiblePosition wordBreak;
+ while (true) {
+ if (box->direction() == blockDirection)
+ wordBreak = previousWordBreakInBoxInsideBlockWithSameDirectionality(box, wordBreak, offsetOfWordBreak);
+ // FIXME: Implement the 'else' case when the box direction is not equal to the block direction.
+ if (wordBreak.isNull())
+ break;
+ if (offset == invalidOffset || offsetOfWordBreak != offset)
+ return wordBreak;
+ }
+ return VisiblePosition();
+}
+
+static VisiblePosition leftWordBoundary(const InlineBox* box, int offset, TextDirection blockDirection)
+{
+ VisiblePosition wordBreak;
+ for (const InlineBox* adjacentBox = box; adjacentBox; adjacentBox = adjacentBox->prevLeafChild()) {
+ if (blockDirection == LTR)
+ wordBreak = previousWordBreakInBox(adjacentBox, adjacentBox == box ? offset : invalidOffset, blockDirection);
+ // FIXME: Implement the "else" case.
+ if (wordBreak.isNotNull())
+ return wordBreak;
+ }
+ return VisiblePosition();
+}
+
+static VisiblePosition rightWordBoundary(const InlineBox* box, int offset, TextDirection blockDirection)
+{
+
+ VisiblePosition wordBreak;
+ for (const InlineBox* adjacentBox = box; adjacentBox; adjacentBox = adjacentBox->nextLeafChild()) {
+ if (blockDirection == RTL)
+ wordBreak = previousWordBreakInBox(adjacentBox, adjacentBox == box ? offset : invalidOffset, blockDirection);
+ // FIXME: Implement the "else" case.
+ if (!wordBreak.isNull())
+ return wordBreak;
+ }
+ return VisiblePosition();
+}
+
+VisiblePosition leftWordPosition(const VisiblePosition& visiblePosition)
+{
+ InlineBox* box;
+ int offset;
+ visiblePosition.getInlineBoxAndOffset(box, offset);
+ TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent());
+
+ // FIXME: If the box's directionality is the same as that of the enclosing block, when the offset is at the box boundary
+ // and the direction is towards inside the box, do I still need to make it a special case? For example, a LTR box inside a LTR block,
+ // when offset is at box's caretMinOffset and the direction is DirectionRight, should it be taken care as a general case?
+ if (offset == box->caretLeftmostOffset())
+ return leftWordBoundary(box->prevLeafChild(), invalidOffset, blockDirection);
+ if (offset == box->caretRightmostOffset())
+ return leftWordBoundary(box, offset, blockDirection);
+
+ // FIXME: Not implemented.
+ return VisiblePosition();
+}
+
+VisiblePosition rightWordPosition(const VisiblePosition& visiblePosition)
+{
+ InlineBox* box;
+ int offset;
+ visiblePosition.getInlineBoxAndOffset(box, offset);
+ TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent());
+
+ if (offset == box->caretLeftmostOffset())
+ return rightWordBoundary(box, offset, blockDirection);
+ if (offset == box->caretRightmostOffset())
+ return rightWordBoundary(box->nextLeafChild(), -1, blockDirection);
+
+ // FIXME: Not implemented.
+ return VisiblePosition();
+}
+
}
VisiblePosition endOfWord(const VisiblePosition &, EWordSide = RightWordIfOnBoundary);
VisiblePosition previousWordPosition(const VisiblePosition &);
VisiblePosition nextWordPosition(const VisiblePosition &);
+VisiblePosition rightWordPosition(const VisiblePosition&);
+VisiblePosition leftWordPosition(const VisiblePosition&);
// sentences
VisiblePosition startOfSentence(const VisiblePosition &);
granularity = ParagraphBoundary;
else if (equalIgnoringCase(granularityString, "documentboundary"))
granularity = DocumentBoundary;
+ else if (equalIgnoringCase(granularityString, "-webkit-visual-word"))
+ granularity = WebKitVisualWordGranularity;
else
return;