| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "flutter/flow/scene_update_context.h" |
| |
| #include <lib/ui/scenic/cpp/commands.h> |
| #include <lib/ui/scenic/cpp/view_token_pair.h> |
| |
| #include "flutter/flow/layers/layer.h" |
| #include "flutter/flow/matrix_decomposition.h" |
| #include "flutter/flow/view_holder.h" |
| #include "flutter/fml/trace_event.h" |
| #include "include/core/SkColor.h" |
| |
| namespace flutter { |
| namespace { |
| |
| void SetEntityNodeClipPlanes(scenic::EntityNode& entity_node, |
| const SkRect& bounds) { |
| const float top = bounds.top(); |
| const float bottom = bounds.bottom(); |
| const float left = bounds.left(); |
| const float right = bounds.right(); |
| |
| // We will generate 4 oriented planes, one for each edge of the bounding rect. |
| std::vector<fuchsia::ui::gfx::Plane3> clip_planes; |
| clip_planes.resize(4); |
| |
| // Top plane. |
| clip_planes[0].dist = top; |
| clip_planes[0].dir.x = 0.f; |
| clip_planes[0].dir.y = 1.f; |
| clip_planes[0].dir.z = 0.f; |
| |
| // Bottom plane. |
| clip_planes[1].dist = -bottom; |
| clip_planes[1].dir.x = 0.f; |
| clip_planes[1].dir.y = -1.f; |
| clip_planes[1].dir.z = 0.f; |
| |
| // Left plane. |
| clip_planes[2].dist = left; |
| clip_planes[2].dir.x = 1.f; |
| clip_planes[2].dir.y = 0.f; |
| clip_planes[2].dir.z = 0.f; |
| |
| // Right plane. |
| clip_planes[3].dist = -right; |
| clip_planes[3].dir.x = -1.f; |
| clip_planes[3].dir.y = 0.f; |
| clip_planes[3].dir.z = 0.f; |
| |
| entity_node.SetClipPlanes(std::move(clip_planes)); |
| } |
| |
| void SetMaterialColor(scenic::Material& material, |
| SkColor color, |
| SkAlpha opacity) { |
| const SkAlpha color_alpha = static_cast<SkAlpha>( |
| ((float)SkColorGetA(color) * (float)opacity) / 255.0f); |
| material.SetColor(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color), |
| color_alpha); |
| } |
| |
| } // namespace |
| |
| SceneUpdateContext::SceneUpdateContext(std::string debug_label, |
| fuchsia::ui::views::ViewToken view_token, |
| scenic::ViewRefPair view_ref_pair, |
| SessionWrapper& session, |
| bool intercept_all_input) |
| : session_(session), |
| root_view_(session_.get(), |
| std::move(view_token), |
| std::move(view_ref_pair.control_ref), |
| std::move(view_ref_pair.view_ref), |
| debug_label), |
| metrics_node_(session.get()), |
| layer_tree_node_(session_.get()) { |
| layer_tree_node_.SetLabel("Flutter::LayerTree"); |
| metrics_node_.SetLabel("Flutter::MetricsWatcher"); |
| metrics_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask); |
| metrics_node_.AddChild(layer_tree_node_); |
| root_view_.AddChild(metrics_node_); |
| |
| // Set up the input interceptor at the top of the scene, if applicable. It |
| // will capture all input, and any unwanted input will be reinjected into |
| // embedded views. |
| if (intercept_all_input) { |
| input_interceptor_node_.emplace(session_.get()); |
| input_interceptor_node_->SetLabel("Flutter::InputInterceptor"); |
| input_interceptor_node_->SetHitTestBehavior( |
| fuchsia::ui::gfx::HitTestBehavior::kDefault); |
| input_interceptor_node_->SetSemanticVisibility(false); |
| |
| metrics_node_.AddChild(input_interceptor_node_.value()); |
| } |
| |
| session_.Present(); |
| } |
| |
| std::vector<SceneUpdateContext::PaintTask> SceneUpdateContext::GetPaintTasks() { |
| std::vector<PaintTask> frame_paint_tasks = std::move(paint_tasks_); |
| |
| paint_tasks_.clear(); |
| |
| return frame_paint_tasks; |
| } |
| |
| void SceneUpdateContext::EnableWireframe(bool enable) { |
| session_.get()->Enqueue( |
| scenic::NewSetEnableDebugViewBoundsCmd(root_view_.id(), enable)); |
| } |
| |
| void SceneUpdateContext::Reset(const SkISize& frame_size, |
| float device_pixel_ratio) { |
| paint_tasks_.clear(); |
| top_entity_ = nullptr; |
| top_scale_x_ = 1.f; |
| top_scale_y_ = 1.f; |
| top_elevation_ = 0.f; |
| next_elevation_ = 0.f; |
| alpha_ = 1.f; |
| |
| // Adjust scene scaling to match the device pixel ratio. |
| const float inv_dpr = 1.0f / device_pixel_ratio; |
| layer_tree_node_.SetScale(inv_dpr, inv_dpr, 1.0f); |
| |
| // Set up the input interceptor at the top of the scene, if applicable. |
| if (input_interceptor_node_.has_value()) { |
| // TODO(fxb/): Don't hardcode elevation. |
| input_interceptor_node_->SetTranslation(frame_size.width() * 0.5f, |
| frame_size.height() * 0.5f, -100.f); |
| input_interceptor_node_->SetShape(scenic::Rectangle( |
| session_.get(), frame_size.width(), frame_size.height())); |
| } |
| |
| // We are going to be sending down a fresh node hierarchy every frame. So just |
| // enqueue a detach op on the layer tree node. |
| layer_tree_node_.DetachChildren(); |
| } |
| |
| void SceneUpdateContext::CreateFrame(scenic::EntityNode& entity_node, |
| const SkRRect& rrect, |
| SkColor color, |
| SkAlpha opacity, |
| const SkRect& paint_bounds, |
| std::vector<Layer*> paint_layers) { |
| // We don't need a shape if the frame is zero size. |
| if (rrect.isEmpty()) |
| return; |
| |
| // Frames always clip their children. |
| SkRect shape_bounds = rrect.getBounds(); |
| SetEntityNodeClipPlanes(entity_node, shape_bounds); |
| |
| // TODO(SCN-137): Need to be able to express the radii as vectors. |
| scenic::ShapeNode shape_node(session_.get()); |
| scenic::Rectangle shape(session_.get(), rrect.width(), rrect.height()); |
| shape_node.SetShape(shape); |
| shape_node.SetTranslation(shape_bounds.width() * 0.5f + shape_bounds.left(), |
| shape_bounds.height() * 0.5f + shape_bounds.top(), |
| 0.f); |
| |
| // Check whether the painted layers will be visible. |
| if (paint_bounds.isEmpty() || !paint_bounds.intersects(shape_bounds)) |
| paint_layers.clear(); |
| |
| scenic::Material material(session_.get()); |
| shape_node.SetMaterial(material); |
| entity_node.AddChild(shape_node); |
| |
| // Check whether a solid color will suffice. |
| if (paint_layers.empty()) { |
| SetMaterialColor(material, color, opacity); |
| } else { |
| // The final shape's color is material_color * texture_color. The passed in |
| // material color was already used as a background when generating the |
| // texture, so set the model color to |SK_ColorWHITE| in order to allow |
| // using the texture's color unmodified. |
| SetMaterialColor(material, SK_ColorWHITE, opacity); |
| |
| // Enqueue a paint task for these layers, to apply a texture to the whole |
| // shape. |
| // |
| // The task uses the |shape_bounds| as its rendering bounds instead of the |
| // |paint_bounds|. If the paint_bounds is large than the shape_bounds it |
| // will be clipped. |
| paint_tasks_.emplace_back(PaintTask{.paint_bounds = shape_bounds, |
| .scale_x = top_scale_x_, |
| .scale_y = top_scale_y_, |
| .background_color = color, |
| .material = std::move(material), |
| .layers = std::move(paint_layers)}); |
| } |
| } |
| |
| void SceneUpdateContext::UpdateView(int64_t view_id, |
| const SkPoint& offset, |
| const SkSize& size, |
| std::optional<bool> override_hit_testable) { |
| auto* view_holder = ViewHolder::FromId(view_id); |
| if (view_holder == nullptr) { |
| FML_LOG(ERROR) << "UpdateView did not find view holder for: " << view_id; |
| return; |
| } |
| |
| if (size.width() > 0.f && size.height() > 0.f) { |
| view_holder->SetProperties(size.width(), size.height(), 0, 0, 0, 0, |
| view_holder->focusable()); |
| } |
| |
| bool hit_testable = override_hit_testable.has_value() |
| ? *override_hit_testable |
| : view_holder->hit_testable(); |
| view_holder->UpdateScene(session_.get(), top_entity_->embedder_node(), offset, |
| size, SkScalarRoundToInt(alphaf() * 255), |
| hit_testable); |
| |
| // Assume embedded views are 10 "layers" wide. |
| next_elevation_ += 10 * kScenicZElevationBetweenLayers; |
| } |
| |
| void SceneUpdateContext::CreateView(int64_t view_id, |
| bool hit_testable, |
| bool focusable) { |
| FML_LOG(INFO) << "CreateView for view holder: " << view_id; |
| zx_handle_t handle = (zx_handle_t)view_id; |
| flutter::ViewHolder::Create(handle, nullptr, |
| scenic::ToViewHolderToken(zx::eventpair(handle)), |
| nullptr); |
| auto* view_holder = ViewHolder::FromId(view_id); |
| FML_DCHECK(view_holder); |
| |
| view_holder->set_hit_testable(hit_testable); |
| view_holder->set_focusable(focusable); |
| } |
| |
| void SceneUpdateContext::UpdateView(int64_t view_id, |
| bool hit_testable, |
| bool focusable) { |
| auto* view_holder = ViewHolder::FromId(view_id); |
| if (view_holder == nullptr) { |
| FML_LOG(ERROR) << "UpdateView did not find view holder for: " << view_id; |
| return; |
| } |
| |
| view_holder->set_hit_testable(hit_testable); |
| view_holder->set_focusable(focusable); |
| } |
| |
| void SceneUpdateContext::DestroyView(int64_t view_id) { |
| ViewHolder::Destroy(view_id); |
| } |
| |
| SceneUpdateContext::Entity::Entity(std::shared_ptr<SceneUpdateContext> context) |
| : context_(context), |
| previous_entity_(context->top_entity_), |
| entity_node_(context->session_.get()) { |
| context->top_entity_ = this; |
| } |
| |
| SceneUpdateContext::Entity::~Entity() { |
| if (previous_entity_) { |
| previous_entity_->embedder_node().AddChild(entity_node_); |
| } else { |
| context_->layer_tree_node_.AddChild(entity_node_); |
| } |
| |
| FML_DCHECK(context_->top_entity_ == this); |
| context_->top_entity_ = previous_entity_; |
| } |
| |
| SceneUpdateContext::Transform::Transform( |
| std::shared_ptr<SceneUpdateContext> context, |
| const SkMatrix& transform) |
| : Entity(context), |
| previous_scale_x_(context->top_scale_x_), |
| previous_scale_y_(context->top_scale_y_) { |
| entity_node().SetLabel("flutter::Transform"); |
| if (!transform.isIdentity()) { |
| // TODO(SCN-192): The perspective and shear components in the matrix |
| // are not handled correctly. |
| MatrixDecomposition decomposition(transform); |
| if (decomposition.IsValid()) { |
| // Don't allow clients to control the z dimension; we control that |
| // instead to make sure layers appear in proper order. |
| entity_node().SetTranslation(decomposition.translation().x, // |
| decomposition.translation().y, // |
| 0.f // |
| ); |
| |
| entity_node().SetScale(decomposition.scale().x, // |
| decomposition.scale().y, // |
| 1.f // |
| ); |
| context->top_scale_x_ *= decomposition.scale().x; |
| context->top_scale_y_ *= decomposition.scale().y; |
| |
| entity_node().SetRotation(decomposition.rotation().x, // |
| decomposition.rotation().y, // |
| decomposition.rotation().z, // |
| decomposition.rotation().w // |
| ); |
| } |
| } |
| } |
| |
| SceneUpdateContext::Transform::Transform( |
| std::shared_ptr<SceneUpdateContext> context, |
| float scale_x, |
| float scale_y, |
| float scale_z) |
| : Entity(context), |
| previous_scale_x_(context->top_scale_x_), |
| previous_scale_y_(context->top_scale_y_) { |
| entity_node().SetLabel("flutter::Transform"); |
| if (scale_x != 1.f || scale_y != 1.f || scale_z != 1.f) { |
| entity_node().SetScale(scale_x, scale_y, scale_z); |
| context->top_scale_x_ *= scale_x; |
| context->top_scale_y_ *= scale_y; |
| } |
| } |
| |
| SceneUpdateContext::Transform::~Transform() { |
| context()->top_scale_x_ = previous_scale_x_; |
| context()->top_scale_y_ = previous_scale_y_; |
| } |
| |
| SceneUpdateContext::Frame::Frame(std::shared_ptr<SceneUpdateContext> context, |
| const SkRRect& rrect, |
| SkColor color, |
| SkAlpha opacity, |
| std::string label) |
| : Entity(context), |
| previous_elevation_(context->top_elevation_), |
| rrect_(rrect), |
| color_(color), |
| opacity_(opacity), |
| opacity_node_(context->session_.get()), |
| paint_bounds_(SkRect::MakeEmpty()) { |
| // Increment elevation trackers before calculating any local elevation. |
| // |UpdateView| can modify context->next_elevation_, which is why it is |
| // necessary to track this additional state. |
| context->top_elevation_ += kScenicZElevationBetweenLayers; |
| context->next_elevation_ += kScenicZElevationBetweenLayers; |
| |
| float local_elevation = context->next_elevation_ - previous_elevation_; |
| entity_node().SetTranslation(0.f, 0.f, -local_elevation); |
| entity_node().SetLabel(label); |
| entity_node().AddChild(opacity_node_); |
| |
| // Scenic currently lacks an API to enable rendering of alpha channel; alpha |
| // channels are only rendered if there is a OpacityNode higher in the tree |
| // with opacity != 1. For now, clamp to a infinitesimally smaller value than |
| // 1, which does not cause visual problems in practice. |
| opacity_node_.SetOpacity(std::min(kOneMinusEpsilon, opacity_ / 255.0f)); |
| } |
| |
| SceneUpdateContext::Frame::~Frame() { |
| context()->top_elevation_ = previous_elevation_; |
| |
| // Add a part which represents the frame's geometry for clipping purposes |
| context()->CreateFrame(entity_node(), rrect_, color_, opacity_, paint_bounds_, |
| std::move(paint_layers_)); |
| } |
| |
| void SceneUpdateContext::Frame::AddPaintLayer(Layer* layer) { |
| FML_DCHECK(!layer->is_empty()); |
| paint_layers_.push_back(layer); |
| paint_bounds_.join(layer->paint_bounds()); |
| } |
| |
| SceneUpdateContext::Clip::Clip(std::shared_ptr<SceneUpdateContext> context, |
| const SkRect& shape_bounds) |
| : Entity(context) { |
| entity_node().SetLabel("flutter::Clip"); |
| SetEntityNodeClipPlanes(entity_node(), shape_bounds); |
| } |
| |
| } // namespace flutter |