blob: 5118161d962eca80b8bdf4370a17d17ab90829a0 [file] [log] [blame]
// 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