1 | /* |
2 | * Copyright (C) 2015 Apple Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * 1. Redistributions of source code must retain the above copyright |
8 | * notice, this list of conditions and the following disclaimer. |
9 | * 2. Redistributions in binary form must reproduce the above copyright |
10 | * notice, this list of conditions and the following disclaimer in the |
11 | * documentation and/or other materials provided with the distribution. |
12 | * |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
23 | * THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "SimpleLineLayoutTextFragmentIterator.h" |
28 | |
29 | #include "FontCascade.h" |
30 | #include "Hyphenation.h" |
31 | #include "RenderBlockFlow.h" |
32 | #include "RenderChildIterator.h" |
33 | #include "SimpleLineLayoutFlowContents.h" |
34 | |
35 | namespace WebCore { |
36 | namespace SimpleLineLayout { |
37 | |
38 | TextFragmentIterator::Style::Style(const RenderStyle& style) |
39 | : font(style.fontCascade()) |
40 | , textAlign(style.textAlign()) |
41 | , hasKerningOrLigatures(font.enableKerning() || font.requiresShaping()) |
42 | , collapseWhitespace(style.collapseWhiteSpace()) |
43 | , preserveNewline(style.preserveNewline()) |
44 | , wrapLines(style.autoWrap()) |
45 | , breakSpaces(style.whiteSpace() == WhiteSpace::BreakSpaces) |
46 | , breakAnyWordOnOverflow(style.wordBreak() == WordBreak::BreakAll && wrapLines) |
47 | , breakWordOnOverflow(style.breakWords() && (wrapLines || preserveNewline)) |
48 | , breakFirstWordOnOverflow(breakAnyWordOnOverflow || breakWordOnOverflow) |
49 | , breakNBSP(wrapLines && style.nbspMode() == NBSPMode::Space) |
50 | , keepAllWordsForCJK(style.wordBreak() == WordBreak::KeepAll) |
51 | , wordSpacing(font.wordSpacing()) |
52 | , tabWidth(collapseWhitespace ? 0 : style.tabSize()) |
53 | , shouldHyphenate(style.hyphens() == Hyphens::Auto && canHyphenate(style.locale())) |
54 | , hyphenStringWidth(shouldHyphenate ? font.width(TextRun(String(style.hyphenString()))) : 0) |
55 | , hyphenLimitBefore(style.hyphenationLimitBefore() < 0 ? 2 : style.hyphenationLimitBefore()) |
56 | , hyphenLimitAfter(style.hyphenationLimitAfter() < 0 ? 2 : style.hyphenationLimitAfter()) |
57 | , locale(style.locale()) |
58 | { |
59 | if (style.hyphenationLimitLines() > -1) |
60 | hyphenLimitLines = style.hyphenationLimitLines(); |
61 | } |
62 | |
63 | TextFragmentIterator::TextFragmentIterator(const RenderBlockFlow& flow) |
64 | : m_flowContents(flow) |
65 | , m_currentSegment(m_flowContents.begin()) |
66 | , m_lineBreakIterator(m_currentSegment->text, flow.style().locale()) |
67 | , m_style(flow.style()) |
68 | { |
69 | } |
70 | |
71 | TextFragmentIterator::TextFragment TextFragmentIterator::nextTextFragment(float xPosition) |
72 | { |
73 | TextFragmentIterator::TextFragment nextFragment = findNextTextFragment(xPosition); |
74 | m_atEndOfSegment = (m_currentSegment == m_flowContents.end()) || (m_position == m_currentSegment->end); |
75 | return nextFragment; |
76 | } |
77 | |
78 | TextFragmentIterator::TextFragment TextFragmentIterator::findNextTextFragment(float xPosition) |
79 | { |
80 | // A fragment can either be |
81 | // 1. line break when <br> is present or preserveNewline is on (not considered as whitespace) or |
82 | // 2. whitespace (collasped, non-collapsed multi or single) or |
83 | // 3. non-whitespace characters. |
84 | // 4. content end. |
85 | ASSERT(m_currentSegment != m_flowContents.end()); |
86 | unsigned startPosition = m_position; |
87 | if (m_atEndOfSegment) |
88 | ++m_currentSegment; |
89 | |
90 | if (m_currentSegment == m_flowContents.end()) |
91 | return TextFragment(startPosition, startPosition, 0, TextFragment::ContentEnd); |
92 | if (isHardLineBreak(m_currentSegment)) |
93 | return TextFragment(startPosition, startPosition, 0, TextFragment::HardLineBreak); |
94 | if (isSoftLineBreak(startPosition)) { |
95 | unsigned endPosition = ++m_position; |
96 | return TextFragment(startPosition, endPosition, 0, TextFragment::SoftLineBreak); |
97 | } |
98 | float width = 0; |
99 | bool overlappingFragment = false; |
100 | unsigned endPosition = skipToNextPosition(PositionType::NonWhitespace, startPosition, width, xPosition, overlappingFragment); |
101 | unsigned segmentEndPosition = m_currentSegment->end; |
102 | ASSERT(startPosition <= endPosition); |
103 | if (startPosition < endPosition) { |
104 | bool multipleWhitespace = startPosition + 1 < endPosition; |
105 | bool isCollapsed = multipleWhitespace && m_style.collapseWhitespace; |
106 | m_position = endPosition; |
107 | return TextFragment(startPosition, endPosition, width, TextFragment::Whitespace, endPosition == segmentEndPosition, false, isCollapsed, m_style.collapseWhitespace); |
108 | } |
109 | endPosition = skipToNextPosition(PositionType::Breakable, startPosition, width, xPosition, overlappingFragment); |
110 | m_position = endPosition; |
111 | return TextFragment(startPosition, endPosition, width, TextFragment::NonWhitespace, endPosition == segmentEndPosition, overlappingFragment, false, false); |
112 | } |
113 | |
114 | void TextFragmentIterator::revertToEndOfFragment(const TextFragment& fragment) |
115 | { |
116 | ASSERT(m_position >= fragment.end()); |
117 | while (m_currentSegment->start > fragment.end()) |
118 | --m_currentSegment; |
119 | // TODO: It reverts to the last fragment on the same position, but that's ok for now as we don't need to |
120 | // differentiate multiple renderers on the same position. |
121 | m_position = fragment.end(); |
122 | m_atEndOfSegment = false; |
123 | } |
124 | |
125 | static inline unsigned nextBreakablePositionInSegment(LazyLineBreakIterator& lineBreakIterator, unsigned startPosition, bool breakNBSP, bool keepAllWordsForCJK) |
126 | { |
127 | if (keepAllWordsForCJK) { |
128 | if (breakNBSP) |
129 | return nextBreakablePositionKeepingAllWords(lineBreakIterator, startPosition); |
130 | return nextBreakablePositionKeepingAllWordsIgnoringNBSP(lineBreakIterator, startPosition); |
131 | } |
132 | |
133 | if (lineBreakIterator.mode() == LineBreakIteratorMode::Default) { |
134 | if (breakNBSP) |
135 | return WebCore::nextBreakablePosition(lineBreakIterator, startPosition); |
136 | return nextBreakablePositionIgnoringNBSP(lineBreakIterator, startPosition); |
137 | } |
138 | |
139 | if (breakNBSP) |
140 | return nextBreakablePositionWithoutShortcut(lineBreakIterator, startPosition); |
141 | return nextBreakablePositionIgnoringNBSPWithoutShortcut(lineBreakIterator, startPosition); |
142 | } |
143 | |
144 | unsigned TextFragmentIterator::nextBreakablePosition(const FlowContents::Segment& segment, unsigned startPosition) |
145 | { |
146 | ASSERT(startPosition < segment.end); |
147 | StringView currentText = m_lineBreakIterator.stringView(); |
148 | StringView segmentText = StringView(segment.text); |
149 | if (segmentText != currentText) { |
150 | unsigned textLength = currentText.length(); |
151 | UChar lastCharacter = textLength > 0 ? currentText[textLength - 1] : 0; |
152 | UChar secondToLastCharacter = textLength > 1 ? currentText[textLength - 2] : 0; |
153 | m_lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter); |
154 | m_lineBreakIterator.resetStringAndReleaseIterator(segment.text, m_style.locale, LineBreakIteratorMode::Default); |
155 | } |
156 | return segment.toRenderPosition(nextBreakablePositionInSegment(m_lineBreakIterator, segment.toSegmentPosition(startPosition), m_style.breakNBSP, m_style.keepAllWordsForCJK)); |
157 | } |
158 | |
159 | unsigned TextFragmentIterator::nextNonWhitespacePosition(const FlowContents::Segment& segment, unsigned startPosition) |
160 | { |
161 | ASSERT(startPosition < segment.end); |
162 | unsigned position = startPosition; |
163 | for (; position < segment.end; ++position) { |
164 | auto character = segment.text[segment.toSegmentPosition(position)]; |
165 | bool isWhitespace = character == ' ' || character == '\t' || (!m_style.preserveNewline && character == '\n'); |
166 | if (!isWhitespace) |
167 | return position; |
168 | } |
169 | return position; |
170 | } |
171 | |
172 | Optional<unsigned> TextFragmentIterator::lastHyphenPosition(const TextFragmentIterator::TextFragment& run, unsigned before) const |
173 | { |
174 | ASSERT(run.start() < before); |
175 | auto& segment = *m_currentSegment; |
176 | ASSERT(segment.start <= before && before <= segment.end); |
177 | ASSERT(is<RenderText>(segment.renderer)); |
178 | if (!m_style.shouldHyphenate || run.type() != TextFragment::NonWhitespace) |
179 | return WTF::nullopt; |
180 | // Check if there are enough characters in the run. |
181 | unsigned runLength = run.end() - run.start(); |
182 | if (m_style.hyphenLimitBefore >= runLength || m_style.hyphenLimitAfter >= runLength || m_style.hyphenLimitBefore + m_style.hyphenLimitAfter > runLength) |
183 | return WTF::nullopt; |
184 | auto runStart = segment.toSegmentPosition(run.start()); |
185 | auto beforeIndex = segment.toSegmentPosition(before) - runStart; |
186 | if (beforeIndex <= m_style.hyphenLimitBefore) |
187 | return WTF::nullopt; |
188 | // Adjust before index to accommodate the limit-after value (this is the last potential hyphen location). |
189 | beforeIndex = std::min(beforeIndex, runLength - m_style.hyphenLimitAfter + 1); |
190 | auto substringForHyphenation = StringView(segment.text).substring(runStart, run.end() - run.start()); |
191 | auto hyphenLocation = lastHyphenLocation(substringForHyphenation, beforeIndex, m_style.locale); |
192 | // Check if there are enough characters before and after the hyphen. |
193 | if (hyphenLocation && hyphenLocation >= m_style.hyphenLimitBefore && m_style.hyphenLimitAfter <= (runLength - hyphenLocation)) |
194 | return segment.toRenderPosition(hyphenLocation + runStart); |
195 | return WTF::nullopt; |
196 | } |
197 | |
198 | unsigned TextFragmentIterator::skipToNextPosition(PositionType positionType, unsigned startPosition, float& width, float xPosition, bool& overlappingFragment) |
199 | { |
200 | overlappingFragment = false; |
201 | unsigned currentPosition = startPosition; |
202 | unsigned nextPosition = currentPosition; |
203 | // Collapsed whitespace has constant width. Do not measure it. |
204 | if (positionType == NonWhitespace) |
205 | nextPosition = nextNonWhitespacePosition(*m_currentSegment, currentPosition); |
206 | else if (positionType == Breakable) { |
207 | nextPosition = nextBreakablePosition(*m_currentSegment, currentPosition); |
208 | // 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. |
209 | bool skipCurrentPosition = nextPosition == currentPosition; |
210 | if (skipCurrentPosition) { |
211 | // 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. |
212 | ASSERT(currentPosition < m_currentSegment->end); |
213 | if (currentPosition == m_currentSegment->end - 1) |
214 | nextPosition = m_currentSegment->end; |
215 | else |
216 | nextPosition = nextBreakablePosition(*m_currentSegment, currentPosition + 1); |
217 | } |
218 | // We need to know whether the word actually finishes at the end of this renderer or not. |
219 | if (nextPosition == m_currentSegment->end) { |
220 | const auto nextSegment = m_currentSegment + 1; |
221 | if (nextSegment != m_flowContents.end() && !isHardLineBreak(nextSegment)) |
222 | overlappingFragment = nextPosition < nextBreakablePosition(*nextSegment, nextPosition); |
223 | } |
224 | } |
225 | width = 0; |
226 | if (nextPosition == currentPosition) |
227 | return currentPosition; |
228 | // Both non-collapsed whitespace and non-whitespace runs need to be measured. |
229 | bool measureText = positionType != NonWhitespace || !m_style.collapseWhitespace; |
230 | if (measureText) |
231 | width = this->textWidth(currentPosition, nextPosition, xPosition); |
232 | else if (startPosition < nextPosition) |
233 | width = m_style.font.spaceWidth() + m_style.wordSpacing; |
234 | return nextPosition; |
235 | } |
236 | |
237 | float TextFragmentIterator::textWidth(unsigned from, unsigned to, float xPosition) const |
238 | { |
239 | auto& segment = *m_currentSegment; |
240 | ASSERT(segment.start <= from && from <= segment.end && segment.start <= to && to <= segment.end); |
241 | ASSERT(is<RenderText>(segment.renderer)); |
242 | if (!m_style.font.size() || from == to) |
243 | return 0; |
244 | |
245 | unsigned segmentFrom = segment.toSegmentPosition(from); |
246 | unsigned segmentTo = segment.toSegmentPosition(to); |
247 | if (m_style.font.isFixedPitch()) |
248 | return downcast<RenderText>(segment.renderer).width(segmentFrom, segmentTo - segmentFrom, m_style.font, xPosition, nullptr, nullptr); |
249 | |
250 | bool measureWithEndSpace = m_style.hasKerningOrLigatures && m_style.collapseWhitespace |
251 | && segmentTo < segment.text.length() && segment.text[segmentTo] == ' '; |
252 | if (measureWithEndSpace) |
253 | ++segmentTo; |
254 | float width = 0; |
255 | if (segment.canUseSimplifiedTextMeasuring) |
256 | width = m_style.font.widthForSimpleText(StringView(segment.text).substring(segmentFrom, segmentTo - segmentFrom)); |
257 | else { |
258 | TextRun run(StringView(segment.text).substring(segmentFrom, segmentTo - segmentFrom), xPosition); |
259 | if (m_style.tabWidth) |
260 | run.setTabSize(true, m_style.tabWidth); |
261 | width = m_style.font.width(run); |
262 | } |
263 | if (measureWithEndSpace) |
264 | width -= (m_style.font.spaceWidth() + m_style.wordSpacing); |
265 | return std::max<float>(0, width); |
266 | } |
267 | |
268 | } |
269 | } |
270 | |