| // Copyright 2016 The Chromium 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 "chrome/browser/vr/elements/ui_element.h" |
| |
| #include <limits> |
| |
| #include "base/logging.h" |
| #include "base/numerics/ranges.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/vr/input_event.h" |
| #include "chrome/browser/vr/model/camera_model.h" |
| #include "chrome/browser/vr/vr_gl_util.h" |
| #include "third_party/skia/include/core/SkRRect.h" |
| #include "third_party/skia/include/core/SkRect.h" |
| #include "ui/gfx/geometry/angle_conversions.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| |
| namespace vr { |
| |
| namespace { |
| |
| constexpr float kHitTestResolutionInMeter = 0.000001f; |
| constexpr gfx::RectF kRelativeFullRectClip = {-0.5f, 0.5f, 1.0f, 1.0f}; |
| |
| int AllocateId() { |
| static int g_next_id = 1; |
| return g_next_id++; |
| } |
| |
| bool GetRayPlaneDistance(const gfx::Point3F& ray_origin, |
| const gfx::Vector3dF& ray_vector, |
| const gfx::Point3F& plane_origin, |
| const gfx::Vector3dF& plane_normal, |
| float* distance) { |
| float denom = gfx::DotProduct(-ray_vector, plane_normal); |
| if (denom == 0) { |
| return false; |
| } |
| gfx::Vector3dF rel = ray_origin - plane_origin; |
| *distance = gfx::DotProduct(plane_normal, rel) / denom; |
| return true; |
| } |
| |
| #ifndef NDEBUG |
| constexpr char kRed[] = "\x1b[31m"; |
| constexpr char kGreen[] = "\x1b[32m"; |
| constexpr char kBlue[] = "\x1b[34m"; |
| constexpr char kCyan[] = "\x1b[36m"; |
| constexpr char kYellow[] = "\x1b[33m"; |
| constexpr char kReset[] = "\x1b[0m"; |
| |
| void DumpTransformOperations(const cc::TransformOperations& ops, |
| std::ostringstream* os) { |
| if (!ops.at(0).IsIdentity()) { |
| const auto& translate = ops.at(0).translate; |
| *os << "t(" << translate.x << ", " << translate.y << ", " << translate.z |
| << ") "; |
| } |
| |
| if (ops.size() < 2u) { |
| return; |
| } |
| |
| if (!ops.at(1).IsIdentity()) { |
| const auto& rotate = ops.at(1).rotate; |
| if (rotate.axis.x > 0.0f) { |
| *os << "rx(" << rotate.angle << ") "; |
| } else if (rotate.axis.y > 0.0f) { |
| *os << "ry(" << rotate.angle << ") "; |
| } else if (rotate.axis.z > 0.0f) { |
| *os << "rz(" << rotate.angle << ") "; |
| } |
| } |
| |
| if (!ops.at(2).IsIdentity()) { |
| const auto& scale = ops.at(2).scale; |
| *os << "s(" << scale.x << ", " << scale.y << ", " << scale.z << ") "; |
| } |
| } |
| #endif |
| |
| } // namespace |
| |
| EventHandlers::EventHandlers() = default; |
| EventHandlers::~EventHandlers() = default; |
| EventHandlers::EventHandlers(const EventHandlers& other) = default; |
| |
| UiElement::UiElement() : id_(AllocateId()) { |
| animation_.set_target(this); |
| layout_offset_.AppendTranslate(0, 0, 0); |
| transform_operations_.AppendTranslate(0, 0, 0); |
| transform_operations_.AppendRotate(1, 0, 0, 0); |
| transform_operations_.AppendScale(1, 1, 1); |
| } |
| |
| UiElement::~UiElement() { |
| animation_.set_target(nullptr); |
| } |
| |
| void UiElement::SetName(UiElementName name) { |
| name_ = name; |
| OnSetName(); |
| } |
| |
| void UiElement::OnSetName() {} |
| |
| void UiElement::SetType(UiElementType type) { |
| type_ = type; |
| OnSetType(); |
| } |
| |
| UiElement* UiElement::GetDescendantByType(UiElementType type) { |
| if (type_ == type) |
| return this; |
| |
| for (auto& child : children_) { |
| auto* result = child->GetDescendantByType(type); |
| if (result) |
| return result; |
| } |
| return nullptr; |
| } |
| |
| void UiElement::OnSetType() {} |
| |
| void UiElement::SetDrawPhase(DrawPhase draw_phase) { |
| draw_phase_ = draw_phase; |
| OnSetDrawPhase(); |
| } |
| |
| void UiElement::OnSetDrawPhase() {} |
| |
| void UiElement::set_focusable(bool focusable) { |
| focusable_ = focusable; |
| OnSetFocusable(); |
| } |
| |
| void UiElement::OnSetFocusable() {} |
| |
| void UiElement::Render(UiElementRenderer* renderer, |
| const CameraModel& model) const { |
| // Elements without an overridden implementation of Render should have their |
| // draw phase set to kPhaseNone and should, consequently, be filtered out when |
| // the UiRenderer collects elements to draw. Therefore, if we invoke this |
| // function, it is an error. |
| NOTREACHED() << "element: " << DebugName(); |
| } |
| |
| void UiElement::Initialize(SkiaSurfaceProvider* provider) {} |
| |
| void UiElement::OnHoverEnter(const gfx::PointF& position, |
| base::TimeTicks timestamp) { |
| if (GetSounds().hover_enter != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().hover_enter); |
| } |
| |
| if (event_handlers_.hover_enter) { |
| event_handlers_.hover_enter.Run(); |
| } else if (parent() && bubble_events()) { |
| parent()->OnHoverEnter(position, timestamp); |
| } |
| } |
| |
| void UiElement::OnHoverLeave(base::TimeTicks timestamp) { |
| if (GetSounds().hover_leave != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().hover_leave); |
| } |
| if (event_handlers_.hover_leave) { |
| event_handlers_.hover_leave.Run(); |
| } else if (parent() && bubble_events()) { |
| parent()->OnHoverLeave(timestamp); |
| } |
| } |
| |
| void UiElement::OnHoverMove(const gfx::PointF& position, |
| base::TimeTicks timestamp) { |
| if (GetSounds().hover_move != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().hover_move); |
| } |
| if (event_handlers_.hover_move) { |
| event_handlers_.hover_move.Run(position); |
| } else if (parent() && bubble_events()) { |
| parent()->OnHoverMove(position, timestamp); |
| } |
| } |
| |
| void UiElement::OnButtonDown(const gfx::PointF& position, |
| base::TimeTicks timestamp) { |
| if (GetSounds().button_down != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().button_down); |
| } |
| if (event_handlers_.button_down) { |
| event_handlers_.button_down.Run(); |
| } else if (parent() && bubble_events()) { |
| parent()->OnButtonDown(position, timestamp); |
| } |
| } |
| |
| void UiElement::OnButtonUp(const gfx::PointF& position, |
| base::TimeTicks timestamp) { |
| if (GetSounds().button_up != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().button_up); |
| } |
| if (event_handlers_.button_up) { |
| event_handlers_.button_up.Run(); |
| } else if (parent() && bubble_events()) { |
| parent()->OnButtonUp(position, timestamp); |
| } |
| } |
| |
| void UiElement::OnTouchMove(const gfx::PointF& position, |
| base::TimeTicks timestamp) { |
| if (GetSounds().touch_move != kSoundNone && audio_delegate_) { |
| audio_delegate_->PlaySound(GetSounds().touch_move); |
| } |
| if (event_handlers_.touch_move) { |
| event_handlers_.touch_move.Run(position); |
| } else if (parent() && bubble_events()) { |
| parent()->OnTouchMove(position, timestamp); |
| } |
| } |
| |
| void UiElement::OnFlingCancel(std::unique_ptr<InputEvent> gesture, |
| const gfx::PointF& position) {} |
| void UiElement::OnScrollBegin(std::unique_ptr<InputEvent> gesture, |
| const gfx::PointF& position) {} |
| void UiElement::OnScrollUpdate(std::unique_ptr<InputEvent> gesture, |
| const gfx::PointF& position) {} |
| void UiElement::OnScrollEnd(std::unique_ptr<InputEvent> gesture, |
| const gfx::PointF& position) {} |
| |
| void UiElement::OnFocusChanged(bool focused) { |
| NOTREACHED(); |
| } |
| |
| void UiElement::OnInputEdited(const EditedText& info) { |
| NOTREACHED(); |
| } |
| |
| void UiElement::OnInputCommitted(const EditedText& info) { |
| NOTREACHED(); |
| } |
| |
| void UiElement::RequestFocus() { |
| NOTREACHED(); |
| } |
| |
| void UiElement::RequestUnfocus() { |
| NOTREACHED(); |
| } |
| |
| void UiElement::UpdateInput(const EditedText& info) { |
| NOTREACHED(); |
| } |
| |
| bool UiElement::DoBeginFrame(const gfx::Transform& head_pose, |
| bool force_animations_to_completion) { |
| // TODO(mthiesse): This is overly cautious. We may have keyframe_models but |
| // not trigger any updates, so we should refine this logic and have |
| // Animation::Tick return a boolean. Similarly, the bindings update may have |
| // had no visual effect and dirtiness should be related to setting properties |
| // that do indeed cause visual updates. |
| bool keyframe_models_updated = animation_.keyframe_models().size() > 0; |
| if (force_animations_to_completion) { |
| animation_.FinishAll(); |
| } else { |
| animation_.Tick(last_frame_time_); |
| } |
| set_update_phase(kUpdatedAnimations); |
| bool begin_frame_updated = OnBeginFrame(head_pose); |
| UpdateComputedOpacity(); |
| bool was_visible_at_any_point = IsVisible() || |
| updated_visibility_this_frame_ || |
| IsOrWillBeLocallyVisible(); |
| bool dirty = (begin_frame_updated || keyframe_models_updated || |
| updated_bindings_this_frame_) && |
| was_visible_at_any_point; |
| |
| if (was_visible_at_any_point) { |
| for (auto& child : children_) |
| dirty |= child->DoBeginFrame(head_pose, force_animations_to_completion); |
| } |
| |
| return dirty; |
| } |
| |
| bool UiElement::OnBeginFrame(const gfx::Transform& head_pose) { |
| return false; |
| } |
| |
| bool UiElement::PrepareToDraw() { |
| return false; |
| } |
| |
| bool UiElement::HasDirtyTexture() const { |
| return false; |
| } |
| |
| void UiElement::UpdateTexture() {} |
| |
| bool UiElement::IsHitTestable() const { |
| return IsVisible() && hit_testable_; |
| } |
| |
| void UiElement::SetSize(float width, float height) { |
| animation_.TransitionSizeTo(last_frame_time_, BOUNDS, size_, |
| gfx::SizeF(width, height)); |
| OnSetSize(gfx::SizeF(width, height)); |
| } |
| |
| void UiElement::OnSetSize(const gfx::SizeF& size) {} |
| |
| void UiElement::SetVisible(bool visible) { |
| SetOpacity(visible ? opacity_when_visible_ : 0.0); |
| } |
| |
| void UiElement::SetVisibleImmediately(bool visible) { |
| opacity_ = visible ? opacity_when_visible_ : 0.0; |
| animation_.RemoveKeyframeModels(OPACITY); |
| } |
| |
| bool UiElement::IsVisible() const { |
| // Many things rely on checking element visibility, including tests. |
| // Therefore, support reporting visibility even if an element sits in an |
| // invisible portion of the tree. We can infer that if the scene computed |
| // opacities, but this element did not, it must be invisible. |
| DCHECK(update_phase_ >= kUpdatedComputedOpacity || |
| FrameLifecycle::phase() >= kUpdatedComputedOpacity); |
| // TODO(crbug.com/832216): we shouldn't need to check opacity() here. |
| return update_phase_ >= kUpdatedComputedOpacity && opacity() > 0.0f && |
| computed_opacity() > 0.0f; |
| } |
| |
| bool UiElement::IsVisibleAndOpaque() const { |
| DCHECK(update_phase_ >= kUpdatedComputedOpacity || |
| FrameLifecycle::phase() >= kUpdatedComputedOpacity); |
| // TODO(crbug.com/832216): we shouldn't need to check opacity() here. |
| return update_phase_ >= kUpdatedComputedOpacity && opacity() == 1.0f && |
| computed_opacity() == 1.0f; |
| } |
| |
| bool UiElement::IsOrWillBeLocallyVisible() const { |
| return opacity() > 0.0f || GetTargetOpacity() > 0.0f; |
| } |
| |
| gfx::SizeF UiElement::size() const { |
| DCHECK_LE(kUpdatedSize, update_phase_); |
| return size_; |
| } |
| |
| void UiElement::SetLayoutOffset(float x, float y) { |
| if (x_centering() == LEFT) { |
| x += size_.width() / 2; |
| if (!bounds_contain_padding_) |
| x -= left_padding_; |
| } else if (x_centering() == RIGHT) { |
| x -= size_.width() / 2; |
| if (!bounds_contain_padding_) |
| x += right_padding_; |
| } |
| if (y_centering() == TOP) { |
| y -= size_.height() / 2; |
| if (!bounds_contain_padding_) |
| y += top_padding_; |
| } else if (y_centering() == BOTTOM) { |
| y += size_.height() / 2; |
| if (!bounds_contain_padding_) |
| y -= bottom_padding_; |
| } |
| |
| if (x == layout_offset_.at(0).translate.x && |
| y == layout_offset_.at(0).translate.y && |
| !IsAnimatingProperty(LAYOUT_OFFSET)) { |
| return; |
| } |
| |
| cc::TransformOperations operations = layout_offset_; |
| cc::TransformOperation& op = operations.at(0); |
| op.translate = {x, y, 0}; |
| op.Bake(); |
| animation_.TransitionTransformOperationsTo(last_frame_time_, LAYOUT_OFFSET, |
| layout_offset_, operations); |
| } |
| |
| void UiElement::SetTranslate(float x, float y, float z) { |
| if (x == transform_operations_.at(kTranslateIndex).translate.x && |
| y == transform_operations_.at(kTranslateIndex).translate.y && |
| z == transform_operations_.at(kTranslateIndex).translate.z && |
| !IsAnimatingProperty(TRANSFORM)) { |
| return; |
| } |
| |
| cc::TransformOperations operations = transform_operations_; |
| cc::TransformOperation& op = operations.at(kTranslateIndex); |
| op.translate = {x, y, z}; |
| op.Bake(); |
| animation_.TransitionTransformOperationsTo(last_frame_time_, TRANSFORM, |
| transform_operations_, operations); |
| } |
| |
| void UiElement::SetRotate(float x, float y, float z, float radians) { |
| float degrees = gfx::RadToDeg(radians); |
| |
| if (x == transform_operations_.at(kRotateIndex).rotate.axis.x && |
| y == transform_operations_.at(kRotateIndex).rotate.axis.y && |
| z == transform_operations_.at(kRotateIndex).rotate.axis.z && |
| degrees == transform_operations_.at(kRotateIndex).rotate.angle && |
| !IsAnimatingProperty(TRANSFORM)) { |
| return; |
| } |
| |
| cc::TransformOperations operations = transform_operations_; |
| cc::TransformOperation& op = operations.at(kRotateIndex); |
| op.rotate.axis = {x, y, z}; |
| op.rotate.angle = degrees; |
| op.Bake(); |
| animation_.TransitionTransformOperationsTo(last_frame_time_, TRANSFORM, |
| transform_operations_, operations); |
| } |
| |
| void UiElement::SetScale(float x, float y, float z) { |
| if (x == transform_operations_.at(kScaleIndex).scale.x && |
| y == transform_operations_.at(kScaleIndex).scale.y && |
| z == transform_operations_.at(kScaleIndex).scale.z && |
| !IsAnimatingProperty(TRANSFORM)) { |
| return; |
| } |
| |
| cc::TransformOperations operations = transform_operations_; |
| cc::TransformOperation& op = operations.at(kScaleIndex); |
| op.scale = {x, y, z}; |
| op.Bake(); |
| animation_.TransitionTransformOperationsTo(last_frame_time_, TRANSFORM, |
| transform_operations_, operations); |
| } |
| |
| void UiElement::SetOpacity(float opacity) { |
| animation_.TransitionFloatTo(last_frame_time_, OPACITY, opacity_, opacity); |
| } |
| |
| void UiElement::SetCornerRadii(const CornerRadii& radii) { |
| corner_radii_ = radii; |
| OnSetCornerRadii(radii); |
| } |
| |
| void UiElement::OnSetCornerRadii(const CornerRadii& radii) {} |
| |
| gfx::SizeF UiElement::GetTargetSize() const { |
| return animation_.GetTargetSizeValue(TargetProperty::BOUNDS, size_); |
| } |
| |
| cc::TransformOperations UiElement::GetTargetTransform() const { |
| return animation_.GetTargetTransformOperationsValue(TargetProperty::TRANSFORM, |
| transform_operations_); |
| } |
| |
| gfx::Transform UiElement::ComputeTargetWorldSpaceTransform() const { |
| gfx::Transform m; |
| for (const UiElement* current = this; current; current = current->parent()) { |
| m.ConcatTransform(current->GetTargetLocalTransform()); |
| } |
| return m; |
| } |
| |
| float UiElement::GetTargetOpacity() const { |
| return animation_.GetTargetFloatValue(TargetProperty::OPACITY, opacity_); |
| } |
| |
| float UiElement::ComputeTargetOpacity() const { |
| float opacity = 1.0; |
| for (const UiElement* current = this; current; current = current->parent()) { |
| opacity *= current->GetTargetOpacity(); |
| } |
| return opacity; |
| } |
| |
| float UiElement::computed_opacity() const { |
| DCHECK_LE(kUpdatedComputedOpacity, update_phase_) << DebugName(); |
| return computed_opacity_; |
| } |
| |
| float UiElement::ComputedAndLocalOpacityForTest() const { |
| return computed_opacity(); |
| } |
| |
| bool UiElement::LocalHitTest(const gfx::PointF& point) const { |
| if (!gfx::RectF(0.0f, 0.0f, 1.0f, 1.0f).Contains(point) || |
| !GetClipRect().Contains(point)) |
| return false; |
| |
| if (corner_radii_.IsZero()) |
| return true; |
| |
| float width = size().width(); |
| float height = size().height(); |
| SkRRect rrect; |
| SkVector radii[4] = { |
| {corner_radii_.upper_left, corner_radii_.upper_left}, |
| {corner_radii_.upper_right, corner_radii_.upper_right}, |
| {corner_radii_.lower_right, corner_radii_.lower_right}, |
| {corner_radii_.lower_left, corner_radii_.lower_left}, |
| }; |
| rrect.setRectRadii(SkRect::MakeWH(width, height), radii); |
| |
| float left = std::min(point.x() * width, width - kHitTestResolutionInMeter); |
| float top = std::min(point.y() * height, height - kHitTestResolutionInMeter); |
| SkRect point_rect = |
| SkRect::MakeLTRB(left, top, left + kHitTestResolutionInMeter, |
| top + kHitTestResolutionInMeter); |
| return rrect.contains(point_rect); |
| } |
| |
| void UiElement::HitTest(const HitTestRequest& request, |
| HitTestResult* result) const { |
| gfx::Vector3dF ray_vector = request.ray_target - request.ray_origin; |
| ray_vector.GetNormalized(&ray_vector); |
| result->type = HitTestResult::Type::kNone; |
| float distance_to_plane; |
| if (!GetRayDistance(request.ray_origin, ray_vector, &distance_to_plane)) { |
| return; |
| } |
| |
| if (distance_to_plane < 0 || |
| distance_to_plane > request.max_distance_to_plane) { |
| return; |
| } |
| |
| result->type = HitTestResult::Type::kHitsPlane; |
| result->distance_to_plane = distance_to_plane; |
| result->hit_point = |
| request.ray_origin + gfx::ScaleVector3d(ray_vector, distance_to_plane); |
| gfx::PointF unit_xy_point = GetUnitRectangleCoordinates(result->hit_point); |
| result->local_hit_point.set_x(0.5f + unit_xy_point.x()); |
| result->local_hit_point.set_y(0.5f - unit_xy_point.y()); |
| if (LocalHitTest(result->local_hit_point)) { |
| result->type = HitTestResult::Type::kHits; |
| } |
| } |
| |
| const gfx::Transform& UiElement::world_space_transform() const { |
| DCHECK_LE(kUpdatedWorldSpaceTransform, update_phase_); |
| return world_space_transform_; |
| } |
| |
| bool UiElement::IsWorldPositioned() const { |
| return true; |
| } |
| |
| std::string UiElement::DebugName() const { |
| return base::StringPrintf( |
| "%s%s%s", |
| UiElementNameToString(name() == kNone ? owner_name_for_test() : name()) |
| .c_str(), |
| type() == kTypeNone ? "" : ":", |
| type() == kTypeNone ? "" : UiElementTypeToString(type()).c_str()); |
| } |
| |
| #ifndef NDEBUG |
| void DumpLines(const std::vector<size_t>& counts, |
| const std::vector<const UiElement*>& ancestors, |
| std::ostringstream* os) { |
| for (size_t i = 0; i < counts.size(); ++i) { |
| size_t current_count = counts[i]; |
| if (i + 1 < counts.size()) { |
| current_count++; |
| } |
| if (ancestors[ancestors.size() - i - 1]->children().size() > |
| current_count) { |
| *os << "| "; |
| } else { |
| *os << " "; |
| } |
| } |
| } |
| |
| void UiElement::DumpHierarchy(std::vector<size_t> counts, |
| std::ostringstream* os, |
| bool include_bindings) const { |
| // Put our ancestors in a vector for easy reverse traversal. |
| std::vector<const UiElement*> ancestors; |
| for (const UiElement* ancestor = parent(); ancestor; |
| ancestor = ancestor->parent()) { |
| ancestors.push_back(ancestor); |
| } |
| DCHECK_EQ(counts.size(), ancestors.size()); |
| |
| *os << kBlue; |
| for (size_t i = 0; i < counts.size(); ++i) { |
| if (i + 1 == counts.size()) { |
| *os << "+-"; |
| } else if (ancestors[ancestors.size() - i - 1]->children().size() > |
| counts[i] + 1) { |
| *os << "| "; |
| } else { |
| *os << " "; |
| } |
| } |
| *os << kReset; |
| |
| if (update_phase_ < kUpdatedComputedOpacity || !IsVisible()) { |
| *os << kBlue; |
| } |
| |
| *os << DebugName() << kReset << " " << kCyan << DrawPhaseToString(draw_phase_) |
| << " " << kReset; |
| |
| if (update_phase_ >= kUpdatedSize) { |
| if (size().width() != 0.0f || size().height() != 0.0f) { |
| *os << kRed << "[" << size().width() << ", " << size().height() << "] " |
| << kReset; |
| } |
| } |
| |
| if (update_phase_ >= kUpdatedWorldSpaceTransform) { |
| *os << kGreen; |
| DumpGeometry(os); |
| } |
| |
| counts.push_back(0u); |
| |
| if (include_bindings) { |
| std::ostringstream binding_stream; |
| for (auto& binding : bindings_) { |
| std::string binding_text = binding->ToString(); |
| if (binding_text.empty()) |
| continue; |
| binding_stream << binding->ToString() << std::endl; |
| } |
| |
| auto split_bindings = |
| base::SplitString(binding_stream.str(), "\n", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (!split_bindings.empty()) { |
| ancestors.insert(ancestors.begin(), this); |
| } |
| for (const auto& split : split_bindings) { |
| *os << std::endl << kBlue; |
| DumpLines(counts, ancestors, os); |
| *os << kGreen << split; |
| } |
| } |
| |
| *os << kReset << std::endl; |
| |
| for (auto& child : children_) { |
| child->DumpHierarchy(counts, os, include_bindings); |
| counts.back()++; |
| } |
| } |
| |
| void UiElement::DumpGeometry(std::ostringstream* os) const { |
| DumpTransformOperations(transform_operations_, os); |
| *os << kYellow; |
| DumpTransformOperations(layout_offset_, os); |
| } |
| #endif |
| |
| void UiElement::SetSounds(Sounds sounds, AudioDelegate* delegate) { |
| sounds_ = sounds; |
| audio_delegate_ = delegate; |
| } |
| |
| void UiElement::OnUpdatedWorldSpaceTransform() {} |
| |
| void UiElement::AddChild(std::unique_ptr<UiElement> child) { |
| for (UiElement* current = this; current; current = current->parent()) |
| current->set_descendants_updated(true); |
| child->parent_ = this; |
| children_.push_back(std::move(child)); |
| } |
| |
| std::unique_ptr<UiElement> UiElement::RemoveChild(UiElement* to_remove) { |
| return ReplaceChild(to_remove, nullptr); |
| } |
| |
| std::unique_ptr<UiElement> UiElement::ReplaceChild( |
| UiElement* to_remove, |
| std::unique_ptr<UiElement> to_add) { |
| for (UiElement* current = this; current; current = current->parent()) |
| current->set_descendants_updated(true); |
| DCHECK_EQ(this, to_remove->parent_); |
| to_remove->parent_ = nullptr; |
| size_t old_size = children_.size(); |
| |
| auto it = std::find_if(std::begin(children_), std::end(children_), |
| [to_remove](const std::unique_ptr<UiElement>& child) { |
| return child.get() == to_remove; |
| }); |
| DCHECK(it != std::end(children_)); |
| |
| std::unique_ptr<UiElement> removed(it->release()); |
| if (to_add) { |
| to_add->parent_ = this; |
| *it = std::move(to_add); |
| } else { |
| children_.erase(it); |
| DCHECK_EQ(old_size - 1, children_.size()); |
| } |
| return removed; |
| } |
| |
| void UiElement::AddBinding(std::unique_ptr<BindingBase> binding) { |
| bindings_.push_back(std::move(binding)); |
| } |
| |
| void UiElement::UpdateBindings() { |
| bool should_recur = IsOrWillBeLocallyVisible(); |
| updated_bindings_this_frame_ = false; |
| for (auto& binding : bindings_) { |
| if (binding->Update()) |
| updated_bindings_this_frame_ = true; |
| } |
| should_recur |= IsOrWillBeLocallyVisible(); |
| |
| set_update_phase(kUpdatedBindings); |
| if (!should_recur) |
| return; |
| |
| for (auto& child : children_) |
| child->UpdateBindings(); |
| } |
| |
| gfx::Point3F UiElement::GetCenter() const { |
| gfx::Point3F center; |
| world_space_transform_.TransformPoint(¢er); |
| return center; |
| } |
| |
| gfx::PointF UiElement::GetUnitRectangleCoordinates( |
| const gfx::Point3F& world_point) const { |
| gfx::Point3F origin; |
| gfx::Vector3dF x_axis(1, 0, 0); |
| gfx::Vector3dF y_axis(0, 1, 0); |
| world_space_transform_.TransformPoint(&origin); |
| world_space_transform_.TransformVector(&x_axis); |
| world_space_transform_.TransformVector(&y_axis); |
| gfx::Vector3dF origin_to_world = world_point - origin; |
| float x = gfx::DotProduct(origin_to_world, x_axis) / |
| gfx::DotProduct(x_axis, x_axis); |
| float y = gfx::DotProduct(origin_to_world, y_axis) / |
| gfx::DotProduct(y_axis, y_axis); |
| return gfx::PointF(x, y); |
| } |
| |
| gfx::Vector3dF UiElement::GetNormal() const { |
| gfx::Vector3dF x_axis(1, 0, 0); |
| gfx::Vector3dF y_axis(0, 1, 0); |
| world_space_transform_.TransformVector(&x_axis); |
| world_space_transform_.TransformVector(&y_axis); |
| gfx::Vector3dF normal = CrossProduct(x_axis, y_axis); |
| normal.GetNormalized(&normal); |
| return normal; |
| } |
| |
| bool UiElement::GetRayDistance(const gfx::Point3F& ray_origin, |
| const gfx::Vector3dF& ray_vector, |
| float* distance) const { |
| return GetRayPlaneDistance(ray_origin, ray_vector, GetCenter(), GetNormal(), |
| distance); |
| } |
| |
| void UiElement::NotifyClientFloatAnimated(float value, |
| int target_property_id, |
| cc::KeyframeModel* keyframe_model) { |
| opacity_ = base::ClampToRange(value, 0.0f, 1.0f); |
| } |
| |
| void UiElement::NotifyClientTransformOperationsAnimated( |
| const cc::TransformOperations& operations, |
| int target_property_id, |
| cc::KeyframeModel* keyframe_model) { |
| if (target_property_id == TRANSFORM) { |
| transform_operations_ = operations; |
| } else if (target_property_id == LAYOUT_OFFSET) { |
| layout_offset_ = operations; |
| } else { |
| NOTREACHED(); |
| } |
| local_transform_ = layout_offset_.Apply() * transform_operations_.Apply(); |
| world_space_transform_dirty_ = true; |
| } |
| |
| void UiElement::NotifyClientSizeAnimated(const gfx::SizeF& size, |
| int target_property_id, |
| cc::KeyframeModel* keyframe_model) { |
| if (size_ == size) |
| return; |
| size_ = size; |
| world_space_transform_dirty_ = true; |
| } |
| |
| void UiElement::SetTransitionedProperties( |
| const std::set<TargetProperty>& properties) { |
| std::set<int> converted_properties(properties.begin(), properties.end()); |
| animation_.SetTransitionedProperties(converted_properties); |
| } |
| |
| void UiElement::SetTransitionDuration(base::TimeDelta delta) { |
| animation_.SetTransitionDuration(delta); |
| } |
| |
| void UiElement::AddKeyframeModel( |
| std::unique_ptr<cc::KeyframeModel> keyframe_model) { |
| animation_.AddKeyframeModel(std::move(keyframe_model)); |
| } |
| |
| void UiElement::RemoveKeyframeModel(int keyframe_model_id) { |
| animation_.RemoveKeyframeModel(keyframe_model_id); |
| } |
| |
| void UiElement::RemoveKeyframeModels(int target_property) { |
| animation_.RemoveKeyframeModels(target_property); |
| } |
| |
| bool UiElement::IsAnimatingProperty(TargetProperty property) const { |
| return animation_.IsAnimatingProperty(static_cast<int>(property)); |
| } |
| |
| bool UiElement::SizeAndLayOut() { |
| if (!IsVisible() && !IsOrWillBeLocallyVisible()) |
| return false; |
| |
| // May be overridden by layout elements. |
| bool changed = SizeAndLayOutChildren(); |
| |
| changed |= PrepareToDraw(); |
| |
| LayOutContributingChildren(); |
| |
| if (bounds_contain_children_) { |
| gfx::RectF bounds = ComputeContributingChildrenBounds(); |
| if (bounds.size() != GetTargetSize()) |
| SetSize(bounds.width(), bounds.height()); |
| } else { |
| DCHECK_EQ(0.0f, right_padding_); |
| DCHECK_EQ(0.0f, left_padding_); |
| DCHECK_EQ(0.0f, top_padding_); |
| DCHECK_EQ(0.0f, bottom_padding_); |
| } |
| set_update_phase(kUpdatedSize); |
| |
| LayOutNonContributingChildren(); |
| |
| if (clips_descendants_) { |
| clip_rect_ = kRelativeFullRectClip; |
| ClipChildren(); |
| } |
| |
| set_update_phase(kUpdatedLayout); |
| return changed; |
| } |
| |
| bool UiElement::SizeAndLayOutChildren() { |
| bool changed = false; |
| for (auto& child : children_) |
| changed |= child->SizeAndLayOut(); |
| return changed; |
| } |
| |
| gfx::RectF UiElement::ComputeContributingChildrenBounds() { |
| gfx::RectF bounds; |
| for (auto& child : children_) { |
| if (!child->IsVisible() || !child->contributes_to_parent_bounds()) |
| continue; |
| |
| gfx::RectF outer_bounds(child->size()); |
| gfx::RectF inner_bounds(child->size()); |
| if (!child->bounds_contain_padding_) { |
| inner_bounds.Inset(child->left_padding_, child->bottom_padding_, |
| child->right_padding_, child->top_padding_); |
| } |
| gfx::SizeF size = inner_bounds.size(); |
| if (size.IsEmpty()) |
| continue; |
| |
| gfx::Vector2dF delta = |
| outer_bounds.CenterPoint() - inner_bounds.CenterPoint(); |
| gfx::Point3F child_center(child->local_origin() - delta); |
| gfx::Vector3dF corner_offset(size.width(), size.height(), 0); |
| corner_offset.Scale(-0.5); |
| gfx::Point3F child_upper_left = child_center + corner_offset; |
| gfx::Point3F child_lower_right = child_center - corner_offset; |
| |
| child->LocalTransform().TransformPoint(&child_upper_left); |
| child->LocalTransform().TransformPoint(&child_lower_right); |
| gfx::RectF local_rect = |
| gfx::RectF(child_upper_left.x(), child_upper_left.y(), |
| child_lower_right.x() - child_upper_left.x(), |
| child_lower_right.y() - child_upper_left.y()); |
| bounds.Union(local_rect); |
| } |
| |
| bounds.Inset(-left_padding_, -bottom_padding_, -right_padding_, |
| -top_padding_); |
| bounds.set_origin(bounds.CenterPoint()); |
| if (local_origin_ != bounds.origin()) { |
| world_space_transform_dirty_ = true; |
| local_origin_ = bounds.origin(); |
| } |
| |
| return bounds; |
| } |
| |
| void UiElement::LayOutContributingChildren() { |
| for (auto& child : children_) { |
| if (!child->IsVisible() || !child->contributes_to_parent_bounds()) |
| continue; |
| // Nothing to actually do since we aren't a layout object. Children that |
| // contribute to parent bounds cannot center or anchor to the edge of the |
| // parent. |
| DCHECK_EQ(child->x_centering(), NONE) << child->DebugName(); |
| DCHECK_EQ(child->y_centering(), NONE) << child->DebugName(); |
| DCHECK_EQ(child->x_anchoring(), NONE) << child->DebugName(); |
| DCHECK_EQ(child->y_anchoring(), NONE) << child->DebugName(); |
| } |
| } |
| |
| void UiElement::LayOutNonContributingChildren() { |
| DCHECK_LE(kUpdatedSize, update_phase_); |
| for (auto& child : children_) { |
| if (!child->IsVisible() || child->contributes_to_parent_bounds()) |
| continue; |
| |
| // To anchor a child, use the parent's size to find its edge. |
| float x_offset = 0.0f; |
| if (child->x_anchoring() == LEFT) { |
| x_offset = -0.5f * size().width(); |
| if (!child->bounds_contain_padding()) |
| x_offset += left_padding_; |
| } else if (child->x_anchoring() == RIGHT) { |
| x_offset = 0.5f * size().width(); |
| if (!child->bounds_contain_padding()) |
| x_offset -= right_padding_; |
| } |
| float y_offset = 0.0f; |
| if (child->y_anchoring() == TOP) { |
| y_offset = 0.5f * size().height(); |
| if (!child->bounds_contain_padding()) |
| y_offset -= top_padding_; |
| } else if (child->y_anchoring() == BOTTOM) { |
| y_offset = -0.5f * size().height(); |
| if (!child->bounds_contain_padding()) |
| y_offset += bottom_padding_; |
| } |
| child->SetLayoutOffset(x_offset, y_offset); |
| } |
| } |
| |
| void UiElement::ClipChildren() { |
| ClipChildren(GetAbsoluteClipRect()); |
| } |
| |
| void UiElement::ClipChildren(const gfx::RectF& abs_clip) { |
| for (auto& child : children_) { |
| // Nested clipping is not supported yet. |
| DCHECK(!child->clips_descendants_); |
| |
| if (!child->IsVisible()) |
| continue; |
| |
| DCHECK(child->LocalTransform().IsScaleOrTranslation()); |
| auto child_abs_clip = abs_clip; |
| child->LocalTransform().TransformRectReverse(&child_abs_clip); |
| if (!child->size().IsEmpty()) { |
| child->clip_rect_ = child_abs_clip; |
| child->clip_rect_.Scale(1.0f / child->size().width(), |
| 1.0f / child->size().height()); |
| } else { |
| child->clip_rect_ = kRelativeFullRectClip; |
| } |
| child->ClipChildren(child_abs_clip); |
| } |
| } |
| |
| gfx::RectF UiElement::GetAbsoluteClipRect() const { |
| auto result = clip_rect_; |
| result.Scale(size().width(), size().height()); |
| return result; |
| } |
| |
| gfx::RectF UiElement::GetClipRect() const { |
| auto corner_origin = clip_rect_.origin() - gfx::Vector2dF(-0.5f, 0.5f); |
| return gfx::RectF({corner_origin.x(), -corner_origin.y()}, clip_rect_.size()); |
| } |
| |
| void UiElement::SetClipRect(const gfx::RectF& rect) { |
| auto new_origin = gfx::PointF(rect.origin().x(), -rect.origin().y()) + |
| gfx::Vector2dF(-0.5f, 0.5f); |
| clip_rect_ = gfx::RectF(new_origin, rect.size()); |
| } |
| |
| UiElement* UiElement::FirstLaidOutChild() const { |
| auto i = std::find_if( |
| children_.begin(), children_.end(), |
| [](const std::unique_ptr<UiElement>& e) { return e->requires_layout(); }); |
| return i == children_.end() ? nullptr : i->get(); |
| } |
| |
| UiElement* UiElement::LastLaidOutChild() const { |
| auto i = std::find_if( |
| children_.rbegin(), children_.rend(), |
| [](const std::unique_ptr<UiElement>& e) { return e->requires_layout(); }); |
| return i == children_.rend() ? nullptr : i->get(); |
| } |
| |
| void UiElement::UpdateComputedOpacity() { |
| bool was_visible = computed_opacity_ > 0.0f; |
| set_computed_opacity(opacity_); |
| if (parent_) { |
| set_computed_opacity(computed_opacity_ * parent_->computed_opacity()); |
| } |
| set_update_phase(kUpdatedComputedOpacity); |
| updated_visibility_this_frame_ = IsVisible() != was_visible; |
| } |
| |
| bool UiElement::UpdateWorldSpaceTransform(bool parent_changed) { |
| if (!IsVisible() && !updated_visibility_this_frame_) |
| return false; |
| |
| bool changed = false; |
| bool should_update = ShouldUpdateWorldSpaceTransform(parent_changed); |
| if (should_update) { |
| gfx::Transform transform; |
| transform.Translate(local_origin_.x(), local_origin_.y()); |
| |
| if (!size_.IsEmpty()) { |
| transform.Scale(size_.width(), size_.height()); |
| } |
| |
| // Compute an inheritable transformation that can be applied to this |
| // element, and it's children, if applicable. |
| gfx::Transform inheritable = LocalTransform(); |
| |
| if (parent_) { |
| inheritable.ConcatTransform(parent_->inheritable_transform()); |
| } |
| |
| transform.ConcatTransform(inheritable); |
| changed = !transform.ApproximatelyEqual(world_space_transform_) || |
| !inheritable.ApproximatelyEqual(inheritable_transform_); |
| set_world_space_transform(transform); |
| set_inheritable_transform(inheritable); |
| } |
| |
| bool child_changed = false; |
| set_update_phase(kUpdatedWorldSpaceTransform); |
| for (auto& child : children_) { |
| // TODO(crbug.com/850260): it's unfortunate that we're not passing down the |
| // same dirtiness signal that we return. I.e., we'd ideally use |changed| |
| // here. |
| child_changed |= child->UpdateWorldSpaceTransform(should_update); |
| } |
| |
| OnUpdatedWorldSpaceTransform(); |
| return changed || child_changed; |
| } |
| |
| gfx::Transform UiElement::LocalTransform() const { |
| return local_transform_; |
| } |
| |
| gfx::Transform UiElement::GetTargetLocalTransform() const { |
| return layout_offset_.Apply() * GetTargetTransform().Apply(); |
| } |
| |
| const Sounds& UiElement::GetSounds() const { |
| return sounds_; |
| } |
| |
| bool UiElement::ShouldUpdateWorldSpaceTransform( |
| bool parent_transform_changed) const { |
| return parent_transform_changed || world_space_transform_dirty_; |
| } |
| |
| } // namespace vr |