summaryrefslogtreecommitdiff
path: root/Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp')
-rw-r--r--Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp266
1 files changed, 266 insertions, 0 deletions
diff --git a/Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp b/Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp
new file mode 100644
index 000000000..cfd59c3ed
--- /dev/null
+++ b/Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2015 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "SimpleLineLayoutTextFragmentIterator.h"
+
+#include "Hyphenation.h"
+#include "RenderBlockFlow.h"
+#include "RenderChildIterator.h"
+#include "SimpleLineLayoutFlowContents.h"
+
+namespace WebCore {
+namespace SimpleLineLayout {
+
+TextFragmentIterator::Style::Style(const RenderStyle& style, bool useSimplifiedTextMeasuring)
+ : font(style.fontCascade())
+ , textAlign(style.textAlign())
+ , hasKerningOrLigatures(font.enableKerning() || font.requiresShaping())
+ , collapseWhitespace(style.collapseWhiteSpace())
+ , preserveNewline(style.preserveNewline())
+ , wrapLines(style.autoWrap())
+ , breakAnyWordOnOverflow(style.wordBreak() == BreakAllWordBreak && wrapLines)
+ , breakFirstWordOnOverflow(breakAnyWordOnOverflow || (style.breakWords() && (wrapLines || preserveNewline)))
+ , breakNBSP(wrapLines && style.nbspMode() == SPACE)
+ , keepAllWordsForCJK(style.wordBreak() == KeepAllWordBreak)
+ , wordSpacing(font.wordSpacing())
+ , tabWidth(collapseWhitespace ? 0 : style.tabSize())
+ , shouldHyphenate(style.hyphens() == HyphensAuto && canHyphenate(style.locale()))
+ , hyphenStringWidth(shouldHyphenate ? (useSimplifiedTextMeasuring ? font.widthForSimpleText(style.hyphenString()) : font.width(TextRun(style.hyphenString()))) : 0)
+ , hyphenLimitBefore(style.hyphenationLimitBefore() < 0 ? 2 : style.hyphenationLimitBefore())
+ , hyphenLimitAfter(style.hyphenationLimitAfter() < 0 ? 2 : style.hyphenationLimitAfter())
+ , locale(style.locale())
+{
+ if (style.hyphenationLimitLines() > -1)
+ hyphenLimitLines = style.hyphenationLimitLines();
+}
+
+TextFragmentIterator::TextFragmentIterator(const RenderBlockFlow& flow)
+ : m_flowContents(flow)
+ , m_currentSegment(m_flowContents.begin())
+ , m_lineBreakIterator(m_currentSegment->text, flow.style().locale())
+ , m_style(flow.style(), m_currentSegment->canUseSimplifiedTextMeasuring)
+{
+}
+
+TextFragmentIterator::TextFragment TextFragmentIterator::nextTextFragment(float xPosition)
+{
+ TextFragmentIterator::TextFragment nextFragment = findNextTextFragment(xPosition);
+ m_atEndOfSegment = (m_currentSegment == m_flowContents.end()) || (m_position == m_currentSegment->end);
+ return nextFragment;
+}
+
+TextFragmentIterator::TextFragment TextFragmentIterator::findNextTextFragment(float xPosition)
+{
+ // A fragment can either be
+ // 1. line break when <br> is present or preserveNewline is on (not considered as whitespace) or
+ // 2. whitespace (collasped, non-collapsed multi or single) or
+ // 3. non-whitespace characters.
+ // 4. content end.
+ ASSERT(m_currentSegment != m_flowContents.end());
+ unsigned startPosition = m_position;
+ if (m_atEndOfSegment)
+ ++m_currentSegment;
+
+ if (m_currentSegment == m_flowContents.end())
+ return TextFragment(startPosition, startPosition, 0, TextFragment::ContentEnd);
+ if (isHardLineBreak(m_currentSegment))
+ return TextFragment(startPosition, startPosition, 0, TextFragment::HardLineBreak);
+ if (isSoftLineBreak(startPosition)) {
+ unsigned endPosition = ++m_position;
+ return TextFragment(startPosition, endPosition, 0, TextFragment::SoftLineBreak);
+ }
+ float width = 0;
+ bool overlappingFragment = false;
+ unsigned endPosition = skipToNextPosition(PositionType::NonWhitespace, startPosition, width, xPosition, overlappingFragment);
+ unsigned segmentEndPosition = m_currentSegment->end;
+ ASSERT(startPosition <= endPosition);
+ if (startPosition < endPosition) {
+ bool multipleWhitespace = startPosition + 1 < endPosition;
+ bool isCollapsed = multipleWhitespace && m_style.collapseWhitespace;
+ m_position = endPosition;
+ return TextFragment(startPosition, endPosition, width, TextFragment::Whitespace, endPosition == segmentEndPosition, false, isCollapsed, m_style.collapseWhitespace);
+ }
+ endPosition = skipToNextPosition(PositionType::Breakable, startPosition, width, xPosition, overlappingFragment);
+ m_position = endPosition;
+ return TextFragment(startPosition, endPosition, width, TextFragment::NonWhitespace, endPosition == segmentEndPosition, overlappingFragment, false, false);
+}
+
+void TextFragmentIterator::revertToEndOfFragment(const TextFragment& fragment)
+{
+ ASSERT(m_position >= fragment.end());
+ while (m_currentSegment->start > fragment.end())
+ --m_currentSegment;
+ // TODO: It reverts to the last fragment on the same position, but that's ok for now as we don't need to
+ // differentiate multiple renderers on the same position.
+ m_position = fragment.end();
+ m_atEndOfSegment = false;
+}
+
+static inline unsigned nextBreakablePositionInSegment(LazyLineBreakIterator& lineBreakIterator, unsigned startPosition, bool breakNBSP, bool keepAllWordsForCJK)
+{
+ if (keepAllWordsForCJK) {
+ if (breakNBSP)
+ return nextBreakablePositionKeepingAllWords(lineBreakIterator, startPosition);
+ return nextBreakablePositionKeepingAllWordsIgnoringNBSP(lineBreakIterator, startPosition);
+ }
+
+ if (lineBreakIterator.mode() == LineBreakIteratorMode::Default) {
+ if (breakNBSP)
+ return WebCore::nextBreakablePosition(lineBreakIterator, startPosition);
+ return nextBreakablePositionIgnoringNBSP(lineBreakIterator, startPosition);
+ }
+
+ if (breakNBSP)
+ return nextBreakablePositionWithoutShortcut(lineBreakIterator, startPosition);
+ return nextBreakablePositionIgnoringNBSPWithoutShortcut(lineBreakIterator, startPosition);
+}
+
+unsigned TextFragmentIterator::nextBreakablePosition(const FlowContents::Segment& segment, unsigned startPosition)
+{
+ ASSERT(startPosition < segment.end);
+ StringView currentText = m_lineBreakIterator.stringView();
+ StringView segmentText = StringView(segment.text);
+ if (segmentText != currentText) {
+ unsigned textLength = currentText.length();
+ UChar lastCharacter = textLength > 0 ? currentText[textLength - 1] : 0;
+ UChar secondToLastCharacter = textLength > 1 ? currentText[textLength - 2] : 0;
+ m_lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
+ m_lineBreakIterator.resetStringAndReleaseIterator(segment.text, m_style.locale, LineBreakIteratorMode::Default);
+ }
+ return segment.toRenderPosition(nextBreakablePositionInSegment(m_lineBreakIterator, segment.toSegmentPosition(startPosition), m_style.breakNBSP, m_style.keepAllWordsForCJK));
+}
+
+unsigned TextFragmentIterator::nextNonWhitespacePosition(const FlowContents::Segment& segment, unsigned startPosition)
+{
+ ASSERT(startPosition < segment.end);
+ unsigned position = startPosition;
+ for (; position < segment.end; ++position) {
+ auto character = segment.text[segment.toSegmentPosition(position)];
+ bool isWhitespace = character == ' ' || character == '\t' || (!m_style.preserveNewline && character == '\n');
+ if (!isWhitespace)
+ return position;
+ }
+ return position;
+}
+
+std::optional<unsigned> TextFragmentIterator::lastHyphenPosition(const TextFragmentIterator::TextFragment& run, unsigned before) const
+{
+ ASSERT(run.start() < before);
+ auto& segment = *m_currentSegment;
+ ASSERT(segment.start <= before && before <= segment.end);
+ ASSERT(is<RenderText>(segment.renderer));
+ if (!m_style.shouldHyphenate || run.type() != TextFragment::NonWhitespace)
+ return std::nullopt;
+ // Check if there are enough characters in the run.
+ unsigned runLength = run.end() - run.start();
+ if (m_style.hyphenLimitBefore >= runLength || m_style.hyphenLimitAfter >= runLength || m_style.hyphenLimitBefore + m_style.hyphenLimitAfter > runLength)
+ return std::nullopt;
+ auto runStart = segment.toSegmentPosition(run.start());
+ auto beforeIndex = segment.toSegmentPosition(before) - runStart;
+ if (beforeIndex <= m_style.hyphenLimitBefore)
+ return std::nullopt;
+ // Adjust before index to accommodate the limit-after value (this is the last potential hyphen location).
+ beforeIndex = std::min(beforeIndex, runLength - m_style.hyphenLimitAfter + 1);
+ auto substringForHyphenation = StringView(segment.text).substring(runStart, run.end() - run.start());
+ auto hyphenLocation = lastHyphenLocation(substringForHyphenation, beforeIndex, m_style.locale);
+ // Check if there are enough characters before and after the hyphen.
+ if (hyphenLocation && hyphenLocation >= m_style.hyphenLimitBefore && m_style.hyphenLimitAfter <= (runLength - hyphenLocation))
+ return segment.toRenderPosition(hyphenLocation + runStart);
+ return std::nullopt;
+}
+
+unsigned TextFragmentIterator::skipToNextPosition(PositionType positionType, unsigned startPosition, float& width, float xPosition, bool& overlappingFragment)
+{
+ overlappingFragment = false;
+ unsigned currentPosition = startPosition;
+ unsigned nextPosition = currentPosition;
+ // Collapsed whitespace has constant width. Do not measure it.
+ if (positionType == NonWhitespace)
+ nextPosition = nextNonWhitespacePosition(*m_currentSegment, currentPosition);
+ else if (positionType == Breakable) {
+ nextPosition = nextBreakablePosition(*m_currentSegment, currentPosition);
+ // nextBreakablePosition returns the same position for certain characters such as hyphens. Call next again with modified position unless we are at the end of the segment.
+ bool skipCurrentPosition = nextPosition == currentPosition;
+ if (skipCurrentPosition) {
+ // When we are skipping the last character in the segment, just move to the end of the segment and we'll check the next segment whether it is an overlapping fragment.
+ ASSERT(currentPosition < m_currentSegment->end);
+ if (currentPosition == m_currentSegment->end - 1)
+ nextPosition = m_currentSegment->end;
+ else
+ nextPosition = nextBreakablePosition(*m_currentSegment, currentPosition + 1);
+ }
+ // We need to know whether the word actually finishes at the end of this renderer or not.
+ if (nextPosition == m_currentSegment->end) {
+ const auto nextSegment = m_currentSegment + 1;
+ if (nextSegment != m_flowContents.end() && !isHardLineBreak(nextSegment))
+ overlappingFragment = nextPosition < nextBreakablePosition(*nextSegment, nextPosition);
+ }
+ }
+ width = 0;
+ if (nextPosition == currentPosition)
+ return currentPosition;
+ // Both non-collapsed whitespace and non-whitespace runs need to be measured.
+ bool measureText = positionType != NonWhitespace || !m_style.collapseWhitespace;
+ if (measureText)
+ width = this->textWidth(currentPosition, nextPosition, xPosition);
+ else if (startPosition < nextPosition)
+ width = m_style.font.spaceWidth() + m_style.wordSpacing;
+ return nextPosition;
+}
+
+float TextFragmentIterator::textWidth(unsigned from, unsigned to, float xPosition) const
+{
+ auto& segment = *m_currentSegment;
+ ASSERT(segment.start <= from && from <= segment.end && segment.start <= to && to <= segment.end);
+ ASSERT(is<RenderText>(segment.renderer));
+ if (!m_style.font.size() || from == to)
+ return 0;
+
+ unsigned segmentFrom = segment.toSegmentPosition(from);
+ unsigned segmentTo = segment.toSegmentPosition(to);
+ if (m_style.font.isFixedPitch())
+ return downcast<RenderText>(segment.renderer).width(segmentFrom, segmentTo - segmentFrom, m_style.font, xPosition, nullptr, nullptr);
+
+ bool measureWithEndSpace = m_style.hasKerningOrLigatures && m_style.collapseWhitespace
+ && segmentTo < segment.text.length() && segment.text[segmentTo] == ' ';
+ if (measureWithEndSpace)
+ ++segmentTo;
+ float width = 0;
+ if (segment.canUseSimplifiedTextMeasuring)
+ width = m_style.font.widthForSimpleText(StringView(segment.text).substring(segmentFrom, segmentTo - segmentFrom));
+ else {
+ TextRun run(StringView(segment.text).substring(segmentFrom, segmentTo - segmentFrom), xPosition);
+ if (m_style.tabWidth)
+ run.setTabSize(true, m_style.tabWidth);
+ width = m_style.font.width(run);
+ }
+ if (measureWithEndSpace)
+ width -= (m_style.font.spaceWidth() + m_style.wordSpacing);
+ return std::max<float>(0, width);
+}
+
+}
+}