910e62b5创建于 1月15日历史提交
// Copyright 2016 The Chromium Authors
// 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 <algorithm>
#include <limits>

#include "base/check_op.h"
#include "base/containers/adapters.h"
#include "base/notreached.h"
#include "base/numerics/angle_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "chrome/browser/vr/model/camera_model.h"
#include "device/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/rect_f.h"

namespace vr {

namespace {

int AllocateId() {
  static int g_next_id = 1;
  return g_next_id++;
}

#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 gfx::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

UiElement::UiElement() : id_(AllocateId()) {
  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() = default;

void UiElement::SetName(UiElementName name) {
  name_ = name;
}

void UiElement::SetType(UiElementType type) {
  type_ = type;
}

void UiElement::SetDrawPhase(DrawPhase draw_phase) {
  draw_phase_ = draw_phase;
}

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) {}

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
  // KeyframeEffect::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 = !animator_.keyframe_models().empty();
  if (force_animations_to_completion) {
    animator_.FinishAll();
  } else {
    animator_.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() {}

void UiElement::SetSize(float width, float height) {
  animator_.TransitionSizeTo(this, last_frame_time_, BOUNDS, size_,
                             gfx::SizeF(width, height));
}

void UiElement::SetVisible(bool visible) {
  SetOpacity(visible ? opacity_when_visible_ : 0.0);
}

void UiElement::SetVisibleImmediately(bool visible) {
  opacity_ = visible ? opacity_when_visible_ : 0.0;
  animator_.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/41382805): we shouldn't need to check opacity() here.
  return update_phase_ >= kUpdatedComputedOpacity && opacity() > 0.0f &&
         computed_opacity() > 0.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;
  }

  gfx::TransformOperations operations = layout_offset_;
  gfx::TransformOperation& op = operations.at(0);
  op.translate = {x, y, 0};
  op.Bake();
  animator_.TransitionTransformOperationsTo(
      this, 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;
  }

  gfx::TransformOperations operations = transform_operations_;
  gfx::TransformOperation& op = operations.at(kTranslateIndex);
  op.translate = {x, y, z};
  op.Bake();
  animator_.TransitionTransformOperationsTo(this, last_frame_time_, TRANSFORM,
                                            transform_operations_, operations);
}

void UiElement::SetRotate(float x, float y, float z, float radians) {
  float degrees = base::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;
  }

  gfx::TransformOperations operations = transform_operations_;
  gfx::TransformOperation& op = operations.at(kRotateIndex);
  op.rotate.axis = {x, y, z};
  op.rotate.angle = degrees;
  op.Bake();
  animator_.TransitionTransformOperationsTo(this, 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;
  }

  gfx::TransformOperations operations = transform_operations_;
  gfx::TransformOperation& op = operations.at(kScaleIndex);
  op.scale = {x, y, z};
  op.Bake();
  animator_.TransitionTransformOperationsTo(this, last_frame_time_, TRANSFORM,
                                            transform_operations_, operations);
}

void UiElement::SetOpacity(float opacity) {
  animator_.TransitionFloatTo(this, last_frame_time_, OPACITY, opacity_,
                              opacity);
}

void UiElement::SetCornerRadii(const CornerRadii& radii) {
  corner_radii_ = radii;
}

gfx::SizeF UiElement::GetTargetSize() const {
  return animator_.GetTargetSizeValue(TargetProperty::BOUNDS, size_);
}

gfx::TransformOperations UiElement::GetTargetTransform() const {
  return animator_.GetTargetTransformOperationsValue(TargetProperty::TRANSFORM,
                                                     transform_operations_);
}

gfx::Transform UiElement::ComputeTargetWorldSpaceTransform() const {
  gfx::Transform m;
  for (const UiElement* current = this; current; current = current->parent()) {
    m.PostConcat(current->GetTargetLocalTransform());
  }
  return m;
}

float UiElement::GetTargetOpacity() const {
  return animator_.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_;
}

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::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::ranges::find(children_, to_remove, &std::unique_ptr<UiElement>::get);
  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 {
  return world_space_transform_.MapPoint(gfx::Point3F());
}

void UiElement::OnFloatAnimated(const float& value,
                                int target_property_id,
                                gfx::KeyframeModel* keyframe_model) {
  opacity_ = std::clamp(value, 0.0f, 1.0f);
}

void UiElement::OnTransformAnimated(const gfx::TransformOperations& operations,
                                    int target_property_id,
                                    gfx::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::OnSizeAnimated(const gfx::SizeF& size,
                               int target_property_id,
                               gfx::KeyframeModel* keyframe_model) {
  if (size_ == size)
    return;
  size_ = size;
  world_space_transform_dirty_ = true;
}

void UiElement::OnColorAnimated(const SkColor& size,
                                int target_property_id,
                                gfx::KeyframeModel* keyframe_model) {}

void UiElement::SetTransitionedProperties(
    const std::set<TargetProperty>& properties) {
  std::set<int> converted_properties(properties.begin(), properties.end());
  animator_.SetTransitionedProperties(converted_properties);
}

void UiElement::AddKeyframeModel(
    std::unique_ptr<gfx::KeyframeModel> keyframe_model) {
  animator_.AddKeyframeModel(std::move(keyframe_model));
}

void UiElement::RemoveKeyframeModels(int target_property) {
  animator_.RemoveKeyframeModels(target_property);
}

bool UiElement::IsAnimatingProperty(TargetProperty property) const {
  return animator_.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();

  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(
          gfx::InsetsF::TLBR(child->top_padding_, child->left_padding_,
                             child->bottom_padding_, child->right_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_upper_left = child->LocalTransform().MapPoint(child_upper_left);
    child_lower_right = child->LocalTransform().MapPoint(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(gfx::InsetsF::TLBR(-top_padding_, -left_padding_,
                                  -bottom_padding_, -right_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);
  }
}

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());
}

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.PostConcat(parent_->inheritable_transform());
    }

    transform.PostConcat(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/41393128): 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);
  }

  return changed || child_changed;
}

gfx::Transform UiElement::LocalTransform() const {
  return local_transform_;
}

gfx::Transform UiElement::GetTargetLocalTransform() const {
  return layout_offset_.Apply() * GetTargetTransform().Apply();
}

bool UiElement::ShouldUpdateWorldSpaceTransform(
    bool parent_transform_changed) const {
  return parent_transform_changed || world_space_transform_dirty_;
}

}  // namespace vr