blob: 7bcb96a35ef168fdb2dfcb7b1792ed09f31b7a68 [file] [log] [blame]
/*
* Copyright 2014 Google Inc. 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/replaced_box.h"
#include <algorithm>
#include "base/bind.h"
#include "base/logging.h"
#include "cobalt/cssom/filter_function_list_value.h"
#include "cobalt/cssom/keyword_value.h"
#include "cobalt/cssom/mtm_function.h"
#include "cobalt/layout/container_box.h"
#include "cobalt/layout/letterboxed_image.h"
#include "cobalt/layout/used_style.h"
#include "cobalt/layout/white_space_processing.h"
#include "cobalt/math/transform_2d.h"
#include "cobalt/math/vector2d_f.h"
#include "cobalt/render_tree/brush.h"
#include "cobalt/render_tree/color_rgba.h"
#include "cobalt/render_tree/filter_node.h"
#include "cobalt/render_tree/image_node.h"
#include "cobalt/render_tree/map_to_mesh_filter.h"
#include "cobalt/render_tree/punch_through_video_node.h"
#include "cobalt/render_tree/rect_node.h"
namespace cobalt {
namespace layout {
using render_tree::animations::AnimateNode;
using render_tree::CompositionNode;
using render_tree::FilterNode;
using render_tree::ImageNode;
using render_tree::MapToMeshFilter;
using render_tree::Node;
using render_tree::PunchThroughVideoNode;
using render_tree::RectNode;
using render_tree::SolidColorBrush;
namespace {
// Used when intrinsic ratio cannot be determined,
// as per https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width.
const float kFallbackIntrinsicRatio = 2.0f;
// Becomes a used value of "width" if it cannot be determined by any other
// means, as per https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width.
const float kFallbackWidth = 300.0f;
// Convert the parsed keyword value for a stereo mode into a stereo mode enum
// value.
render_tree::StereoMode ReadStereoMode(
const scoped_refptr<cssom::KeywordValue>& keyword_value) {
cssom::KeywordValue::Value value = keyword_value->value();
if (value == cssom::KeywordValue::kMonoscopic) {
return render_tree::kMono;
} else if (value == cssom::KeywordValue::kStereoscopicLeftRight) {
return render_tree::kLeftRight;
} else if (value == cssom::KeywordValue::kStereoscopicTopBottom) {
return render_tree::kTopBottom;
} else {
LOG(DFATAL) << "Stereo mode has an invalid non-NULL value, defaulting to "
<< "monoscopic";
return render_tree::kMono;
}
}
} // namespace
ReplacedBox::ReplacedBox(
const scoped_refptr<cssom::CSSComputedStyleDeclaration>&
css_computed_style_declaration,
const ReplaceImageCB& replace_image_cb, const 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,
UsedStyleProvider* used_style_provider,
LayoutStatTracker* layout_stat_tracker)
: Box(css_computed_style_declaration, used_style_provider,
layout_stat_tracker),
maybe_intrinsic_width_(maybe_intrinsic_width),
maybe_intrinsic_height_(maybe_intrinsic_height),
// Like Chromium, we assume that an element must always have an intrinsic
// ratio, although technically it's a spec violation. For details see
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width.
intrinsic_ratio_(maybe_intrinsic_ratio.value_or(kFallbackIntrinsicRatio)),
replace_image_cb_(replace_image_cb),
set_bounds_cb_(set_bounds_cb),
paragraph_(paragraph),
text_position_(text_position) {}
WrapResult ReplacedBox::TryWrapAt(
WrapAtPolicy /*wrap_at_policy*/,
WrapOpportunityPolicy wrap_opportunity_policy,
bool is_line_existence_justified, LayoutUnit /*available_width*/,
bool /*should_collapse_trailing_white_space*/) {
// NOTE: This logic must stay in sync with
// InlineLevelBlockContainerBox::TryWrapAt().
DCHECK(!IsAbsolutelyPositioned());
// Wrapping is not allowed until the line's existence is justified, meaning
// that wrapping cannot occur before the box. Given that this box cannot be
// split, no wrappable point is available.
if (!is_line_existence_justified) {
return kWrapResultNoWrap;
}
// Atomic inline elements participate in the inline formatting context as a
// single opaque box. Therefore, the parent's style should be used, as the
// internals of the atomic inline element have no impact on the formatting of
// the line.
// https://www.w3.org/TR/CSS21/visuren.html#inline-boxes
if (!parent()) {
return kWrapResultNoWrap;
}
bool style_allows_break_word = parent()->computed_style()->overflow_wrap() ==
cssom::KeywordValue::GetBreakWord();
if (!ShouldProcessWrapOpportunityPolicy(wrap_opportunity_policy,
style_allows_break_word)) {
return kWrapResultNoWrap;
}
// Even when the style prevents wrapping, wrapping can still occur before the
// box if the line's existence has already been justified and whitespace
// precedes it.
if (!DoesAllowTextWrapping(parent()->computed_style()->white_space())) {
if (text_position_ > 0 &&
paragraph_->IsCollapsibleWhiteSpace(text_position_ - 1)) {
return kWrapResultWrapBefore;
} else {
return kWrapResultNoWrap;
}
}
Paragraph::BreakPolicy break_policy =
Paragraph::GetBreakPolicyFromWrapOpportunityPolicy(
wrap_opportunity_policy, style_allows_break_word);
return paragraph_->IsBreakPosition(text_position_, break_policy)
? kWrapResultWrapBefore
: kWrapResultNoWrap;
}
void ReplacedBox::SplitBidiLevelRuns() {}
bool ReplacedBox::TrySplitAtSecondBidiLevelRun() { return false; }
base::optional<int> ReplacedBox::GetBidiLevel() const {
return paragraph_->GetBidiLevel(text_position_);
}
void ReplacedBox::SetShouldCollapseLeadingWhiteSpace(
bool /*should_collapse_leading_white_space*/) {
// Do nothing.
}
void ReplacedBox::SetShouldCollapseTrailingWhiteSpace(
bool /*should_collapse_trailing_white_space*/) {
// Do nothing.
}
bool ReplacedBox::HasLeadingWhiteSpace() const { return false; }
bool ReplacedBox::HasTrailingWhiteSpace() const { return false; }
bool ReplacedBox::IsCollapsed() const { return false; }
bool ReplacedBox::JustifiesLineExistence() const { return true; }
bool ReplacedBox::AffectsBaselineInBlockFormattingContext() const {
return false;
}
LayoutUnit ReplacedBox::GetBaselineOffsetFromTopMarginEdge() const {
return GetMarginBoxHeight();
}
#if !PUNCH_THROUGH_VIDEO_RENDERING
namespace {
void AddLetterboxFillRects(const LetterboxDimensions& dimensions,
CompositionNode::Builder* composition_node_builder) {
const render_tree::ColorRGBA kSolidBlack(0, 0, 0, 1);
for (uint32 i = 0; i < dimensions.fill_rects.size(); ++i) {
const math::RectF& fill_rect = dimensions.fill_rects[i];
composition_node_builder->AddChild(new RectNode(
fill_rect,
scoped_ptr<render_tree::Brush>(new SolidColorBrush(kSolidBlack))));
}
}
void AddLetterboxedImageToRenderTree(
const LetterboxDimensions& dimensions,
const scoped_refptr<render_tree::Image>& image,
CompositionNode::Builder* composition_node_builder) {
if (dimensions.image_rect) {
ImageNode::Builder image_builder(image, *dimensions.image_rect);
composition_node_builder->AddChild(new ImageNode(image_builder));
}
AddLetterboxFillRects(dimensions, composition_node_builder);
}
void AnimateCB(const ReplacedBox::ReplaceImageCB& replace_image_cb,
math::SizeF destination_size,
CompositionNode::Builder* composition_node_builder,
base::TimeDelta time) {
UNREFERENCED_PARAMETER(time);
DCHECK(!replace_image_cb.is_null());
DCHECK(composition_node_builder);
scoped_refptr<render_tree::Image> image = replace_image_cb.Run();
// TODO: Detect better when the intrinsic video size is used for the
// node size, and trigger a re-layout from the media element when the size
// changes.
if (image && 0 == destination_size.height()) {
destination_size = image->GetSize();
}
if (image) {
AddLetterboxedImageToRenderTree(
GetLetterboxDimensions(image->GetSize(), destination_size), image,
composition_node_builder);
}
}
} // namespace
#endif // !PUNCH_THROUGH_VIDEO_RENDERING
void ReplacedBox::RenderAndAnimateContent(
CompositionNode::Builder* border_node_builder) const {
if (computed_style()->visibility() != cssom::KeywordValue::GetVisible()) {
return;
}
if (replace_image_cb_.is_null()) {
// Nothing to render.
return;
}
CompositionNode::Builder composition_node_builder(
math::Vector2dF((border_left_width() + padding_left()).toFloat(),
(border_top_width() + padding_top()).toFloat()));
scoped_refptr<CompositionNode> composition_node =
new CompositionNode(composition_node_builder);
#if PUNCH_THROUGH_VIDEO_RENDERING
// For systems that have their own path to blitting video to the display, we
// simply punch a hole through our scene so that the video can appear there.
PunchThroughVideoNode::Builder builder(math::RectF(content_box_size()),
set_bounds_cb_);
Node* frame_node = new PunchThroughVideoNode(builder);
#else
AnimateNode::Builder animate_node_builder;
animate_node_builder.Add(
composition_node, base::Bind(AnimateCB, replace_image_cb_,
content_box_size()));
Node* frame_node = new AnimateNode(animate_node_builder, composition_node);
#endif // PUNCH_THROUGH_VIDEO_RENDERING
const cssom::MTMFunction* mtm_filter_function =
cssom::MTMFunction::ExtractFromFilterList(computed_style()->filter());
Node* content_node;
if (mtm_filter_function) {
const scoped_refptr<cssom::KeywordValue>& stereo_mode_keyword_value =
mtm_filter_function->stereo_mode();
render_tree::StereoMode stereo_mode =
ReadStereoMode(stereo_mode_keyword_value);
content_node = new FilterNode(MapToMeshFilter(stereo_mode), frame_node);
} else {
content_node = frame_node;
}
border_node_builder->AddChild(content_node);
}
void ReplacedBox::UpdateContentSizeAndMargins(
const LayoutParams& layout_params) {
base::optional<LayoutUnit> maybe_width = GetUsedWidthIfNotAuto(
computed_style(), layout_params.containing_block_size, NULL);
base::optional<LayoutUnit> maybe_height = GetUsedHeightIfNotAuto(
computed_style(), layout_params.containing_block_size);
base::optional<LayoutUnit> maybe_left = GetUsedLeftIfNotAuto(
computed_style(), layout_params.containing_block_size);
base::optional<LayoutUnit> maybe_top = GetUsedTopIfNotAuto(
computed_style(), layout_params.containing_block_size);
if (IsAbsolutelyPositioned()) {
// TODO: Implement CSS section 10.3.8, see
// https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width.
set_left(maybe_left.value_or(LayoutUnit()));
set_top(maybe_top.value_or(LayoutUnit()));
}
if (!maybe_width) {
if (!maybe_height) {
if (maybe_intrinsic_width_) {
// If "height" and "width" both have computed values of "auto" and
// the element also has an intrinsic width, then that intrinsic width
// is the used value of "width".
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
set_width(*maybe_intrinsic_width_);
} else if (maybe_intrinsic_height_) {
// If "height" and "width" both have computed values of "auto" and
// the element has no intrinsic width, but does have an intrinsic height
// and intrinsic ratio then the used value of "width" is:
// (intrinsic height) * (intrinsic ratio)
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
set_width(*maybe_intrinsic_height_ * intrinsic_ratio_);
} else {
// Otherwise, if "width" has a computed value of "auto", but none of
// the conditions above are met, then the used value of "width" becomes
// 300px.
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
set_width(LayoutUnit(kFallbackWidth));
}
} else {
// If "width" has a computed value of "auto", "height" has some other
// computed value, and the element does have an intrinsic ratio then
// the used value of "width" is:
// (used height) * (intrinsic ratio)
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
set_width(*maybe_height * intrinsic_ratio_);
}
} else {
set_width(*maybe_width);
}
if (!maybe_height) {
if (!maybe_width && maybe_intrinsic_height_) {
// If "height" and "width" both have computed values of "auto" and
// the element also has an intrinsic height, then that intrinsic height
// is the used value of "height".
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
set_height(*maybe_intrinsic_height_);
} else {
// Otherwise, if "height" has a computed value of "auto", and the element
// has an intrinsic ratio then the used value of "height" is:
// (used width) / (intrinsic ratio)
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
set_height(width() / intrinsic_ratio_);
}
} else {
set_height(*maybe_height);
}
if (!maybe_width && !maybe_height) {
// For replaced elements with an intrinsic ratio and both 'width' and
// 'height' specified as 'auto', the algorithm is as described in
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths.
base::optional<LayoutUnit> maybe_max_width = GetUsedMaxWidthIfNotNone(
computed_style(), layout_params.containing_block_size, NULL);
LayoutUnit min_width = GetUsedMinWidth(
computed_style(), layout_params.containing_block_size, NULL);
base::optional<LayoutUnit> maybe_max_height = GetUsedMaxHeightIfNotNone(
computed_style(), layout_params.containing_block_size, NULL);
LayoutUnit min_height = GetUsedMinHeight(
computed_style(), layout_params.containing_block_size, NULL);
// The values w and h stand for the results of the width and height
// computations ignoring the 'min-width', 'min-height', 'max-width' and
// 'max-height' properties. Normally these are the intrinsic width and
// height, but they may not be in the case of replaced elements with
// intrinsic ratios.
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
LayoutUnit w = width();
LayoutUnit h = height();
// Take the max-width and max-height as max(min, max) so that min <= max
// holds true.
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths
base::optional<LayoutUnit> max_height;
bool h_greater_than_max_height = false;
if (maybe_max_height) {
max_height = std::max(min_height, *maybe_max_height);
h_greater_than_max_height = h > *max_height;
}
base::optional<LayoutUnit> max_width;
bool w_greater_than_max_width = false;
if (maybe_max_width) {
max_width = std::max(min_width, *maybe_max_width);
w_greater_than_max_width = w > *max_width;
}
// This block sets resolved width and resolved height values according to
// the table listing a number of different constraint violations in
// https://www.w3.org/TR/CSS21/visudet.html#min-max-widths.
if (w_greater_than_max_width) {
if (h_greater_than_max_height) {
LayoutUnit max_width_ratio = *max_width / w.toFloat();
LayoutUnit max_height_ratio = *max_height / h.toFloat();
if (max_width_ratio > max_height_ratio) {
// Constraint: (w > max-width) and (h > max-height), where
// (max-width/w > max-height/h)
set_width(
std::max(min_width, *max_height * (w.toFloat() / h.toFloat())));
set_height(*max_height);
} else {
// Constraint: (w > max-width) and (h > max-height), where
// (max-width/w <= max-height/h)
set_width(*max_width);
set_height(
std::max(min_height, *max_width * (h.toFloat() / w.toFloat())));
}
} else { // not h_greater_than_max_height
if (h < min_height) {
// Constraint: (w > max-width) and (h < min-height)
set_width(*max_width);
set_height(min_height);
} else { // not h < min_height
// Constraint: w > max-width
set_width(*max_width);
set_height(
std::max(*max_width * (h.toFloat() / w.toFloat()), min_height));
}
}
} else { // not w_greater_than_max_width
if (w < min_width) {
if (h_greater_than_max_height) {
// Constraint: (w < min-width) and (h > max-height)
set_width(min_width);
set_height(*max_height);
} else { // not h_greater_than_max_height
if (h < min_height) {
LayoutUnit min_width_ratio = min_width / w.toFloat();
LayoutUnit min_height_ratio = min_height / h.toFloat();
if (min_width_ratio > min_height_ratio) {
// Constraint: (w < min-width) and (h < min-height), where
// (min-width/w > min-height/h)
set_width(min_width);
LayoutUnit height = min_width * (h.toFloat() / w.toFloat());
if (max_height) {
set_height(std::min(*max_height, height));
} else {
set_height(height);
}
} else {
// Constraint: (w < min-width) and (h < min-height), where
// (min-width/w <= min-height/h)
LayoutUnit width = min_height * (w.toFloat() / h.toFloat());
if (max_width) {
set_width(std::min(*max_width, width));
} else {
set_width(width);
}
set_height(min_height);
}
} else { // not h < min-height
// Constraint: w < min-width
set_width(min_width);
LayoutUnit height = min_width * (h.toFloat() / w.toFloat());
if (max_height) {
set_height(std::min(height, *max_height));
} else {
set_height(height);
}
}
}
} else { // not w < min_width
if (h_greater_than_max_height) {
// Constraint: h > max-height
set_width(
std::max(*max_height * (w.toFloat() / h.toFloat()), min_width));
set_height(*max_height);
} else { // not h_greater_than_max_height
if (h < min_height) {
// Constraint: h < min-height
LayoutUnit width = min_height * (w.toFloat() / h.toFloat());
if (max_width) {
set_width(std::min(width, *max_width));
} else {
set_width(width);
}
set_height(min_height);
} else { // not h < min_height
// Constraint: none
// Do nothing (keep w and h).
}
}
}
}
}
// The horizontal margin rules are difference for block level replaced boxes
// versus inline level replaced boxes.
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
// https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
base::optional<LayoutUnit> maybe_margin_left = GetUsedMarginLeftIfNotAuto(
computed_style(), layout_params.containing_block_size);
base::optional<LayoutUnit> maybe_margin_right = GetUsedMarginRightIfNotAuto(
computed_style(), layout_params.containing_block_size);
LayoutUnit border_box_width = GetBorderBoxWidth();
UpdateHorizontalMargins(layout_params.containing_block_size.width(),
border_box_width, maybe_margin_left,
maybe_margin_right);
base::optional<LayoutUnit> maybe_margin_top = GetUsedMarginTopIfNotAuto(
computed_style(), layout_params.containing_block_size);
base::optional<LayoutUnit> maybe_margin_bottom = GetUsedMarginBottomIfNotAuto(
computed_style(), layout_params.containing_block_size);
// If "margin-top", or "margin-bottom" are "auto", their used value is 0.
// https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
set_margin_top(maybe_margin_top.value_or(LayoutUnit()));
set_margin_bottom(maybe_margin_bottom.value_or(LayoutUnit()));
}
#ifdef COBALT_BOX_DUMP_ENABLED
void ReplacedBox::DumpProperties(std::ostream* stream) const {
Box::DumpProperties(stream);
*stream << "text_position=" << text_position_ << " "
<< "bidi_level=" << paragraph_->GetBidiLevel(text_position_) << " ";
}
#endif // COBALT_BOX_DUMP_ENABLED
} // namespace layout
} // namespace cobalt