| // Copyright 2015 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/layout/paragraph.h" |
| |
| #include "base/i18n/char_iterator.h" |
| #include "cobalt/base/unicode/character_values.h" |
| |
| #include "third_party/icu/source/common/unicode/char16ptr.h" |
| #include "third_party/icu/source/common/unicode/ubidi.h" |
| |
| namespace cobalt { |
| namespace layout { |
| |
| namespace { |
| |
| int ConvertBaseDirectionToBidiLevel(BaseDirection base_direction) { |
| return base_direction == kRightToLeftBaseDirection ? 1 : 0; |
| } |
| |
| } // namespace |
| |
| Paragraph::Paragraph( |
| const icu::Locale& locale, BaseDirection base_direction, |
| const DirectionalFormattingStack& directional_formatting_stack, |
| icu::BreakIterator* line_break_iterator, |
| icu::BreakIterator* character_break_iterator) |
| : locale_(locale), |
| base_direction_(base_direction), |
| directional_formatting_stack_(directional_formatting_stack), |
| line_break_iterator_(line_break_iterator), |
| character_break_iterator_(character_break_iterator), |
| is_closed_(false), |
| last_retrieved_run_index_(0) { |
| DCHECK(line_break_iterator_); |
| DCHECK(character_break_iterator_); |
| |
| level_runs_.push_back( |
| BidiLevelRun(0, ConvertBaseDirectionToBidiLevel(base_direction))); |
| |
| // Walk through the passed in directional formatting stack and add each |
| // formatting to the text. This allows a paragraph to continue the directional |
| // state of a prior paragraph. |
| DCHECK(unicode_text_.isEmpty()); |
| for (size_t i = 0; i < directional_formatting_stack_.size(); ++i) { |
| if (directional_formatting_stack_[i] == kRightToLeftDirectionalIsolate) { |
| unicode_text_ += base::unicode::kRightToLeftIsolateCharacter; |
| } else { |
| unicode_text_ += base::unicode::kLeftToRightIsolateCharacter; |
| } |
| } |
| } |
| |
| int32 Paragraph::AppendUtf8String(const std::string& utf8_string) { |
| return AppendUtf8String(utf8_string, kNoTextTransform); |
| } |
| |
| int32 Paragraph::AppendUtf8String(const std::string& utf8_string, |
| TextTransform transform) { |
| int32 start_position = GetTextEndPosition(); |
| DCHECK(!is_closed_); |
| if (!is_closed_) { |
| icu::UnicodeString unicode_string = |
| icu::UnicodeString::fromUTF8(utf8_string); |
| if (transform == kUppercaseTextTransform) { |
| unicode_string.toUpper(locale_); |
| } |
| |
| unicode_text_ += unicode_string; |
| } |
| |
| return start_position; |
| } |
| |
| int32 Paragraph::AppendCodePoint(CodePoint code_point) { |
| int32 start_position = GetTextEndPosition(); |
| DCHECK(!is_closed_); |
| if (!is_closed_) { |
| switch (code_point) { |
| case kLeftToRightIsolateCodePoint: |
| // If this is a directional isolate that is being added, then add it |
| // to the directional formatting stack. This guarantees that a |
| // corresponding pop directional isolate will later be added to the |
| // text and allows later paragraphs to copy the directional state. |
| // http://unicode.org/reports/tr9/#Explicit_Directional_Isolates |
| directional_formatting_stack_.push_back(kLeftToRightDirectionalIsolate); |
| unicode_text_ += base::unicode::kLeftToRightIsolateCharacter; |
| break; |
| case kLineFeedCodePoint: |
| unicode_text_ += base::unicode::kNewLineCharacter; |
| break; |
| case kNoBreakSpaceCodePoint: |
| unicode_text_ += base::unicode::kNoBreakSpaceCharacter; |
| break; |
| case kObjectReplacementCharacterCodePoint: |
| unicode_text_ += base::unicode::kObjectReplacementCharacter; |
| break; |
| case kPopDirectionalIsolateCodePoint: |
| directional_formatting_stack_.pop_back(); |
| unicode_text_ += base::unicode::kPopDirectionalIsolateCharacter; |
| break; |
| case kRightToLeftIsolateCodePoint: |
| // If this is a directional isolate that is being added, then add it |
| // to the directional formatting stack. This guarantees that a |
| // corresponding pop directional isolate will later be added to the |
| // text and allows later paragraphs to copy the directional state. |
| // http://unicode.org/reports/tr9/#Explicit_Directional_Isolates |
| directional_formatting_stack_.push_back(kRightToLeftDirectionalIsolate); |
| unicode_text_ += base::unicode::kRightToLeftIsolateCharacter; |
| break; |
| } |
| } |
| return start_position; |
| } |
| |
| bool Paragraph::FindBreakPosition( |
| BaseDirection direction, bool should_attempt_to_wrap, |
| const scoped_refptr<dom::FontList>& used_font, int32 start_position, |
| int32 end_position, LayoutUnit available_width, |
| bool should_collapse_trailing_white_space, bool allow_overflow, |
| Paragraph::BreakPolicy break_policy, int32* break_position, |
| LayoutUnit* break_width) { |
| DCHECK(is_closed_); |
| |
| DCHECK(direction == base_direction_); |
| if (AreInlineAndScriptDirectionsTheSame(direction, start_position) || |
| should_attempt_to_wrap) { |
| *break_position = start_position; |
| } else { |
| *break_position = end_position; |
| } |
| *break_width = LayoutUnit(); |
| |
| // If overflow isn't allowed and there is no available width, then there is |
| // nothing to do. No break position can be found. |
| if (!allow_overflow && available_width <= LayoutUnit()) { |
| return false; |
| } |
| |
| // Normal overflow is not allowed when the break policy is break-word, so |
| // only attempt to find normal break positions if the break policy is normal |
| // or there is width still available. |
| // https://www.w3.org/TR/css-text-3/#overflow-wrap |
| // NOTE: Normal break positions are still found when the break policy is |
| // break-word and overflow has not yet occurred because width calculations are |
| // more accurate between word intervals, as these properly take into complex |
| // shaping. Due to this, calculating the width using word intervals until |
| // reaching the final word that must be broken between grapheme clusters |
| // results in less accumulated width calculation error. |
| if (break_policy == kBreakPolicyNormal || available_width > LayoutUnit()) { |
| // Only allow normal overflow if the policy is |kBreakPolicyNormal|; |
| // otherwise, overflowing via normal breaking would be too greedy in what it |
| // included and overflow wrapping will be attempted via word breaking. |
| bool allow_normal_overflow = |
| allow_overflow && break_policy == kBreakPolicyNormal; |
| |
| // Find the last available normal break position. |break_position| and |
| // |break_width| will be updated with the position of the last available |
| // break position. |
| FindIteratorBreakPosition( |
| direction, should_attempt_to_wrap, used_font, line_break_iterator_, |
| start_position, end_position, available_width, |
| should_collapse_trailing_white_space, allow_normal_overflow, |
| break_position, break_width); |
| } |
| |
| // If break word is the break policy, attempt to break unbreakable "words" at |
| // an arbitrary point, while still maintaining grapheme clusters as |
| // indivisible units. |
| // https://www.w3.org/TR/css3-text/#overflow-wrap |
| if (break_policy == kBreakPolicyBreakWord) { |
| // Only continue allowing overflow if the break position has not moved from |
| // start, meaning that no normal break positions were found. |
| if (AreInlineAndScriptDirectionsTheSame(direction, start_position) || |
| should_attempt_to_wrap) { |
| allow_overflow = allow_overflow && (*break_position == start_position); |
| } else { |
| allow_overflow = allow_overflow && (*break_position == end_position); |
| } |
| |
| // Find the last available break-word break position. |break_position| and |
| // |break_width| will be updated with the position of the last available |
| // break position. The search begins at the location of the last normal |
| // break position that fit within the available width. |
| if (AreInlineAndScriptDirectionsTheSame(direction, start_position) || |
| should_attempt_to_wrap) { |
| FindIteratorBreakPosition(direction, should_attempt_to_wrap, used_font, |
| character_break_iterator_, *break_position, |
| end_position, available_width, false, |
| allow_overflow, break_position, break_width); |
| } else { |
| FindIteratorBreakPosition(direction, should_attempt_to_wrap, used_font, |
| character_break_iterator_, start_position, |
| *break_position, available_width, false, |
| allow_overflow, break_position, break_width); |
| } |
| } |
| |
| // No usable break position was found if the break position has not moved |
| // from the start position. |
| if (AreInlineAndScriptDirectionsTheSame(direction, start_position) || |
| should_attempt_to_wrap) { |
| return *break_position > start_position; |
| } else { |
| return *break_position < end_position; |
| } |
| } |
| |
| int32 Paragraph::GetNextBreakPosition(int32 position, |
| BreakPolicy break_policy) { |
| icu::BreakIterator* break_iterator = GetBreakIterator(break_policy); |
| break_iterator->setText(unicode_text_); |
| return break_iterator->following(position); |
| } |
| |
| int32 Paragraph::GetPreviousBreakPosition(int32 position, |
| BreakPolicy break_policy) { |
| icu::BreakIterator* break_iterator = GetBreakIterator(break_policy); |
| break_iterator->setText(unicode_text_); |
| return break_iterator->preceding(position); |
| } |
| |
| bool Paragraph::IsBreakPosition(int32 position, BreakPolicy break_policy) { |
| icu::BreakIterator* break_iterator = GetBreakIterator(break_policy); |
| break_iterator->setText(unicode_text_); |
| return break_iterator->isBoundary(position) == true; |
| } |
| |
| std::string Paragraph::RetrieveUtf8SubString(int32 start_position, |
| int32 end_position) const { |
| return RetrieveUtf8SubString(start_position, end_position, kLogicalTextOrder); |
| } |
| |
| std::string Paragraph::RetrieveUtf8SubString(int32 start_position, |
| int32 end_position, |
| TextOrder text_order) const { |
| std::string utf8_sub_string; |
| |
| if (start_position < end_position) { |
| icu::UnicodeString unicode_sub_string = |
| unicode_text_.tempSubStringBetween(start_position, end_position); |
| |
| // Odd bidi levels signify RTL directionality. If the text is being |
| // retrieved in visual order and has RTL directionality, then reverse the |
| // text. |
| if (text_order == kVisualTextOrder && IsRTL(start_position)) { |
| unicode_sub_string.reverse(); |
| } |
| |
| unicode_sub_string.toUTF8String(utf8_sub_string); |
| } |
| |
| return utf8_sub_string; |
| } |
| |
| const base::char16* Paragraph::GetTextBuffer() const { |
| return icu::toUCharPtr(unicode_text_.getBuffer()); |
| } |
| |
| const icu::Locale& Paragraph::GetLocale() const { return locale_; } |
| |
| BaseDirection Paragraph::GetDirectionalFormattingStackDirection() const { |
| size_t stack_size = directional_formatting_stack_.size(); |
| if (stack_size > 0) { |
| if (directional_formatting_stack_[stack_size - 1] == |
| kRightToLeftDirectionalIsolate) { |
| return kRightToLeftBaseDirection; |
| } else { |
| return kLeftToRightBaseDirection; |
| } |
| } else { |
| return base_direction_; |
| } |
| } |
| |
| int Paragraph::GetBidiLevel(int32 position) const { |
| return level_runs_[GetRunIndex(position)].level_; |
| } |
| |
| bool Paragraph::IsRTL(int32 position) const { |
| return (GetBidiLevel(position) % 2) == 1; |
| } |
| |
| bool Paragraph::AreInlineAndScriptDirectionsTheSame(BaseDirection direction, |
| int32 position) const { |
| return ((direction == kLeftToRightBaseDirection && !IsRTL(position)) || |
| (direction == kRightToLeftBaseDirection && IsRTL(position))); |
| } |
| |
| bool Paragraph::IsCollapsibleWhiteSpace(int32 position) const { |
| // Only check for the space character. Other collapsible white space |
| // characters will have already been converted into the space characters and |
| // do not need to be checked against. |
| return unicode_text_[position] == base::unicode::kSpaceCharacter; |
| } |
| |
| bool Paragraph::GetNextRunPosition(int32 position, |
| int32* next_run_position) const { |
| size_t next_run_index = GetRunIndex(position) + 1; |
| if (next_run_index >= level_runs_.size()) { |
| return false; |
| } else { |
| *next_run_position = level_runs_[next_run_index].start_position_; |
| return true; |
| } |
| } |
| |
| int32 Paragraph::GetTextEndPosition() const { return unicode_text_.length(); } |
| |
| const Paragraph::DirectionalFormattingStack& |
| Paragraph::GetDirectionalFormattingStack() const { |
| return directional_formatting_stack_; |
| } |
| |
| void Paragraph::Close() { |
| DCHECK(!is_closed_); |
| if (!is_closed_) { |
| // Terminate all of the explicit directional isolates that were previously |
| // added. However, do not clear the stack. A subsequent paragraph may need |
| // to copy it. |
| // http://unicode.org/reports/tr9/#Terminating_Explicit_Directional_Isolates |
| for (size_t count = directional_formatting_stack_.size(); count > 0; |
| --count) { |
| unicode_text_ += base::unicode::kPopDirectionalIsolateCharacter; |
| } |
| |
| is_closed_ = true; |
| GenerateBidiLevelRuns(); |
| } |
| } |
| |
| bool Paragraph::IsClosed() const { return is_closed_; } |
| |
| Paragraph::BreakPolicy Paragraph::GetBreakPolicyFromWrapOpportunityPolicy( |
| WrapOpportunityPolicy wrap_opportunity_policy, |
| bool does_style_allow_break_word) { |
| // Initialize here to prevent a potential compiler warning. |
| BreakPolicy break_policy = kBreakPolicyNormal; |
| switch (wrap_opportunity_policy) { |
| case kWrapOpportunityPolicyNormal: |
| break_policy = kBreakPolicyNormal; |
| break; |
| case kWrapOpportunityPolicyBreakWord: |
| break_policy = kBreakPolicyBreakWord; |
| break; |
| case kWrapOpportunityPolicyBreakWordOrNormal: |
| break_policy = does_style_allow_break_word ? kBreakPolicyBreakWord |
| : kBreakPolicyNormal; |
| break; |
| } |
| return break_policy; |
| } |
| |
| icu::BreakIterator* Paragraph::GetBreakIterator(BreakPolicy break_policy) { |
| return break_policy == kBreakPolicyNormal ? line_break_iterator_ |
| : character_break_iterator_; |
| } |
| |
| void Paragraph::FindIteratorBreakPosition( |
| BaseDirection direction, bool should_attempt_to_wrap, |
| const scoped_refptr<dom::FontList>& used_font, |
| icu::BreakIterator* const break_iterator, int32 start_position, |
| int32 end_position, LayoutUnit available_width, |
| bool should_collapse_trailing_white_space, bool allow_overflow, |
| int32* break_position, LayoutUnit* break_width) { |
| // Iterate through break segments, beginning from the passed in start |
| // position. Continue until TryIncludeSegmentWithinAvailableWidth() returns |
| // false, indicating that no more segments can be included. |
| break_iterator->setText(unicode_text_); |
| if (AreInlineAndScriptDirectionsTheSame(direction, start_position) || |
| should_attempt_to_wrap) { |
| for (int32 segment_end = break_iterator->following(start_position); |
| segment_end != icu::BreakIterator::DONE && segment_end < end_position; |
| segment_end = break_iterator->next()) { |
| if (!TryIncludeSegmentWithinAvailableWidth( |
| direction, should_attempt_to_wrap, used_font, *break_position, |
| segment_end, available_width, |
| should_collapse_trailing_white_space, &allow_overflow, |
| break_position, break_width)) { |
| break; |
| } |
| } |
| } else { |
| for (int32 segment_begin = break_iterator->preceding(end_position); |
| segment_begin != icu::BreakIterator::DONE && |
| segment_begin > start_position; |
| segment_begin = break_iterator->previous()) { |
| if (!TryIncludeSegmentWithinAvailableWidth( |
| direction, should_attempt_to_wrap, used_font, segment_begin, |
| *break_position, available_width, |
| should_collapse_trailing_white_space, &allow_overflow, |
| break_position, break_width)) { |
| break; |
| } |
| } |
| } |
| } |
| |
| bool Paragraph::TryIncludeSegmentWithinAvailableWidth( |
| BaseDirection direction, bool should_attempt_to_wrap, |
| const scoped_refptr<dom::FontList>& used_font, int32 segment_start, |
| int32 segment_end, LayoutUnit available_width, |
| bool should_collapse_trailing_white_space, bool* allow_overflow, |
| int32* break_position, LayoutUnit* break_width) { |
| // Add the width of the segment encountered to the total, until reaching one |
| // that causes the available width to be exceeded. The previous break position |
| // is the last usable one. However, if overflow is allowed and no segment has |
| // been found, then the first overflowing segment is accepted. |
| LayoutUnit segment_width = LayoutUnit(used_font->GetTextWidth( |
| icu::toUCharPtr(unicode_text_.getBuffer()) + segment_start, |
| segment_end - segment_start, IsRTL(segment_start), NULL)); |
| |
| // If trailing white space is being collapsed, then it will not be included |
| // when determining if the segment can fit within the available width. |
| // However, it is still added to |break_width|, as it will impact the |
| // width available to additional segments. |
| LayoutUnit collapsible_trailing_white_space_width = |
| LayoutUnit(should_collapse_trailing_white_space && |
| IsCollapsibleWhiteSpace(segment_end - 1) |
| ? used_font->GetSpaceWidth() |
| : 0); |
| |
| if (!*allow_overflow && |
| *break_width + segment_width - collapsible_trailing_white_space_width > |
| available_width) { |
| return false; |
| } |
| |
| if (AreInlineAndScriptDirectionsTheSame(direction, segment_start) || |
| should_attempt_to_wrap) { |
| *break_position = segment_end; |
| } else { |
| *break_position = segment_start; |
| } |
| *break_width += segment_width; |
| |
| if (*allow_overflow) { |
| *allow_overflow = false; |
| if (*break_width >= available_width) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| |
| // This class guarantees that a UBiDi object is closed when it goes out of |
| // scope. |
| class ScopedUBiDiPtr { |
| public: |
| explicit ScopedUBiDiPtr(UBiDi* ubidi) : ubidi_(ubidi) {} |
| |
| ~ScopedUBiDiPtr() { ubidi_close(ubidi_); } |
| |
| UBiDi* get() const { return ubidi_; } |
| |
| private: |
| UBiDi* ubidi_; |
| }; |
| |
| } // namespace |
| |
| // Should only be called by Close(). |
| void Paragraph::GenerateBidiLevelRuns() { |
| DCHECK(is_closed_); |
| |
| UErrorCode error = U_ZERO_ERROR; |
| |
| // Create a scoped ubidi ptr when opening the text. It guarantees that the |
| // UBiDi object will be closed when it goes out of scope. |
| ScopedUBiDiPtr ubidi(ubidi_openSized(unicode_text_.length(), 0, &error)); |
| if (U_FAILURE(error)) { |
| return; |
| } |
| |
| ubidi_setPara(ubidi.get(), icu::toUCharPtr(unicode_text_.getBuffer()), |
| unicode_text_.length(), |
| UBiDiLevel(ConvertBaseDirectionToBidiLevel(base_direction_)), |
| NULL, &error); |
| if (U_FAILURE(error)) { |
| return; |
| } |
| |
| const int runs = ubidi_countRuns(ubidi.get(), &error); |
| if (U_FAILURE(error)) { |
| return; |
| } |
| |
| level_runs_.clear(); |
| level_runs_.reserve(size_t(runs)); |
| |
| int run_start_position = 0; |
| int run_end_position; |
| UBiDiLevel run_ubidi_level; |
| |
| int32 text_end_position = GetTextEndPosition(); |
| while (run_start_position < text_end_position) { |
| ubidi_getLogicalRun(ubidi.get(), run_start_position, &run_end_position, |
| &run_ubidi_level); |
| level_runs_.push_back( |
| BidiLevelRun(run_start_position, static_cast<int>(run_ubidi_level))); |
| run_start_position = run_end_position; |
| } |
| } |
| |
| size_t Paragraph::GetRunIndex(int32 position) const { |
| DCHECK(is_closed_); |
| |
| // Start the search from the last retrieved index. This is tracked because |
| // the next retrieved run index is almost always either the same as the last |
| // retrieved one or a neighbor of it, so this reduces nearly all calls to only |
| // requiring a couple comparisons. |
| |
| // First iterate up the level run vector, finding the first element that has |
| // a start position larger than the passed in position. The upward iteration |
| // is stopped if the last element is reached. |
| while (last_retrieved_run_index_ < level_runs_.size() - 1 && |
| level_runs_[last_retrieved_run_index_].start_position_ < position) { |
| ++last_retrieved_run_index_; |
| } |
| // Iterate down the level run vector until a start position smaller than or |
| // equal to the passed in position is reached. This run contains the passed in |
| // position. The start position of the first run has a value of 0 and serves |
| // as a sentinel for the loop. |
| while (level_runs_[last_retrieved_run_index_].start_position_ > position) { |
| --last_retrieved_run_index_; |
| } |
| |
| return last_retrieved_run_index_; |
| } |
| |
| } // namespace layout |
| } // namespace cobalt |