blob: ad09c7d9723ad1e880f5ea304e508faed1b0f9f5 [file] [log] [blame]
// Copyright 2014 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/box_generator.h"
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/cssom/computed_style.h"
#include "cobalt/cssom/css_computed_style_declaration.h"
#include "cobalt/cssom/css_transition_set.h"
#include "cobalt/cssom/keyword_value.h"
#include "cobalt/cssom/property_definitions.h"
#include "cobalt/cssom/property_value_visitor.h"
#include "cobalt/cssom/url_value.h"
#include "cobalt/dom/html_br_element.h"
#include "cobalt/dom/html_element.h"
#include "cobalt/dom/html_video_element.h"
#include "cobalt/dom/lottie_player.h"
#include "cobalt/dom/text.h"
#include "cobalt/layout/base_direction.h"
#include "cobalt/layout/block_formatting_block_container_box.h"
#include "cobalt/layout/block_level_replaced_box.h"
#include "cobalt/layout/flex_container_box.h"
#include "cobalt/layout/inline_container_box.h"
#include "cobalt/layout/inline_level_replaced_box.h"
#include "cobalt/layout/layout_boxes.h"
#include "cobalt/layout/layout_stat_tracker.h"
#include "cobalt/layout/text_box.h"
#include "cobalt/layout/used_style.h"
#include "cobalt/layout/white_space_processing.h"
#include "cobalt/loader/image/lottie_animation.h"
#include "cobalt/media/base/video_frame_provider.h"
#include "cobalt/render_tree/image.h"
#include "cobalt/web_animations/keyframe_effect_read_only.h"
#include "starboard/decode_target.h"
namespace cobalt {
namespace layout {
using media::VideoFrameProvider;
namespace {
scoped_refptr<render_tree::Image> GetVideoFrame(
const scoped_refptr<VideoFrameProvider>& frame_provider,
render_tree::ResourceProvider* resource_provider) {
TRACE_EVENT0("cobalt::layout", "GetVideoFrame()");
SbDecodeTarget decode_target = frame_provider->GetCurrentSbDecodeTarget();
if (SbDecodeTargetIsValid(decode_target)) {
return resource_provider->CreateImageFromSbDecodeTarget(decode_target);
} else {
DCHECK(frame_provider);
return NULL;
}
}
scoped_refptr<render_tree::Image> GetLottieAnimation(
scoped_refptr<loader::image::Image> lottie_animation) {
TRACE_EVENT0("cobalt::layout", "GetLottieAnimation()");
return base::polymorphic_downcast<loader::image::LottieAnimation*>(
lottie_animation.get())
->animation();
}
} // namespace
BoxGenerator::BoxGenerator(
const scoped_refptr<cssom::CSSComputedStyleDeclaration>&
parent_css_computed_style_declaration,
const scoped_refptr<const web_animations::AnimationSet>& parent_animations,
scoped_refptr<Paragraph>* paragraph, const int dom_element_depth,
const Context* context)
: parent_css_computed_style_declaration_(
parent_css_computed_style_declaration),
parent_animations_(parent_animations),
paragraph_(paragraph),
dom_element_depth_(dom_element_depth),
context_(context) {}
BoxGenerator::~BoxGenerator() {
// Later code assumes that if layout_boxes() is non-null, then it contains
// more than one box. This allows us to avoid some allocations of LayoutBoxes
// objects. We don't need to worry about setting layout_boxes() back to
// null because this should end up being done in html_element.cc when the
// boxes become invalidated.
if (generating_html_element_ && !boxes_.empty()) {
generating_html_element_->set_layout_boxes(
std::unique_ptr<dom::LayoutBoxes>(new LayoutBoxes(std::move(boxes_))));
}
}
void BoxGenerator::Visit(dom::Element* element) {
if (dom_element_depth_ > context_->dom_max_element_depth) {
LOG(WARNING) << "Elements too deep in the DOM tree are ignored in layout.";
return;
}
scoped_refptr<dom::HTMLElement> html_element = element->AsHTMLElement();
if (!html_element) {
return;
}
generating_html_element_ = html_element;
bool partial_layout_is_enabled = true;
#if defined(ENABLE_PARTIAL_LAYOUT_CONTROL)
partial_layout_is_enabled =
html_element->node_document()->partial_layout_is_enabled();
#endif // defined(ENABLE_PARTIAL_LAYOUT_CONTROL)
// If the html element already has layout boxes, we can reuse them.
if (partial_layout_is_enabled && html_element->layout_boxes()) {
LayoutBoxes* layout_boxes =
base::polymorphic_downcast<LayoutBoxes*>(html_element->layout_boxes());
DCHECK(boxes_.empty());
DCHECK(!layout_boxes->boxes().empty());
if (layout_boxes->boxes().front()->GetLevel() == Box::kBlockLevel) {
boxes_ = layout_boxes->boxes();
for (Boxes::const_iterator box_iterator = boxes_.begin();
box_iterator != boxes_.end(); ++box_iterator) {
Box* box = *box_iterator;
do {
box->SetUiNavItem(html_element->GetUiNavItem());
box->InvalidateParent();
box = box->GetSplitSibling();
} while (box != NULL);
}
return;
}
}
scoped_refptr<dom::HTMLVideoElement> video_element =
html_element->AsHTMLVideoElement();
if (video_element) {
VisitVideoElement(video_element);
return;
}
scoped_refptr<dom::HTMLBRElement> br_element =
html_element->AsHTMLBRElement();
if (br_element) {
VisitBrElement(br_element);
return;
}
scoped_refptr<dom::LottiePlayer> lottie_player =
html_element->AsLottiePlayer();
if (lottie_player) {
VisitLottiePlayer(lottie_player);
return;
}
VisitNonReplacedElement(html_element);
}
namespace {
class ReplacedBoxGenerator : public cssom::NotReachedPropertyValueVisitor {
public:
ReplacedBoxGenerator(
const scoped_refptr<cssom::CSSComputedStyleDeclaration>&
css_computed_style_declaration,
const ReplacedBox::ReplaceImageCB& replace_image_cb,
const ReplacedBox::SetBoundsCB& set_bounds_cb,
const scoped_refptr<Paragraph>& paragraph, int32 text_position,
const base::Optional<LayoutUnit>& maybe_intrinsic_width,
const base::Optional<LayoutUnit>& maybe_intrinsic_height,
const base::Optional<float>& maybe_intrinsic_ratio,
const BoxGenerator::Context* context,
base::Optional<ReplacedBox::ReplacedBoxMode> replaced_box_mode,
math::SizeF content_size,
base::Optional<render_tree::LottieAnimation::LottieProperties>
lottie_properties)
: css_computed_style_declaration_(css_computed_style_declaration),
replace_image_cb_(replace_image_cb),
set_bounds_cb_(set_bounds_cb),
paragraph_(paragraph),
text_position_(text_position),
maybe_intrinsic_width_(maybe_intrinsic_width),
maybe_intrinsic_height_(maybe_intrinsic_height),
maybe_intrinsic_ratio_(maybe_intrinsic_ratio),
context_(context),
replaced_box_mode_(replaced_box_mode),
content_size_(content_size),
lottie_properties_(lottie_properties) {}
void VisitKeyword(cssom::KeywordValue* keyword) override;
const scoped_refptr<ReplacedBox>& replaced_box() { return replaced_box_; }
private:
const scoped_refptr<cssom::CSSComputedStyleDeclaration>
css_computed_style_declaration_;
const ReplacedBox::ReplaceImageCB replace_image_cb_;
const ReplacedBox::SetBoundsCB set_bounds_cb_;
const scoped_refptr<Paragraph> paragraph_;
const int32 text_position_;
const base::Optional<LayoutUnit> maybe_intrinsic_width_;
const base::Optional<LayoutUnit> maybe_intrinsic_height_;
const base::Optional<float> maybe_intrinsic_ratio_;
const BoxGenerator::Context* context_;
base::Optional<ReplacedBox::ReplacedBoxMode> replaced_box_mode_;
math::SizeF content_size_;
base::Optional<render_tree::LottieAnimation::LottieProperties>
lottie_properties_;
scoped_refptr<ReplacedBox> replaced_box_;
};
void ReplacedBoxGenerator::VisitKeyword(cssom::KeywordValue* keyword) {
// See https://www.w3.org/TR/CSS21/visuren.html#display-prop.
switch (keyword->value()) {
// Generate a block-level replaced box.
case cssom::KeywordValue::kBlock:
case cssom::KeywordValue::kFlex:
replaced_box_ = WrapRefCounted(new BlockLevelReplacedBox(
css_computed_style_declaration_, replace_image_cb_, set_bounds_cb_,
paragraph_, text_position_, maybe_intrinsic_width_,
maybe_intrinsic_height_, maybe_intrinsic_ratio_,
context_->used_style_provider, replaced_box_mode_, content_size_,
lottie_properties_, context_->layout_stat_tracker));
break;
// Generate an inline-level replaced box. There is no need to distinguish
// between inline replaced elements and inline-block replaced elements
// because their widths, heights, and margins are calculated in the same
// way.
case cssom::KeywordValue::kInline:
case cssom::KeywordValue::kInlineBlock:
case cssom::KeywordValue::kInlineFlex:
replaced_box_ = WrapRefCounted(new InlineLevelReplacedBox(
css_computed_style_declaration_, replace_image_cb_, set_bounds_cb_,
paragraph_, text_position_, maybe_intrinsic_width_,
maybe_intrinsic_height_, maybe_intrinsic_ratio_,
context_->used_style_provider, replaced_box_mode_, content_size_,
lottie_properties_, context_->layout_stat_tracker));
break;
// The element generates no boxes and has no effect on layout.
case cssom::KeywordValue::kNone:
// Leave |replaced_box_| NULL.
break;
case cssom::KeywordValue::kAbsolute:
case cssom::KeywordValue::kAlternate:
case cssom::KeywordValue::kAlternateReverse:
case cssom::KeywordValue::kAuto:
case cssom::KeywordValue::kBackwards:
case cssom::KeywordValue::kBaseline:
case cssom::KeywordValue::kBoth:
case cssom::KeywordValue::kBottom:
case cssom::KeywordValue::kBreakWord:
case cssom::KeywordValue::kCenter:
case cssom::KeywordValue::kClip:
case cssom::KeywordValue::kCollapse:
case cssom::KeywordValue::kColumn:
case cssom::KeywordValue::kColumnReverse:
case cssom::KeywordValue::kContain:
case cssom::KeywordValue::kContent:
case cssom::KeywordValue::kCover:
case cssom::KeywordValue::kCurrentColor:
case cssom::KeywordValue::kCursive:
case cssom::KeywordValue::kEllipsis:
case cssom::KeywordValue::kEnd:
case cssom::KeywordValue::kEquirectangular:
case cssom::KeywordValue::kFantasy:
case cssom::KeywordValue::kFixed:
case cssom::KeywordValue::kFlexEnd:
case cssom::KeywordValue::kFlexStart:
case cssom::KeywordValue::kForwards:
case cssom::KeywordValue::kHidden:
case cssom::KeywordValue::kInfinite:
case cssom::KeywordValue::kInherit:
case cssom::KeywordValue::kInitial:
case cssom::KeywordValue::kLeft:
case cssom::KeywordValue::kLineThrough:
case cssom::KeywordValue::kMiddle:
case cssom::KeywordValue::kMonoscopic:
case cssom::KeywordValue::kMonospace:
case cssom::KeywordValue::kNoRepeat:
case cssom::KeywordValue::kNormal:
case cssom::KeywordValue::kNowrap:
case cssom::KeywordValue::kPre:
case cssom::KeywordValue::kPreLine:
case cssom::KeywordValue::kPreWrap:
case cssom::KeywordValue::kRelative:
case cssom::KeywordValue::kRepeat:
case cssom::KeywordValue::kReverse:
case cssom::KeywordValue::kRight:
case cssom::KeywordValue::kRow:
case cssom::KeywordValue::kRowReverse:
case cssom::KeywordValue::kSansSerif:
case cssom::KeywordValue::kScroll:
case cssom::KeywordValue::kSerif:
case cssom::KeywordValue::kSolid:
case cssom::KeywordValue::kSpaceAround:
case cssom::KeywordValue::kSpaceBetween:
case cssom::KeywordValue::kStart:
case cssom::KeywordValue::kStatic:
case cssom::KeywordValue::kStereoscopicLeftRight:
case cssom::KeywordValue::kStereoscopicTopBottom:
case cssom::KeywordValue::kStretch:
case cssom::KeywordValue::kTop:
case cssom::KeywordValue::kUppercase:
case cssom::KeywordValue::kVisible:
case cssom::KeywordValue::kWrap:
case cssom::KeywordValue::kWrapReverse:
NOTREACHED();
break;
}
}
} // namespace
void BoxGenerator::VisitVideoElement(dom::HTMLVideoElement* video_element) {
// For video elements, create a replaced box.
// A replaced box is formatted as an atomic inline element. It is treated
// directionally as a neutral character and its line breaking behavior is
// equivalent to that of the Object Replacement Character.
// https://www.w3.org/TR/CSS21/visuren.html#inline-boxes
// https://www.w3.org/TR/CSS21/visuren.html#propdef-unicode-bidi
// https://www.w3.org/TR/css3-text/#line-break-details
int32 text_position =
(*paragraph_)
->AppendCodePoint(Paragraph::kObjectReplacementCharacterCodePoint);
render_tree::ResourceProvider* resource_provider =
*video_element->node_document()
->html_element_context()
->resource_provider();
// If the optional is disengaged, then we don't know if punch out is enabled
// or not.
base::Optional<ReplacedBox::ReplacedBoxMode> replaced_box_mode;
if (video_element->GetVideoFrameProvider()) {
VideoFrameProvider::OutputMode output_mode =
video_element->GetVideoFrameProvider()->GetOutputMode();
if (output_mode != VideoFrameProvider::kOutputModeInvalid) {
replaced_box_mode =
(output_mode == VideoFrameProvider::kOutputModePunchOut)
? ReplacedBox::ReplacedBoxMode::kPunchOutVideo
: ReplacedBox::ReplacedBoxMode::kVideo;
}
}
ReplacedBoxGenerator replaced_box_generator(
video_element->css_computed_style_declaration(),
video_element->GetVideoFrameProvider()
? base::Bind(GetVideoFrame, video_element->GetVideoFrameProvider(),
resource_provider)
: ReplacedBox::ReplaceImageCB(),
video_element->GetSetBoundsCB(), *paragraph_, text_position,
base::nullopt, base::nullopt, base::nullopt, context_, replaced_box_mode,
video_element->GetVideoSize(), base::nullopt);
video_element->computed_style()->display()->Accept(&replaced_box_generator);
scoped_refptr<ReplacedBox> replaced_box =
replaced_box_generator.replaced_box();
if (replaced_box.get() == NULL) {
// The element with "display: none" generates no boxes and has no effect
// on layout. Descendant elements do not generate any boxes either.
// This behavior cannot be overridden by setting the "display" property on
// the descendants.
// https://www.w3.org/TR/CSS21/visuren.html#display-prop
return;
}
#ifdef COBALT_BOX_DUMP_ENABLED
replaced_box->SetGeneratingNode(video_element);
#endif // COBALT_BOX_DUMP_ENABLED
replaced_box->SetUiNavItem(video_element->GetUiNavItem());
boxes_.push_back(replaced_box);
// The content of replaced elements is not considered in the CSS rendering
// model.
// https://www.w3.org/TR/CSS21/conform.html#replaced-element
}
void BoxGenerator::VisitBrElement(dom::HTMLBRElement* br_element) {
// If the br element has "display: none", then it has no effect on the layout.
if (br_element->computed_style()->display() ==
cssom::KeywordValue::GetNone()) {
return;
}
scoped_refptr<cssom::CSSComputedStyleDeclaration>
css_computed_style_declaration = new cssom::CSSComputedStyleDeclaration();
css_computed_style_declaration->SetData(GetComputedStyleOfAnonymousBox(
br_element->css_computed_style_declaration()));
css_computed_style_declaration->set_animations(br_element->animations());
DCHECK(*paragraph_);
int32 text_position = (*paragraph_)->GetTextEndPosition();
const bool kTriggersLineBreakTrue = true;
const bool kIsProductOfSplitFalse = false;
scoped_refptr<TextBox> br_text_box =
new TextBox(css_computed_style_declaration, *paragraph_, text_position,
text_position, kTriggersLineBreakTrue, kIsProductOfSplitFalse,
context_->used_style_provider, context_->layout_stat_tracker);
// Add a line feed code point to the paragraph to signify the new line for
// the line breaking and bidirectional algorithms.
(*paragraph_)->AppendCodePoint(Paragraph::kLineFeedCodePoint);
#ifdef COBALT_BOX_DUMP_ENABLED
br_text_box->SetGeneratingNode(br_element);
#endif // COBALT_BOX_DUMP_ENABLED
br_text_box->SetUiNavItem(br_element->GetUiNavItem());
boxes_.push_back(br_text_box);
}
void BoxGenerator::VisitLottiePlayer(dom::LottiePlayer* lottie_player) {
int32 text_position =
(*paragraph_)
->AppendCodePoint(Paragraph::kObjectReplacementCharacterCodePoint);
ReplacedBoxGenerator replaced_box_generator(
lottie_player->css_computed_style_declaration(),
lottie_player->cached_image() &&
lottie_player->cached_image()->TryGetResource()
? base::Bind(GetLottieAnimation,
lottie_player->cached_image()->TryGetResource())
: ReplacedBox::ReplaceImageCB(),
ReplacedBox::SetBoundsCB(), *paragraph_, text_position, base::nullopt,
base::nullopt, base::nullopt, context_,
ReplacedBox::ReplacedBoxMode::kLottie,
math::Size() /* only relevant to punch out video */,
lottie_player->GetProperties());
lottie_player->computed_style()->display()->Accept(&replaced_box_generator);
scoped_refptr<ReplacedBox> replaced_box =
replaced_box_generator.replaced_box();
if (replaced_box.get() == NULL) {
// The element with "display: none" generates no boxes and has no effect
// on layout. Descendant elements do not generate any boxes either.
// This behavior cannot be overridden by setting the "display" property on
// the descendants.
// https://www.w3.org/TR/CSS21/visuren.html#display-prop
// A LottiePlayer element with "display: none" should potentially trigger
// a freeze event.
if (!lottie_player->GetProperties().onfreeze_callback.is_null()) {
lottie_player->GetProperties().onfreeze_callback.Run();
}
return;
}
#ifdef COBALT_BOX_DUMP_ENABLED
replaced_box->SetGeneratingNode(lottie_player);
#endif // COBALT_BOX_DUMP_ENABLED
replaced_box->SetUiNavItem(lottie_player->GetUiNavItem());
boxes_.push_back(replaced_box);
}
namespace {
typedef dom::HTMLElement::DirState DirState;
class ContainerBoxGenerator : public cssom::NotReachedPropertyValueVisitor {
public:
enum CloseParagraph {
kDoNotCloseParagraph,
kCloseParagraph,
};
ContainerBoxGenerator(DirState element_dir,
const scoped_refptr<cssom::CSSComputedStyleDeclaration>&
css_computed_style_declaration,
scoped_refptr<Paragraph>* paragraph,
const BoxGenerator::Context* context)
: element_dir_(element_dir),
css_computed_style_declaration_(css_computed_style_declaration),
context_(context),
has_scoped_directional_isolate_(false),
paragraph_(paragraph),
paragraph_scoped_(false) {}
~ContainerBoxGenerator();
void VisitKeyword(cssom::KeywordValue* keyword) override;
const scoped_refptr<ContainerBox>& container_box() { return container_box_; }
private:
void CreateScopedParagraph(CloseParagraph close_prior_paragraph);
const DirState element_dir_;
const scoped_refptr<cssom::CSSComputedStyleDeclaration>
css_computed_style_declaration_;
const BoxGenerator::Context* context_;
// If a directional isolate was added to the paragraph by this container box
// and needs to be popped in the destructor:
// http://unicode.org/reports/tr9/#Explicit_Directional_Isolates
bool has_scoped_directional_isolate_;
scoped_refptr<Paragraph>* paragraph_;
scoped_refptr<Paragraph> prior_paragraph_;
bool paragraph_scoped_;
scoped_refptr<ContainerBox> container_box_;
};
ContainerBoxGenerator::~ContainerBoxGenerator() {
// If there's a scoped directional isolate, then it needs to popped from
// the paragraph so that this box does not impact the directionality of later
// boxes in the paragraph.
// http://unicode.org/reports/tr9/#Terminating_Explicit_Directional_Isolates
if (has_scoped_directional_isolate_) {
(*paragraph_)->AppendCodePoint(Paragraph::kPopDirectionalIsolateCodePoint);
}
if (paragraph_scoped_) {
(*paragraph_)->Close();
// If the prior paragraph was closed, then replace it with a new paragraph
// that has the same direction as the previous one. Otherwise, restore the
// prior one.
if (prior_paragraph_->IsClosed()) {
*paragraph_ = new Paragraph(
prior_paragraph_->GetLocale(), prior_paragraph_->base_direction(),
prior_paragraph_->GetDirectionalFormattingStack(),
context_->line_break_iterator, context_->character_break_iterator);
} else {
*paragraph_ = prior_paragraph_;
}
}
}
void ContainerBoxGenerator::VisitKeyword(cssom::KeywordValue* keyword) {
// See https://www.w3.org/TR/CSS21/visuren.html#display-prop.
switch (keyword->value()) {
// Generate a block-level block container box.
case cssom::KeywordValue::kBlock:
// The block ends the current paragraph and begins a new one that ends
// with the block, so close the current paragraph, and create a new
// paragraph that will close when the container box generator is
// destroyed.
CreateScopedParagraph(kCloseParagraph);
container_box_ = base::WrapRefCounted(new BlockLevelBlockContainerBox(
css_computed_style_declaration_, (*paragraph_)->base_direction(),
context_->used_style_provider, context_->layout_stat_tracker));
break;
case cssom::KeywordValue::kFlex:
container_box_ = base::WrapRefCounted(new BlockLevelFlexContainerBox(
css_computed_style_declaration_, (*paragraph_)->base_direction(),
context_->used_style_provider, context_->layout_stat_tracker));
break;
case cssom::KeywordValue::kInlineFlex: {
// An inline flex container is an atomic inline and therefore is treated
// directionally as a neutral character and its line breaking behavior is
// equivalent to that of the Object Replacement Character.
// https://www.w3.org/TR/css-display-3/#atomic-inline
// https://www.w3.org/TR/CSS21/visuren.html#propdef-unicode-bidi
// https://www.w3.org/TR/css3-text/#line-break-details
int32 text_position =
(*paragraph_)
->AppendCodePoint(
Paragraph::kObjectReplacementCharacterCodePoint);
scoped_refptr<Paragraph> prior_paragraph = *paragraph_;
// The inline flex container creates a new paragraph, which the old
// paragraph flows around. Create a new paragraph, which will close with
// the end of the flex container. However, do not close the old
// paragraph, because it will continue once the scope of the inline block
// ends.
CreateScopedParagraph(kDoNotCloseParagraph);
container_box_ = base::WrapRefCounted(new InlineLevelFlexContainerBox(
css_computed_style_declaration_, (*paragraph_)->base_direction(),
prior_paragraph, text_position, context_->used_style_provider,
context_->layout_stat_tracker));
} break;
// Generate one or more inline boxes. Note that more inline boxes may be
// generated when the original inline box is split due to participation
// in the formatting context.
case cssom::KeywordValue::kInline:
// If the creating HTMLElement had an explicit directionality, then append
// a directional isolate to the paragraph. This will be popped from the
// paragraph, when the ContainerBoxGenerator goes out of scope.
// https://dev.w3.org/html5/spec-preview/global-attributes.html#the-directionality
// http://unicode.org/reports/tr9/#Explicit_Directional_Isolates
// http://unicode.org/reports/tr9/#Markup_And_Formatting
if (element_dir_ == DirState::kDirLeftToRight) {
has_scoped_directional_isolate_ = true;
(*paragraph_)->AppendCodePoint(Paragraph::kLeftToRightIsolateCodePoint);
} else if (element_dir_ == DirState::kDirRightToLeft) {
has_scoped_directional_isolate_ = true;
(*paragraph_)->AppendCodePoint(Paragraph::kRightToLeftIsolateCodePoint);
}
// If the paragraph has not started yet, then add a no-break space to it,
// thereby starting the paragraph without providing a wrappable location,
// as the line should never wrap at the start of text.
// http://unicode.org/reports/tr14/#BreakingRules
//
// Starting the paragraph ensures that subsequent text nodes create text
// boxes, even when they consist of only collapsible white-space. This is
// necessary because empty inline container boxes can justify a line's
// existence if they have a non-zero margin, border or padding, which
// means that the collapsible white-space is potentially wrappable
// regardless of whether any intervening text is added to the paragraph.
// Not creating the collapsible text box in this case would incorrectly
// eliminate a wrappable location from the line.
if ((*paragraph_)->GetTextEndPosition() == 0) {
(*paragraph_)->AppendCodePoint(Paragraph::kNoBreakSpaceCodePoint);
}
container_box_ = base::WrapRefCounted(new InlineContainerBox(
css_computed_style_declaration_, context_->used_style_provider,
context_->layout_stat_tracker));
break;
// Generate an inline-level block container box. The inside of
// an inline-block is formatted as a block box, and the element itself
// is formatted as an atomic inline-level box.
// https://www.w3.org/TR/CSS21/visuren.html#inline-boxes
case cssom::KeywordValue::kInlineBlock: {
// An inline block is an atomic inline and therefore is treated
// directionally as a neutral character and its line breaking behavior is
// equivalent to that of the Object Replacement Character.
// https://www.w3.org/TR/css-display-3/#atomic-inline
// https://www.w3.org/TR/CSS21/visuren.html#propdef-unicode-bidi
// https://www.w3.org/TR/css3-text/#line-break-details
int32 text_position =
(*paragraph_)
->AppendCodePoint(
Paragraph::kObjectReplacementCharacterCodePoint);
scoped_refptr<Paragraph> prior_paragraph = *paragraph_;
// The inline block creates a new paragraph, which the old paragraph
// flows around. Create a new paragraph, which will close with the end
// of the inline block. However, do not close the old paragraph, because
// it will continue once the scope of the inline block ends.
CreateScopedParagraph(kDoNotCloseParagraph);
container_box_ = base::WrapRefCounted(new InlineLevelBlockContainerBox(
css_computed_style_declaration_, (*paragraph_)->base_direction(),
prior_paragraph, text_position, context_->used_style_provider,
context_->layout_stat_tracker));
} break;
// The element generates no boxes and has no effect on layout.
case cssom::KeywordValue::kNone:
// Leave |container_box_| NULL.
break;
case cssom::KeywordValue::kAbsolute:
case cssom::KeywordValue::kAlternate:
case cssom::KeywordValue::kAlternateReverse:
case cssom::KeywordValue::kAuto:
case cssom::KeywordValue::kBackwards:
case cssom::KeywordValue::kBaseline:
case cssom::KeywordValue::kBoth:
case cssom::KeywordValue::kBottom:
case cssom::KeywordValue::kBreakWord:
case cssom::KeywordValue::kCenter:
case cssom::KeywordValue::kClip:
case cssom::KeywordValue::kCollapse:
case cssom::KeywordValue::kColumn:
case cssom::KeywordValue::kColumnReverse:
case cssom::KeywordValue::kContain:
case cssom::KeywordValue::kContent:
case cssom::KeywordValue::kCover:
case cssom::KeywordValue::kCurrentColor:
case cssom::KeywordValue::kCursive:
case cssom::KeywordValue::kEllipsis:
case cssom::KeywordValue::kEnd:
case cssom::KeywordValue::kEquirectangular:
case cssom::KeywordValue::kFantasy:
case cssom::KeywordValue::kFixed:
case cssom::KeywordValue::kFlexEnd:
case cssom::KeywordValue::kFlexStart:
case cssom::KeywordValue::kForwards:
case cssom::KeywordValue::kHidden:
case cssom::KeywordValue::kInfinite:
case cssom::KeywordValue::kInherit:
case cssom::KeywordValue::kInitial:
case cssom::KeywordValue::kLeft:
case cssom::KeywordValue::kLineThrough:
case cssom::KeywordValue::kMiddle:
case cssom::KeywordValue::kMonoscopic:
case cssom::KeywordValue::kMonospace:
case cssom::KeywordValue::kNoRepeat:
case cssom::KeywordValue::kNormal:
case cssom::KeywordValue::kNowrap:
case cssom::KeywordValue::kPre:
case cssom::KeywordValue::kPreLine:
case cssom::KeywordValue::kPreWrap:
case cssom::KeywordValue::kRelative:
case cssom::KeywordValue::kRepeat:
case cssom::KeywordValue::kReverse:
case cssom::KeywordValue::kRight:
case cssom::KeywordValue::kRow:
case cssom::KeywordValue::kRowReverse:
case cssom::KeywordValue::kSansSerif:
case cssom::KeywordValue::kScroll:
case cssom::KeywordValue::kSerif:
case cssom::KeywordValue::kSolid:
case cssom::KeywordValue::kSpaceAround:
case cssom::KeywordValue::kSpaceBetween:
case cssom::KeywordValue::kStart:
case cssom::KeywordValue::kStatic:
case cssom::KeywordValue::kStereoscopicLeftRight:
case cssom::KeywordValue::kStereoscopicTopBottom:
case cssom::KeywordValue::kStretch:
case cssom::KeywordValue::kTop:
case cssom::KeywordValue::kUppercase:
case cssom::KeywordValue::kVisible:
case cssom::KeywordValue::kWrap:
case cssom::KeywordValue::kWrapReverse:
NOTREACHED();
break;
}
}
void ContainerBoxGenerator::CreateScopedParagraph(
CloseParagraph close_prior_paragraph) {
DCHECK(!paragraph_scoped_);
paragraph_scoped_ = true;
prior_paragraph_ = *paragraph_;
// Determine the base direction of the new paragraph based upon the
// directionality of the creating HTMLElement. If there was no explicit
// directionality, then it is based upon the prior paragraph, meaning that
// it is inherited from the parent element.
// https://dev.w3.org/html5/spec-preview/global-attributes.html#the-directionality
BaseDirection base_direction;
if (element_dir_ == DirState::kDirLeftToRight) {
base_direction = kLeftToRightBaseDirection;
} else if (element_dir_ == DirState::kDirRightToLeft) {
base_direction = kRightToLeftBaseDirection;
} else {
base_direction = prior_paragraph_->GetDirectionalFormattingStackDirection();
}
if (close_prior_paragraph == kCloseParagraph) {
prior_paragraph_->Close();
}
*paragraph_ = new Paragraph(prior_paragraph_->GetLocale(), base_direction,
Paragraph::DirectionalFormattingStack(),
context_->line_break_iterator,
context_->character_break_iterator);
}
} // namespace
void BoxGenerator::AppendChildBoxToLine(const scoped_refptr<Box>& child_box) {
// When an inline box contains an in-flow block-level box, the inline box
// (and its inline ancestors within the same block container box*) are
// broken around the block-level box, splitting the inline box into two
// boxes (even if either side is empty), one on each side of
// the block-level box. The line boxes before the break and after
// the break are enclosed in anonymous block boxes, and the block-level
// box becomes a sibling of those anonymous boxes.
// https://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level
//
// * CSS 2.1 says "the same line box" but line boxes are not real boxes
// in Cobalt, see |LineBox| for details.
ContainerBox* last_container_box =
base::polymorphic_downcast<ContainerBox*>(boxes_.back().get());
if (!last_container_box->TryAddChild(child_box)) {
scoped_refptr<ContainerBox> next_container_box =
last_container_box->TrySplitAtEnd();
DCHECK(next_container_box);
// Attempt to add the box to the next container before adding it to the top
// level. In the case where a line break was blocking the add in the last
// container, the child should successfully go into the next container.
if (!next_container_box->TryAddChild(child_box)) {
boxes_.push_back(child_box);
}
boxes_.push_back(next_container_box);
}
}
namespace {
class ContentProvider : public cssom::NotReachedPropertyValueVisitor {
public:
ContentProvider() : is_element_generated_(false) {}
const std::string& content_string() const { return content_string_; }
bool is_element_generated() const { return is_element_generated_; }
void VisitString(cssom::StringValue* string_value) override {
content_string_ = string_value->value();
is_element_generated_ = true;
}
void VisitURL(cssom::URLValue* url_value) override {
// TODO: Implement support for 'content: url(foo)'.
DLOG(ERROR) << "Unsupported content property value: "
<< url_value->ToString();
}
void VisitKeyword(cssom::KeywordValue* keyword) override {
switch (keyword->value()) {
case cssom::KeywordValue::kNone:
case cssom::KeywordValue::kNormal:
// The pseudo-element is not generated.
// https://www.w3.org/TR/CSS21/generate.html#propdef-content
is_element_generated_ = false;
break;
case cssom::KeywordValue::kAbsolute:
case cssom::KeywordValue::kAlternate:
case cssom::KeywordValue::kAlternateReverse:
case cssom::KeywordValue::kAuto:
case cssom::KeywordValue::kBackwards:
case cssom::KeywordValue::kBaseline:
case cssom::KeywordValue::kBlock:
case cssom::KeywordValue::kBoth:
case cssom::KeywordValue::kBottom:
case cssom::KeywordValue::kBreakWord:
case cssom::KeywordValue::kCenter:
case cssom::KeywordValue::kClip:
case cssom::KeywordValue::kCollapse:
case cssom::KeywordValue::kColumn:
case cssom::KeywordValue::kColumnReverse:
case cssom::KeywordValue::kContain:
case cssom::KeywordValue::kContent:
case cssom::KeywordValue::kCover:
case cssom::KeywordValue::kCurrentColor:
case cssom::KeywordValue::kCursive:
case cssom::KeywordValue::kEllipsis:
case cssom::KeywordValue::kEnd:
case cssom::KeywordValue::kEquirectangular:
case cssom::KeywordValue::kFantasy:
case cssom::KeywordValue::kFixed:
case cssom::KeywordValue::kFlex:
case cssom::KeywordValue::kFlexEnd:
case cssom::KeywordValue::kFlexStart:
case cssom::KeywordValue::kForwards:
case cssom::KeywordValue::kHidden:
case cssom::KeywordValue::kInfinite:
case cssom::KeywordValue::kInherit:
case cssom::KeywordValue::kInitial:
case cssom::KeywordValue::kInline:
case cssom::KeywordValue::kInlineBlock:
case cssom::KeywordValue::kInlineFlex:
case cssom::KeywordValue::kLeft:
case cssom::KeywordValue::kLineThrough:
case cssom::KeywordValue::kMiddle:
case cssom::KeywordValue::kMonoscopic:
case cssom::KeywordValue::kMonospace:
case cssom::KeywordValue::kNoRepeat:
case cssom::KeywordValue::kNowrap:
case cssom::KeywordValue::kPre:
case cssom::KeywordValue::kPreLine:
case cssom::KeywordValue::kPreWrap:
case cssom::KeywordValue::kRelative:
case cssom::KeywordValue::kRepeat:
case cssom::KeywordValue::kReverse:
case cssom::KeywordValue::kRight:
case cssom::KeywordValue::kRow:
case cssom::KeywordValue::kRowReverse:
case cssom::KeywordValue::kSansSerif:
case cssom::KeywordValue::kScroll:
case cssom::KeywordValue::kSerif:
case cssom::KeywordValue::kSolid:
case cssom::KeywordValue::kSpaceAround:
case cssom::KeywordValue::kSpaceBetween:
case cssom::KeywordValue::kStart:
case cssom::KeywordValue::kStatic:
case cssom::KeywordValue::kStereoscopicLeftRight:
case cssom::KeywordValue::kStereoscopicTopBottom:
case cssom::KeywordValue::kStretch:
case cssom::KeywordValue::kTop:
case cssom::KeywordValue::kUppercase:
case cssom::KeywordValue::kVisible:
case cssom::KeywordValue::kWrap:
case cssom::KeywordValue::kWrapReverse:
NOTREACHED();
}
}
private:
std::string content_string_;
bool is_element_generated_;
};
bool HasOnlyColorPropertyAnimations(
const scoped_refptr<const web_animations::AnimationSet>& animations) {
const web_animations::AnimationSet::InternalSet& animation_set =
animations->animations();
if (animation_set.empty()) {
return false;
}
for (web_animations::AnimationSet::InternalSet::const_iterator iter =
animation_set.begin();
iter != animation_set.end(); ++iter) {
const web_animations::KeyframeEffectReadOnly* keyframe_effect =
base::polymorphic_downcast<
const web_animations::KeyframeEffectReadOnly*>(
(*iter)->effect().get());
if (!keyframe_effect->data().IsOnlyPropertyAnimated(
cssom::kColorProperty)) {
return false;
}
}
return true;
}
} // namespace
void BoxGenerator::AppendPseudoElementToLine(
dom::HTMLElement* html_element,
dom::PseudoElementType pseudo_element_type) {
// Add boxes with generated content from :before or :after pseudo elements to
// the line.
// https://www.w3.org/TR/CSS21/generate.html#before-after-content
dom::PseudoElement* pseudo_element =
html_element->pseudo_element(pseudo_element_type);
if (!pseudo_element) {
return;
}
// We assume that if our parent element's boxes are being regenerated, then we
// should regenerate the pseudo element boxes. There are some cases where
// the parent element may be regenerating its boxes even if it already had
// some, such as if its boxes were inline level. In that case, pseudo
// elements may also have boxes, so we make it clear that we will not be
// reusing pseudo element boxes even if they exist by explicitly resetting
// them now.
pseudo_element->reset_layout_boxes();
ContainerBoxGenerator pseudo_element_box_generator(
DirState::kDirNotDefined,
pseudo_element->css_computed_style_declaration(), paragraph_, context_);
pseudo_element->computed_style()->display()->Accept(
&pseudo_element_box_generator);
scoped_refptr<ContainerBox> pseudo_element_box =
pseudo_element_box_generator.container_box();
// A pseudo element with "display: none" generates no boxes and has no
// effect on layout.
if (pseudo_element_box.get() == NULL) {
return;
}
// Generate the box(es) to be added to the associated html element, using
// the computed style of the pseudo element.
// The generated content is a text node with the string value of the
// 'content' property.
ContentProvider content_provider;
pseudo_element->computed_style()->content()->Accept(&content_provider);
if (!content_provider.is_element_generated()) {
return;
}
scoped_refptr<dom::Text> child_node(new dom::Text(
html_element->node_document(), content_provider.content_string()));
// In the case where the pseudo element has no color property of its
// own, but is directly inheriting a color property from its parent html
// element, we use the parent's animations if the pseudo element has
// none and the parent has only color property animations. This allows
// the child text boxes to animate properly and fixes bugs, while
// keeping the impact of the fix as small as possible to minimize the
// risk of introducing new bugs.
// TODO: Remove this logic when support for inheriting
// animations on inherited properties is added.
bool use_html_element_animations =
!pseudo_element->computed_style()->IsDeclared(cssom::kColorProperty) &&
html_element->computed_style()->IsDeclared(cssom::kColorProperty) &&
pseudo_element->css_computed_style_declaration()
->animations()
->IsEmpty() &&
HasOnlyColorPropertyAnimations(
html_element->css_computed_style_declaration()->animations());
BoxGenerator child_box_generator(
pseudo_element->css_computed_style_declaration(),
use_html_element_animations ? html_element->animations()
: pseudo_element->animations(),
paragraph_, dom_element_depth_ + 1, context_);
child_node->Accept(&child_box_generator);
for (const auto& child_box : child_box_generator.boxes()) {
if (!pseudo_element_box->TryAddChild(child_box)) {
return;
}
}
pseudo_element->set_layout_boxes(
std::unique_ptr<dom::LayoutBoxes>(new LayoutBoxes({pseudo_element_box})));
// Add the box(es) from the pseudo element to the associated element.
AppendChildBoxToLine(pseudo_element_box);
}
namespace {
scoped_refptr<cssom::CSSComputedStyleDeclaration> StripBackground(
const scoped_refptr<cssom::CSSComputedStyleDeclaration>& style) {
scoped_refptr<cssom::CSSComputedStyleDeclaration> new_style(
new cssom::CSSComputedStyleDeclaration());
new_style->set_animations(style->animations());
scoped_refptr<cssom::MutableCSSComputedStyleData> new_data(
new cssom::MutableCSSComputedStyleData());
new_data->AssignFrom(*style->data());
new_data->SetPropertyValue(cssom::kBackgroundColorProperty, NULL);
new_data->SetPropertyValue(cssom::kBackgroundImageProperty, NULL);
new_style->SetData(new_data);
return new_style;
}
} // namespace
void BoxGenerator::VisitNonReplacedElement(dom::HTMLElement* html_element) {
const scoped_refptr<cssom::CSSComputedStyleDeclaration>& element_style(
html_element->css_computed_style_declaration());
ContainerBoxGenerator container_box_generator(
html_element->GetUsedDirState(),
html_element == context_->ignore_background_element
? StripBackground(element_style)
: element_style,
paragraph_, context_);
html_element->computed_style()->display()->Accept(&container_box_generator);
scoped_refptr<ContainerBox> container_box_before_split =
container_box_generator.container_box();
if (container_box_before_split.get() == NULL) {
// The element with "display: none" generates no boxes and has no effect
// on layout. Descendant elements do not generate any boxes either.
// This behavior cannot be overridden by setting the "display" property on
// the descendants.
// https://www.w3.org/TR/CSS21/visuren.html#display-prop
return;
}
#ifdef COBALT_BOX_DUMP_ENABLED
container_box_before_split->SetGeneratingNode(html_element);
#endif // COBALT_BOX_DUMP_ENABLED
container_box_before_split->SetUiNavItem(html_element->GetUiNavItem());
boxes_.push_back(container_box_before_split);
// We already handle the case where the Intersection Observer root is the
// viewport with the initial containing block in layout.
if (html_element !=
html_element->node_document()->document_element()->AsHTMLElement()) {
BoxIntersectionObserverModule::IntersectionObserverRootVector roots =
html_element->GetLayoutIntersectionObserverRoots();
BoxIntersectionObserverModule::IntersectionObserverTargetVector targets =
html_element->GetLayoutIntersectionObserverTargets();
container_box_before_split->AddIntersectionObserverRootsAndTargets(
std::move(roots), std::move(targets));
}
AppendPseudoElementToLine(html_element, dom::kBeforePseudoElementType);
// Generate child boxes.
for (dom::Node* child_node = html_element->first_child(); child_node;
child_node = child_node->next_sibling()) {
BoxGenerator child_box_generator(
html_element->css_computed_style_declaration(),
html_element->css_computed_style_declaration()->animations(),
paragraph_, dom_element_depth_ + 1, context_);
child_node->Accept(&child_box_generator);
const Boxes& child_boxes = child_box_generator.boxes();
for (Boxes::const_iterator child_box_iterator = child_boxes.begin();
child_box_iterator != child_boxes.end(); ++child_box_iterator) {
AppendChildBoxToLine(*child_box_iterator);
}
}
AppendPseudoElementToLine(html_element, dom::kAfterPseudoElementType);
}
void BoxGenerator::Visit(dom::CDATASection* cdata_section) {}
void BoxGenerator::Visit(dom::Comment* comment) {}
void BoxGenerator::Visit(dom::Document* document) { NOTREACHED(); }
void BoxGenerator::Visit(dom::DocumentType* document_type) { NOTREACHED(); }
namespace {
scoped_refptr<web_animations::AnimationSet> GetAnimationsForAnonymousBox(
const scoped_refptr<const web_animations::AnimationSet>&
parent_animations) {
scoped_refptr<web_animations::AnimationSet> animations(
new web_animations::AnimationSet);
const web_animations::AnimationSet::InternalSet& animation_set =
parent_animations->animations();
const cssom::PropertyKeyVector& properties_set =
cssom::GetInheritedAnimatableProperties();
// Go through all the parent animations and only add those pertaining to
// inheritable properties.
for (const auto& animation : animation_set) {
const web_animations::KeyframeEffectReadOnly* keyframe_effect =
base::polymorphic_downcast<
const web_animations::KeyframeEffectReadOnly*>(
animation->effect().get());
for (const auto& property : properties_set) {
if (keyframe_effect->data().IsPropertyAnimated(property)) {
animations->AddAnimation(animation);
break;
}
}
}
return animations;
}
} // namespace
// Append the text from the text node to the text paragraph and create the
// node's initial text box. The text box has indices that map to the paragraph,
// which allows it to retrieve its underlying text. Initially, a single text box
// is created that encompasses the entire node.
// Prior to layout, the paragraph applies the Unicode bidirectional algorithm
// to its text (http://www.unicode.org/reports/tr9/) and causes the text boxes
// referencing it to split at level runs.
//
// During layout, the text boxes are potentially split further, as the paragraph
// determines line breaking locations for its text at soft wrap opportunities
// (https://www.w3.org/TR/css-text-3/#soft-wrap-opportunity) according to the
// Unicode line breaking algorithm (http://www.unicode.org/reports/tr14/).
void BoxGenerator::Visit(dom::Text* text) {
scoped_refptr<cssom::CSSComputedStyleDeclaration>
css_computed_style_declaration = new cssom::CSSComputedStyleDeclaration();
css_computed_style_declaration->SetData(
GetComputedStyleOfAnonymousBox(parent_css_computed_style_declaration_));
// Copy inheritable animatable properties from the parent.
css_computed_style_declaration->set_animations(
GetAnimationsForAnonymousBox(parent_animations_));
DCHECK(text);
DCHECK(css_computed_style_declaration->data());
const std::string& original_text = text->text();
if (original_text.empty()) {
return;
}
const scoped_refptr<cssom::PropertyValue>& white_space_property =
css_computed_style_declaration->data()->white_space();
bool should_preserve_segment_breaks =
!DoesCollapseSegmentBreaks(white_space_property);
bool should_collapse_white_space =
DoesCollapseWhiteSpace(white_space_property);
bool should_prevent_text_wrapping =
!DoesAllowTextWrapping(white_space_property);
// Loop until the entire text is consumed. If the white-space property does
// not have a value of "pre" or "pre-wrap" then the entire text will be
// processed the first time through the loop; otherwise, the text will be
// split at newline sequences.
size_t start_index = 0;
while (start_index < original_text.size()) {
size_t end_index;
size_t newline_sequence_length;
// Phase I: Segment Break Transformation Rules
// https://www.w3.org/TR/css3-text/#line-break-transform
bool generates_newline = false;
if (should_preserve_segment_breaks) {
generates_newline = FindNextNewlineSequence(
original_text, start_index, &end_index, &newline_sequence_length);
} else {
end_index = original_text.size();
newline_sequence_length = 0;
}
std::string modifiable_text =
original_text.substr(start_index, end_index - start_index);
// Phase I: Collapsing and Transformation
// https://www.w3.org/TR/css3-text/#white-space-phase-1
if (should_collapse_white_space) {
CollapseWhiteSpace(&modifiable_text);
// If the paragraph hasn't been started yet and the text only consists of
// a collapsible space, then return without creating the box. The leading
// spaces in a line box are collapsed, so this box would be collapsed
// away during the layout.
if ((*paragraph_)->GetTextEndPosition() == 0 && modifiable_text == " ") {
return;
}
}
Paragraph::TextTransform transform;
if (css_computed_style_declaration->data()->text_transform() ==
cssom::KeywordValue::GetUppercase()) {
transform = Paragraph::kUppercaseTextTransform;
} else {
transform = Paragraph::kNoTextTransform;
}
DCHECK(*paragraph_);
int32 text_start_position =
(*paragraph_)->AppendUtf8String(modifiable_text, transform);
int32 text_end_position = (*paragraph_)->GetTextEndPosition();
const bool kIsProductOfSplitFalse = false;
boxes_.push_back(new TextBox(
css_computed_style_declaration, *paragraph_, text_start_position,
text_end_position, generates_newline, kIsProductOfSplitFalse,
context_->used_style_provider, context_->layout_stat_tracker));
// Newline sequences should be transformed into a preserved line feed.
// https://www.w3.org/TR/css3-text/#line-break-transform
if (generates_newline) {
(*paragraph_)->AppendCodePoint(Paragraph::kLineFeedCodePoint);
}
start_index = end_index + newline_sequence_length;
}
// If the white-space style prevents text wrapping and the text ends in a
// space, then add a no-break space to the paragraph, so that the last space
// will be treated as a no-break space when determining if wrapping can occur
// before the subsequent box.
//
// While CSS3 gives little direction to the user agent as to what should occur
// in this case, this is guidance given by CSS2, which states that "any
// sequence of spaces (U+0020) unbroken by an element boundary is treated as a
// sequence of non-breaking spaces." Furthermore, this matches the behavior of
// WebKit, Firefox, and IE.
// https://www.w3.org/TR/css-text-3/#white-space-phase-1
// https://www.w3.org/TR/CSS2/text.html#white-space-model
if (should_prevent_text_wrapping &&
original_text[original_text.size() - 1] == ' ') {
(*paragraph_)->AppendCodePoint(Paragraph::kNoBreakSpaceCodePoint);
}
}
} // namespace layout
} // namespace cobalt