| // 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, 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, 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))), |
| 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, 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, 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 |