blob: dfb4b448a863726809350aac55aef1966a8936c5 [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/layout_manager.h"
#include <algorithm>
#include <cmath>
#include <string>
#include "base/bind.h"
#include "base/debug/trace_event.h"
#include "base/memory/scoped_ptr.h"
#include "base/timer.h"
#include "cobalt/cssom/cascade_precedence.h"
#include "cobalt/dom/camera_3d.h"
#include "cobalt/dom/html_element_context.h"
#include "cobalt/dom/html_html_element.h"
#include "cobalt/layout/benchmark_stat_names.h"
#include "cobalt/layout/block_formatting_block_container_box.h"
#include "cobalt/layout/initial_containing_block.h"
#include "cobalt/layout/layout.h"
#include "cobalt/render_tree/animations/animate_node.h"
#include "cobalt/render_tree/matrix_transform_3d_node.h"
#include "third_party/icu/source/common/unicode/brkiter.h"
#include "third_party/icu/source/common/unicode/locid.h"
namespace cobalt {
namespace layout {
class LayoutManager::Impl : public dom::DocumentObserver {
public:
Impl(const std::string& name, const scoped_refptr<dom::Window>& window,
const OnRenderTreeProducedCallback& on_render_tree_produced,
const OnLayoutCallback& on_layout, LayoutTrigger layout_trigger,
int dom_max_element_depth, float layout_refresh_rate,
const std::string& language, bool enable_image_animations,
LayoutStatTracker* layout_stat_tracker);
~Impl();
// From dom::DocumentObserver.
void OnLoad() OVERRIDE;
void OnMutation() OVERRIDE;
void OnFocusChanged() OVERRIDE {}
// Called to perform a synchronous layout.
void DoSynchronousLayout();
void Suspend();
void Resume();
bool IsNewRenderTreePending() const;
private:
void StartLayoutTimer();
void DoLayoutAndProduceRenderTree();
#if defined(ENABLE_TEST_RUNNER)
void DoTestRunnerLayoutCallback();
#endif // ENABLE_TEST_RUNNER
const scoped_refptr<dom::Window> window_;
const icu::Locale locale_;
const scoped_ptr<UsedStyleProvider> used_style_provider_;
const OnRenderTreeProducedCallback on_render_tree_produced_callback_;
const OnLayoutCallback on_layout_callback_;
const LayoutTrigger layout_trigger_;
// This flag indicates whether or not we should do a re-layout. The flag
// is checked at a regular interval (e.g. 60Hz) and if it is set to true,
// a layout is initiated and it is set back to false. Events such as
// DOM mutations will set this flag back to true.
base::CVal<bool> layout_dirty_;
// Construction of |BreakIterator| requires a disk read, so we cache them
// in the layout manager in order to reuse them with all layouts happening
// in the context of one |WebModule|.
// http://userguide.icu-project.org/boundaryanalysis#TOC-Reuse
scoped_ptr<icu::BreakIterator> line_break_iterator_;
scoped_ptr<icu::BreakIterator> character_break_iterator_;
base::Timer layout_timer_;
int dom_max_element_depth_;
float layout_refresh_rate_;
LayoutStatTracker* const layout_stat_tracker_;
// The initial containing block is kept until the next layout, so that
// the box tree remains valid.
scoped_refptr<BlockLevelBlockContainerBox> initial_containing_block_;
bool suspended_;
DISALLOW_COPY_AND_ASSIGN(Impl);
};
namespace {
void UpdateCamera(
float width_to_height_aspect_ratio, scoped_refptr<input::Camera3D> camera,
float max_horizontal_fov_rad, float max_vertical_fov_rad,
render_tree::MatrixTransform3DNode::Builder* transform_node_builder,
base::TimeDelta time) {
UNREFERENCED_PARAMETER(time);
float vertical_fov_rad =
std::min(max_vertical_fov_rad,
2 * static_cast<float>(atan(tan(max_horizontal_fov_rad * 0.5f) /
width_to_height_aspect_ratio)));
camera->UpdatePerspective(width_to_height_aspect_ratio, vertical_fov_rad);
base::CameraTransform transform(
camera->GetCameraTransformAndUpdateOrientation());
DCHECK(!transform.right_eye);
transform_node_builder->transform =
transform.left_eye_or_mono.projection_matrix *
transform.left_eye_or_mono.view_matrix;
}
scoped_refptr<render_tree::Node> AttachCameraNodes(
const scoped_refptr<dom::Window> window,
const scoped_refptr<render_tree::Node>& source,
float max_horizontal_fov_rad, float max_vertical_fov_rad) {
// Attach a 3D transform node that applies the current camera matrix transform
// to the rest of the render tree.
scoped_refptr<render_tree::MatrixTransform3DNode> transform_node =
new render_tree::MatrixTransform3DNode(
source, glm::mat4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
// We setup an animation on the camera transform node such that the camera
// is driven by the renderer thread and can bypass layout entirely.
render_tree::animations::AnimateNode::Builder animate_node_builder;
animate_node_builder.Add(
transform_node,
base::Bind(&UpdateCamera, window->inner_width() / window->inner_height(),
window->camera_3d()->impl(), max_horizontal_fov_rad,
max_vertical_fov_rad));
return new render_tree::animations::AnimateNode(animate_node_builder,
transform_node);
}
} // namespace
LayoutManager::Impl::Impl(
const std::string& name, const scoped_refptr<dom::Window>& window,
const OnRenderTreeProducedCallback& on_render_tree_produced,
const OnLayoutCallback& on_layout, LayoutTrigger layout_trigger,
int dom_max_element_depth, float layout_refresh_rate,
const std::string& language, bool enable_image_animations,
LayoutStatTracker* layout_stat_tracker)
: window_(window),
locale_(icu::Locale::createCanonical(language.c_str())),
used_style_provider_(new UsedStyleProvider(
window->html_element_context(), window->document()->font_cache(),
base::Bind(&AttachCameraNodes, window), enable_image_animations)),
on_render_tree_produced_callback_(on_render_tree_produced),
on_layout_callback_(on_layout),
layout_trigger_(layout_trigger),
layout_dirty_(StringPrintf("%s.Layout.IsDirty", name.c_str()), true,
"Non-zero when the layout is dirty and a new render tree "
"is pending."),
layout_timer_(true, true, true),
dom_max_element_depth_(dom_max_element_depth),
layout_refresh_rate_(layout_refresh_rate),
layout_stat_tracker_(layout_stat_tracker),
suspended_(false) {
window_->document()->AddObserver(this);
window_->SetSynchronousLayoutCallback(
base::Bind(&Impl::DoSynchronousLayout, base::Unretained(this)));
UErrorCode status = U_ZERO_ERROR;
line_break_iterator_ =
make_scoped_ptr(icu::BreakIterator::createLineInstance(locale_, status));
CHECK(U_SUCCESS(status));
status = U_ZERO_ERROR;
character_break_iterator_ = make_scoped_ptr(
icu::BreakIterator::createCharacterInstance(locale_, status));
CHECK(U_SUCCESS(status));
#if defined(ENABLE_TEST_RUNNER)
if (layout_trigger_ == kTestRunnerMode) {
window_->test_runner()->set_trigger_layout_callback(
base::Bind(&LayoutManager::Impl::DoTestRunnerLayoutCallback,
base::Unretained(this)));
}
#endif // ENABLE_TEST_RUNNER
}
LayoutManager::Impl::~Impl() { window_->document()->RemoveObserver(this); }
void LayoutManager::Impl::OnLoad() {
#if defined(ENABLE_TEST_RUNNER)
if (layout_trigger_ != kTestRunnerMode) {
#else
{
#endif
// Start the layout timer. If the TestRunner is active, then we do not
// start a timer as the TestRunner will drive the triggering of layouts.
StartLayoutTimer();
}
#if defined(ENABLE_TEST_RUNNER)
if (layout_trigger_ == kTestRunnerMode &&
!window_->test_runner()->should_wait()) {
layout_dirty_ = true;
// Run the |DoLayoutAndProduceRenderTree| task after onload event finished.
MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&LayoutManager::Impl::DoLayoutAndProduceRenderTree,
base::Unretained(this)));
}
#endif // ENABLE_TEST_RUNNER
}
void LayoutManager::Impl::OnMutation() {
if (layout_trigger_ == kOnDocumentMutation) {
layout_dirty_ = true;
}
}
void LayoutManager::Impl::DoSynchronousLayout() {
TRACE_EVENT0("cobalt::layout", "LayoutManager::Impl::DoSynchronousLayout()");
if (suspended_) {
return;
}
layout::UpdateComputedStylesAndLayoutBoxTree(
locale_, window_->document(), dom_max_element_depth_,
used_style_provider_.get(), layout_stat_tracker_,
line_break_iterator_.get(), character_break_iterator_.get(),
&initial_containing_block_);
}
void LayoutManager::Impl::Suspend() {
// Mark that we are suspended so that we don't try to perform any layouts.
suspended_ = true;
// Invalidate any cached layout boxes from the document prior to clearing
// the initial containing block. That'll ensure that the full box tree is
// destroyed when the containing block is destroyed and that no children of
// |initial_containing_block_| are holding on to stale parent pointers.
window_->document()->InvalidateLayoutBoxes();
// Clear our reference to the initial containing block to allow any resources
// like images that were referenced by it to be released.
initial_containing_block_ = NULL;
}
void LayoutManager::Impl::Resume() {
// Mark that we are no longer suspended and indicate that the layout is
// dirty since when Suspend() was called we invalidated our previous layout.
layout_dirty_ = true;
suspended_ = false;
}
bool LayoutManager::Impl::IsNewRenderTreePending() const {
return layout_dirty_;
}
#if defined(ENABLE_TEST_RUNNER)
void LayoutManager::Impl::DoTestRunnerLayoutCallback() {
DCHECK_EQ(kTestRunnerMode, layout_trigger_);
layout_dirty_ = true;
if (layout_trigger_ == kTestRunnerMode &&
window_->test_runner()->should_wait()) {
TRACE_EVENT_BEGIN0("cobalt::layout", kBenchmarkStatNonMeasuredLayout);
}
DoLayoutAndProduceRenderTree();
if (layout_trigger_ == kTestRunnerMode &&
window_->test_runner()->should_wait()) {
TRACE_EVENT_END0("cobalt::layout", kBenchmarkStatNonMeasuredLayout);
}
}
#endif // ENABLE_TEST_RUNNER
void LayoutManager::Impl::StartLayoutTimer() {
// TODO: Eventually we would like to instead base our layouts off of a
// "refresh" signal generated by the rasterizer, instead of trying to
// match timers to the graphics' refresh rate, which is error prone.
const int64_t timer_interval_in_microseconds =
static_cast<int64_t>(base::Time::kMicrosecondsPerSecond * 1.0f /
(layout_refresh_rate_ + 1.0f));
layout_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMicroseconds(timer_interval_in_microseconds),
base::Bind(&LayoutManager::Impl::DoLayoutAndProduceRenderTree,
base::Unretained(this)));
}
void LayoutManager::Impl::DoLayoutAndProduceRenderTree() {
TRACE_EVENT0("cobalt::layout",
"LayoutManager::Impl::DoLayoutAndProduceRenderTree()");
if (suspended_) return;
const scoped_refptr<dom::Document>& document = window_->document();
if (!document->html()) {
return;
}
// Update the document's sample time, used for updating animations.
document->SampleTimelineTime();
bool has_layout_processing_started = false;
if (window_->HasPendingAnimationFrameCallbacks()) {
if (layout_dirty_) {
has_layout_processing_started = true;
TRACE_EVENT_BEGIN0("cobalt::layout", kBenchmarkStatLayout);
// Update our computed style before running animation callbacks, so that
// any transitioning elements adjusted during the animation callback will
// transition from their previously set value.
document->UpdateComputedStyles();
}
// Note that according to:
// https://www.w3.org/TR/2015/WD-web-animations-1-20150707/#model-liveness,
// "The time passed to a requestAnimationFrame callback will be equal to
// document.timeline.currentTime". In our case,
// document.timeline.currentTime is derived from the latest sample time.
window_->RunAnimationFrameCallbacks();
}
if (layout_dirty_) {
if (!has_layout_processing_started) {
// We want to catch the beginning of all layout processing. If it didn't
// begin before the call to RunAnimationFrameCallbacks(), then the flow
// starts here instead.
TRACE_EVENT_BEGIN0("cobalt::layout", kBenchmarkStatLayout);
}
scoped_refptr<render_tree::Node> render_tree_root = layout::Layout(
locale_, window_->document(), dom_max_element_depth_,
used_style_provider_.get(), layout_stat_tracker_,
line_break_iterator_.get(), character_break_iterator_.get(),
&initial_containing_block_);
bool run_on_render_tree_produced_callback = true;
#if defined(ENABLE_TEST_RUNNER)
if (layout_trigger_ == kTestRunnerMode &&
window_->test_runner()->should_wait()) {
run_on_render_tree_produced_callback = false;
}
#endif // ENABLE_TEST_RUNNER
if (run_on_render_tree_produced_callback) {
on_render_tree_produced_callback_.Run(LayoutResults(
render_tree_root, base::TimeDelta::FromMillisecondsD(
*document->timeline()->current_time())));
}
layout_dirty_ = false;
TRACE_EVENT_END0("cobalt::layout", kBenchmarkStatLayout);
}
on_layout_callback_.Run();
}
LayoutManager::LayoutManager(
const std::string& name, const scoped_refptr<dom::Window>& window,
const OnRenderTreeProducedCallback& on_render_tree_produced,
const OnLayoutCallback& on_layout, LayoutTrigger layout_trigger,
const int dom_max_element_depth, const float layout_refresh_rate,
const std::string& language, bool enable_image_animations,
LayoutStatTracker* layout_stat_tracker)
: impl_(new Impl(name, window, on_render_tree_produced, on_layout,
layout_trigger, dom_max_element_depth, layout_refresh_rate,
language, enable_image_animations, layout_stat_tracker)) {}
LayoutManager::~LayoutManager() {}
void LayoutManager::Suspend() { impl_->Suspend(); }
void LayoutManager::Resume() { impl_->Resume(); }
bool LayoutManager::IsNewRenderTreePending() const {
return impl_->IsNewRenderTreePending();
}
} // namespace layout
} // namespace cobalt