blob: a18955f5d917197dd25f6eed2b4f3a02dd5bdec5 [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/ui/scenic/lib/gfx/engine/view_tree.h"
#include <zircon/syscalls/object.h>
#include <sstream>
#include <utility>
#include "src/lib/fxl/logging.h"
#include "src/ui/scenic/lib/gfx/resources/view_holder.h"
namespace scenic_impl::gfx {
namespace {
// Convenience functions.
bool IsValid(zx_koid_t koid) { return koid != ZX_KOID_INVALID; }
std::optional<zx_koid_t> wrap(zx_koid_t koid) {
return koid == ZX_KOID_INVALID ? std::nullopt : std::optional(koid);
}
} // namespace
fuchsia::ui::focus::FocusChain ViewTree::CloneFocusChain() const {
FXL_DCHECK(IsStateValid()) << "invariant";
fuchsia::ui::focus::FocusChain full_copy{};
for (zx_koid_t koid : focus_chain_) {
full_copy.mutable_focus_chain()->push_back(CloneViewRefOf(koid));
}
return full_copy;
}
const std::vector<zx_koid_t>& ViewTree::focus_chain() const { return focus_chain_; }
std::optional<zx_koid_t> ViewTree::ParentOf(zx_koid_t child) const {
FXL_DCHECK(IsTracked(child)) << "invariant";
if (const auto ptr = std::get_if<AttachNode>(&nodes_.at(child))) {
return wrap(ptr->parent);
} else if (const auto ptr = std::get_if<RefNode>(&nodes_.at(child))) {
return wrap(ptr->parent);
}
FXL_NOTREACHED() << "impossible";
return std::nullopt;
}
scheduling::SessionId ViewTree::SessionIdOf(zx_koid_t koid) const {
if (!IsValid(koid) || !IsTracked(koid)) {
return 0u;
}
if (const auto ptr = std::get_if<AttachNode>(&nodes_.at(koid))) {
return 0u;
} else if (const auto ptr = std::get_if<RefNode>(&nodes_.at(koid))) {
return ptr->session_id;
}
FXL_NOTREACHED() << "impossible";
return 0u;
}
EventReporterWeakPtr ViewTree::EventReporterOf(zx_koid_t koid) const {
if (!IsValid(koid) || !IsTracked(koid) || std::holds_alternative<AttachNode>(nodes_.at(koid))) {
return EventReporterWeakPtr(/*nullptr*/);
}
if (auto ptr = std::get_if<RefNode>(&nodes_.at(koid))) {
return ptr->event_reporter;
}
FXL_NOTREACHED() << "impossible";
return EventReporterWeakPtr(/*nullptr*/);
}
std::optional<zx_koid_t> ViewTree::ConnectedViewRefKoidOf(SessionId session_id) const {
const auto range = ref_node_koids_.equal_range(session_id);
for (auto it = range.first; it != range.second; ++it) {
const zx_koid_t koid = it->second;
if (IsConnected(koid)) {
return std::optional<zx_koid_t>(koid);
}
}
return std::nullopt;
}
bool ViewTree::IsTracked(zx_koid_t koid) const { return IsValid(koid) && nodes_.count(koid) > 0; }
bool ViewTree::IsConnected(zx_koid_t koid) const {
FXL_DCHECK(IsTracked(koid)) << "precondition";
if (!IsValid(root_))
return false; // No connectivity, base case.
if (koid == root_)
return true; // Trivial connectivity, base case.
if (const auto ptr = std::get_if<AttachNode>(&nodes_.at(koid))) {
if (!IsTracked(ptr->parent))
return false; // Does not reach root.
if (const auto parent_ptr = std::get_if<RefNode>(&nodes_.at(ptr->parent)))
return IsConnected(ptr->parent); // Recursive.
} else if (const auto ptr = std::get_if<RefNode>(&nodes_.at(koid))) {
if (!IsTracked(ptr->parent))
return false; // Does not reach root.
if (const auto parent_ptr = std::get_if<AttachNode>(&nodes_.at(ptr->parent)))
return IsConnected(ptr->parent); // Recursive.
}
FXL_NOTREACHED() << "invariant: child/parent types are known and correctly alternate";
return false; // Impossible.
}
bool ViewTree::IsRefNode(zx_koid_t koid) const {
FXL_DCHECK(IsTracked(koid)) << "precondition";
return std::holds_alternative<RefNode>(nodes_.at(koid));
}
bool ViewTree::MayReceiveFocus(zx_koid_t koid) const {
FXL_DCHECK(IsTracked(koid) && IsRefNode(koid)) << "precondition";
return std::get_if<RefNode>(&nodes_.at(koid))->may_receive_focus();
}
std::optional<glm::mat4> ViewTree::GlobalTransformOf(zx_koid_t koid) const {
if (!IsTracked(koid) || !IsRefNode(koid)) {
return std::nullopt;
}
return std::get_if<RefNode>(&nodes_.at(koid))->global_transform();
}
zx_status_t ViewTree::AddAnnotationViewHolder(zx_koid_t koid, ViewHolderPtr annotation) const {
if (!IsValid(koid)) {
return ZX_ERR_INVALID_ARGS;
}
if (!IsTracked(koid)) {
return ZX_ERR_NOT_FOUND;
}
if (!IsRefNode(koid)) {
return ZX_ERR_INVALID_ARGS;
}
std::get_if<RefNode>(&nodes_.at(koid))->add_annotation_view_holder(std::move(annotation));
return ZX_OK;
}
bool ViewTree::IsStateValid() const {
// Map state
for (const auto& item : nodes_) {
if (!IsValid(item.first)) {
FXL_LOG(ERROR) << "Map key is invalid koid.";
return false;
}
if (const auto ptr = std::get_if<AttachNode>(&item.second)) {
if (IsValid(ptr->parent)) {
if (!IsTracked(ptr->parent)) {
FXL_LOG(ERROR) << "Map item's parent is valid but isn't tracked: " << ptr->parent;
return false;
}
if (!std::holds_alternative<RefNode>(nodes_.at(ptr->parent))) {
FXL_LOG(ERROR) << "Map item's parent should be a RefNode: " << ptr->parent;
return false;
}
}
} else if (const auto ptr = std::get_if<RefNode>(&item.second)) {
if (IsValid(ptr->parent)) {
if (!IsTracked(ptr->parent)) {
FXL_LOG(ERROR) << "Map item's parent is valid but isn't tracked: " << ptr->parent;
return false;
}
if (!std::holds_alternative<AttachNode>(nodes_.at(ptr->parent))) {
FXL_LOG(ERROR) << "Map item's parent should be an AttachNode: " << ptr->parent;
return false;
}
// Only one entity must have ptr->parent as a parent.
size_t parent_count = 0;
for (const auto& j_item : nodes_) {
if (const auto j_ptr = std::get_if<AttachNode>(&j_item.second)) {
if (j_ptr->parent == ptr->parent)
++parent_count;
} else if (const auto j_ptr = std::get_if<RefNode>(&j_item.second)) {
if (j_ptr->parent == ptr->parent)
++parent_count;
} else {
FXL_NOTREACHED() << "unknown type";
return false;
}
}
if (parent_count != 1) {
FXL_LOG(ERROR) << "Map item's parent should have just one child: " << ptr->parent
<< ", count: " << parent_count;
return false;
}
}
} else {
FXL_NOTREACHED() << "unknown type";
return false;
}
}
// SessionId -> RefNode KOID map state
for (const auto& item : ref_node_koids_) {
const SessionId session_id = item.first;
const zx_koid_t koid = item.second;
if (session_id == 0u) {
FXL_LOG(ERROR) << "Map key is invalid SessionId.";
return false;
}
if (!IsValid(koid) || !IsTracked(koid)) {
FXL_LOG(ERROR) << "Map value isn't a valid and tracked koid.";
return false;
}
const auto ptr = std::get_if<RefNode>(&nodes_.at(koid));
if (ptr == nullptr) {
FXL_LOG(ERROR) << "Map item should refer to a RefNode: " << koid;
return false;
}
if (ptr->session_id != session_id) {
FXL_LOG(ERROR) << "Declared SessionId doesn't match: " << ptr->session_id << ", "
<< session_id;
return false;
}
// Count of connected KOIDs from this session_id is at most 1.
int connected_koid = 0;
const auto range = ref_node_koids_.equal_range(session_id);
for (auto it = range.first; it != range.second; ++it) {
if (IsConnected(it->second)) {
++connected_koid;
}
}
if (connected_koid > 1) {
FXL_LOG(ERROR) << "Count of scene-connected ViewRefs for session " << session_id
<< " exceeds 1. Reference SCN-1249.";
// TODO(SCN-1249): Enable invariant check when one-view-per-session is enforced.
// return false;
}
}
// Scene state
if (IsValid(root_)) {
if (!IsTracked(root_)) {
FXL_LOG(ERROR) << "Scene is valid but isn't tracked: " << root_;
return false;
}
if (!std::holds_alternative<RefNode>(nodes_.at(root_))) {
FXL_LOG(ERROR) << "Scene should be a RefNode but isn't: " << root_;
return false;
}
}
// Focus chain state: relationship with root_.
if (IsValid(root_)) {
if (focus_chain_.size() == 0) {
FXL_LOG(ERROR) << "Focus chain should be not empty but is.";
return false;
}
if (focus_chain_[0] != root_) {
FXL_LOG(ERROR) << "Focus chain's zeroth element should be root but isn't: " << root_ << ", "
<< focus_chain_[0];
return false;
}
} else {
if (focus_chain_.size() > 0) {
FXL_LOG(ERROR) << "Focus chain should be empty but isn't.";
return false;
}
}
// Focus chain state: relationship with nodes_.
for (size_t idx = 1; idx < focus_chain_.size(); ++idx) {
const zx_koid_t koid = focus_chain_[idx];
if (!IsValid(koid) || !IsTracked(koid) || !IsRefNode(koid)) {
FXL_LOG(ERROR) << "Focus chain element isn't a valid and tracked RefNode: " << koid
<< ", at index: " << idx;
return false;
}
const zx_koid_t parent = std::get_if<RefNode>(&nodes_.at(koid))->parent;
if (!IsValid(parent) || !IsTracked(parent) || IsRefNode(parent)) {
FXL_LOG(ERROR) << "Focus chain element's parent isn't a valid and tracked AttachNode: "
<< koid << ", at index: " << idx;
return false;
}
const zx_koid_t grandparent = std::get_if<AttachNode>(&nodes_.at(parent))->parent;
if (!IsValid(grandparent) || !IsTracked(grandparent) || !IsRefNode(grandparent)) {
FXL_LOG(ERROR) << "Focus chain element's grandparent isn't a valid and tracked RefNode: "
<< koid << ", at index: " << idx;
return false;
}
if (grandparent != focus_chain_[idx - 1]) {
FXL_LOG(ERROR)
<< "Focus chain element's grandparent doesn't match previous focus chain element: "
<< koid << ", at index: " << idx;
return false;
}
}
// Focus chain state: root node may receive focus, terminal node may receive focus.
if (focus_chain_.size() > 0) {
if (!MayReceiveFocus(root_)) {
FXL_LOG(ERROR) << "Focus chain's root element must be able to receive focus: koid=" << root_;
return false;
}
if (!MayReceiveFocus(focus_chain_.back())) {
FXL_LOG(ERROR) << "Focus chain's terminal element must be able to receive focus: koid="
<< focus_chain_.back();
return false;
}
}
return true;
}
ViewTree::FocusChangeStatus ViewTree::RequestFocusChange(const zx_koid_t requestor,
const zx_koid_t request) {
// Invalid requestor.
if (!IsTracked(requestor) || !IsRefNode(requestor) || !IsConnected(requestor))
return ViewTree::FocusChangeStatus::kErrorRequestorInvalid;
// Invalid request.
if (!IsTracked(request) || !IsRefNode(request) || !IsConnected(request))
return ViewTree::FocusChangeStatus::kErrorRequestInvalid;
// Transfer policy: requestor must be authorized.
{
bool has_authority = false;
for (zx_koid_t authority : focus_chain_) {
if (requestor == authority) {
has_authority = true;
break;
}
}
if (!has_authority)
return ViewTree::FocusChangeStatus::kErrorRequestorNotAuthorized;
}
// Transfer policy: requestor must be an ancestor of request.
{
bool is_ancestor = false;
zx_koid_t curr = request;
while (IsValid(curr)) {
if (curr == requestor) {
is_ancestor = true;
break;
}
// Iterate upward.
if (const auto ptr = std::get_if<RefNode>(&nodes_.at(curr))) {
curr = ptr->parent;
} else if (const auto ptr = std::get_if<AttachNode>(&nodes_.at(curr))) {
curr = ptr->parent;
} else {
FXL_NOTREACHED() << "impossible";
return ViewTree::FocusChangeStatus::kErrorUnhandledCase;
}
}
if (!is_ancestor)
return ViewTree::FocusChangeStatus::kErrorRequestorNotRequestAncestor;
}
// Transfer policy: request must have "may receive focus" property.
{
const auto ptr = std::get_if<RefNode>(&nodes_.at(request));
if (!ptr->may_receive_focus())
return ViewTree::FocusChangeStatus::kErrorRequestCannotReceiveFocus;
}
// It's a valid request for a change to focus chain. Regenerate chain.
{
std::vector<zx_koid_t> buffer;
zx_koid_t curr = request;
while (IsValid(curr)) {
if (const auto ptr = std::get_if<RefNode>(&nodes_.at(curr))) {
buffer.push_back(curr);
curr = ptr->parent;
} else if (const auto ptr = std::get_if<AttachNode>(&nodes_.at(curr))) {
curr = ptr->parent;
} else {
FXL_NOTREACHED() << "impossible";
return ViewTree::FocusChangeStatus::kErrorUnhandledCase;
}
}
// Clear state; populate with root and move downwards.
focus_chain_.clear();
for (auto iter = buffer.crbegin(); iter != buffer.crend(); ++iter) {
focus_chain_.push_back(*iter);
}
}
FXL_DCHECK(IsStateValid()) << "postcondition";
return ViewTree::FocusChangeStatus::kAccept;
}
void ViewTree::NewRefNode(fuchsia::ui::views::ViewRef view_ref, EventReporterWeakPtr reporter,
fit::function<bool()> may_receive_focus,
fit::function<std::optional<glm::mat4>()> global_transform,
fit::function<void(ViewHolderPtr)> add_annotation_view_holder,
scheduling::SessionId session_id) {
const zx_koid_t koid = ExtractKoid(view_ref);
FXL_DCHECK(IsValid(koid)) << "precondition";
FXL_DCHECK(!IsTracked(koid)) << "precondition";
FXL_DCHECK(may_receive_focus) << "precondition"; // Callback exists.
FXL_DCHECK(global_transform) << "precondition"; // Callback exists.
FXL_DCHECK(add_annotation_view_holder) << "precondition"; // Callback exists.
FXL_DCHECK(session_id != scheduling::kInvalidSessionId) << "precondition";
if (!IsValid(koid) || IsTracked(koid))
return; // Bail.
nodes_[koid] = RefNode{.view_ref = std::move(view_ref),
.event_reporter = reporter,
.may_receive_focus = std::move(may_receive_focus),
.global_transform = std::move(global_transform),
.add_annotation_view_holder = std::move(add_annotation_view_holder),
.session_id = session_id};
ref_node_koids_.insert({session_id, koid});
FXL_DCHECK(IsStateValid()) << "postcondition";
}
void ViewTree::NewAttachNode(zx_koid_t koid) {
FXL_DCHECK(IsValid(koid)) << "precondition";
FXL_DCHECK(!IsTracked(koid)) << "precondition";
if (!IsValid(koid) || IsTracked(koid))
return; // Bail.
nodes_[koid] = AttachNode{};
FXL_DCHECK(IsStateValid()) << "postcondition";
}
void ViewTree::DeleteNode(const zx_koid_t koid) {
FXL_DCHECK(IsTracked(koid)) << "precondition";
// Remove from view ref koid mapping, if applicable.
if (IsRefNode(koid)) {
for (auto it = ref_node_koids_.begin(); it != ref_node_koids_.end(); ++it) {
if (it->second == koid) {
ref_node_koids_.erase(it);
break; // |it| is invalid, but we exit loop immediately.
}
}
}
// Remove from node set.
nodes_.erase(koid);
// Remove from node set's parent references.
for (auto& item : nodes_) {
if (auto ptr = std::get_if<AttachNode>(&item.second)) {
if (ptr->parent == koid) {
ptr->parent = ZX_KOID_INVALID;
}
} else if (auto ptr = std::get_if<RefNode>(&item.second)) {
if (ptr->parent == koid) {
ptr->parent = ZX_KOID_INVALID;
}
} else {
FXL_NOTREACHED() << "unknown type";
}
}
// Remove |koid| if it is the root.
if (root_ == koid) {
root_ = ZX_KOID_INVALID;
}
// Remove |koid| if it is in the focus chain.
RepairFocus();
FXL_DCHECK(IsStateValid()) << "postcondition";
}
void ViewTree::MakeGlobalRoot(zx_koid_t koid) {
FXL_DCHECK(!IsValid(koid) || (IsTracked(koid) && IsRefNode(koid) && MayReceiveFocus(koid)))
<< "precondition";
root_ = koid;
RepairFocus();
FXL_DCHECK(IsStateValid()) << "postcondition";
}
void ViewTree::ConnectToParent(zx_koid_t child, zx_koid_t parent) {
FXL_DCHECK(IsTracked(child)) << "precondition";
FXL_DCHECK(IsTracked(parent)) << "precondition";
if (auto ptr = std::get_if<AttachNode>(&nodes_[child])) {
if (std::holds_alternative<RefNode>(nodes_[parent])) {
ptr->parent = parent;
FXL_DCHECK(IsStateValid()) << "postcondition";
return;
}
} else if (auto ptr = std::get_if<RefNode>(&nodes_[child])) {
if (std::holds_alternative<AttachNode>(nodes_[parent])) {
ptr->parent = parent;
FXL_DCHECK(IsStateValid()) << "postcondition";
return;
}
}
FXL_NOTREACHED() << "invariant: child/parent types must be known and must be different";
}
void ViewTree::DisconnectFromParent(zx_koid_t child) {
FXL_DCHECK(IsTracked(child)) << "precondition";
if (auto ptr = std::get_if<AttachNode>(&nodes_[child])) {
if (!IsTracked(ptr->parent))
return; // Parent (a RefNode) was never set, or already deleted.
if (auto parent_ptr = std::get_if<RefNode>(&nodes_[ptr->parent])) {
ptr->parent = ZX_KOID_INVALID;
RepairFocus();
FXL_DCHECK(IsStateValid()) << "postcondition";
return;
}
} else if (auto ptr = std::get_if<RefNode>(&nodes_[child])) {
if (!IsTracked(ptr->parent))
return; // Parent (an AttachNode) was never set, or already deleted.
if (auto parent_ptr = std::get_if<AttachNode>(&nodes_[ptr->parent])) {
ptr->parent = ZX_KOID_INVALID;
RepairFocus();
FXL_DCHECK(IsStateValid()) << "postcondition";
return;
}
}
FXL_NOTREACHED() << "invariant: child/parent types are known and correct";
}
std::string ViewTree::ToString() const {
std::stringstream output;
output << std::endl << "ViewTree Dump" << std::endl;
output << " root: " << root_ << std::endl;
output << " nodes: " << std::endl;
for (const auto& item : nodes_) {
if (const auto ptr = std::get_if<AttachNode>(&item.second)) {
output << " attach-node(" << item.first << ") -> parent: " << ptr->parent << std::endl;
} else if (const auto ptr = std::get_if<RefNode>(&item.second)) {
output << " ref-node(" << item.first << ") -> parent: " << ptr->parent
<< ", event-reporter: " << ptr->event_reporter.get()
<< ", may-receive-focus: " << std::boolalpha << ptr->may_receive_focus()
<< ", session-id: " << ptr->session_id << std::endl;
} else {
FXL_NOTREACHED() << "impossible";
}
}
output << " ref-node-koids:" << std::endl;
for (const auto& item : ref_node_koids_) {
output << " session-id " << item.first << " has koid " << item.second << std::endl;
}
output << " focus-chain: [ ";
for (auto koid : focus_chain_) {
output << koid << " ";
}
output << "]" << std::endl;
return output.str();
}
fuchsia::ui::views::ViewRef ViewTree::CloneViewRefOf(zx_koid_t koid) const {
FXL_DCHECK(IsTracked(koid)) << "precondition";
FXL_DCHECK(IsRefNode(koid)) << "precondition";
fuchsia::ui::views::ViewRef clone;
if (const auto ptr = std::get_if<RefNode>(&nodes_.at(koid))) {
fidl::Clone(ptr->view_ref, &clone);
}
return clone;
}
void ViewTree::RepairFocus() {
// root_ was destroyed, set focus_chain_ to empty.
if (!IsTracked(root_)) {
FXL_DCHECK(!IsValid(root_)) << "invariant";
focus_chain_.clear();
return;
}
// root_ exists, but it's fresh or a replacement. Use it.
if (focus_chain_.size() == 0 || focus_chain_[0] != root_) {
focus_chain_.clear();
focus_chain_.push_back(root_);
return;
}
// root_ exists, and is already valid.
// Walk down chain until we find a divergence from relationship data in nodes_.
{
size_t curr = 1;
for (; curr < focus_chain_.size(); ++curr) {
const zx_koid_t child = focus_chain_[curr];
if (!IsTracked(child))
break; // child destroyed
const std::optional<zx_koid_t> parent = ParentOf(child);
if (!parent.has_value())
break; // parent reset or destroyed
const std::optional<zx_koid_t> grandparent = ParentOf(parent.value());
if (!grandparent.has_value())
break; // grandparent reset or destroyed
if (grandparent.value() != focus_chain_[curr - 1])
break; // focus chain relation changed
}
// Trim out invalid entries.
FXL_DCHECK(curr >= 1 && curr <= focus_chain_.size()) << "invariant";
focus_chain_.erase(focus_chain_.begin() + curr, focus_chain_.end());
}
// Trim upward until we find a node that may receive focus.
FXL_DCHECK(focus_chain_.size() > 0) << "invariant";
while (focus_chain_.size() > 0 && !MayReceiveFocus(focus_chain_.back())) {
focus_chain_.pop_back();
}
// Run state validity check at call site.
}
zx_koid_t ExtractKoid(const fuchsia::ui::views::ViewRef& view_ref) {
zx_info_handle_basic_t info{};
if (view_ref.reference.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr) !=
ZX_OK) {
return ZX_KOID_INVALID; // no info
}
return info.koid;
}
} // namespace scenic_impl::gfx