blob: e00d65e5ffae73d58c86534109f5a2d150576d0b [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/interaction/element_tracker.h"
#include <algorithm>
#include <iterator>
#include <list>
#include <map>
#include <sstream>
#include "base/callback_list.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "ui/base/interaction/element_identifier.h"
namespace ui {
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ElementTracker, kTemporaryIdentifier);
class ElementTracker::ElementData {
public:
ElementData(ElementTracker* tracker,
ElementIdentifier id,
ElementContext context)
: identifier_(id), context_(context) {
auto removal_callback =
base::BindRepeating(&ElementTracker::MaybeCleanup,
base::Unretained(tracker), base::Unretained(this));
shown_callbacks_.set_removal_callback(removal_callback);
activated_callbacks_.set_removal_callback(removal_callback);
hidden_callbacks_.set_removal_callback(removal_callback);
custom_event_callbacks_.set_removal_callback(removal_callback);
}
~ElementData() = default;
ElementIdentifier identifier() const { return identifier_; }
ElementContext context() const { return context_; }
bool HasElement(const TrackedElement* element) const {
return base::Contains(element_lookup_, element);
}
bool empty() const {
return elements_.empty() && shown_callbacks_.empty() &&
activated_callbacks_.empty() && hidden_callbacks_.empty() &&
custom_event_callbacks_.empty();
}
size_t num_elements() const {
// Guaranteed O(1) in C++11 and later.
return elements_.size();
}
const std::list<raw_ptr<TrackedElement, CtnExperimental>>& elements() const {
return elements_;
}
Subscription AddElementShownCallback(Callback callback) {
return shown_callbacks_.Add(callback);
}
Subscription AddElementActivatedCallback(Callback callback) {
return activated_callbacks_.Add(callback);
}
Subscription AddElementHiddenCallback(Callback callback) {
return hidden_callbacks_.Add(callback);
}
Subscription AddCustomEventCallback(Callback callback) {
return custom_event_callbacks_.Add(callback);
}
void NotifyElementShown(raw_ptr<TrackedElement, CtnExperimental>& element) {
DCHECK(element);
DCHECK_EQ(identifier(), element->identifier());
// Zero context data is the "all contexts" entry and doesn't actually store
// new elements, just calls callbacks.
if (context()) {
DCHECK_EQ(static_cast<intptr_t>(context()),
static_cast<intptr_t>(element->context()));
const auto it = elements_.insert(elements_.end(), element);
const bool success = element_lookup_.emplace(element, it).second;
DCHECK(success);
}
shown_callbacks_.Notify(element);
}
void NotifyElementActivated(
raw_ptr<TrackedElement, CtnExperimental>& element) {
// Note: "All contexts" does not require the element to be present here.
DCHECK(!context_ || base::Contains(element_lookup_, element));
activated_callbacks_.Notify(element);
}
void NotifyElementHidden(TrackedElement* element) {
if (context_) {
const auto it = element_lookup_.find(element);
CHECK(it != element_lookup_.end());
elements_.erase(it->second);
element_lookup_.erase(it);
}
hidden_callbacks_.Notify(element);
}
void NotifyCustomEvent(TrackedElement* element) {
custom_event_callbacks_.Notify(element);
}
private:
const ElementIdentifier identifier_;
const ElementContext context_;
// Holds elements in the order they were added to this data block, so that the
// first element or the first element that matches some criterion can be
// easily found.
std::list<raw_ptr<TrackedElement, CtnExperimental>> elements_;
// Provides a fast lookup into `elements_` by element for checking and
// removal. Since there could be many elements (e.g. tabs in a browser) we
// don't want removing a series of them to turn into an O(n^2) operation.
std::map<const TrackedElement*,
std::list<raw_ptr<TrackedElement, CtnExperimental>>::iterator>
element_lookup_;
base::RepeatingCallbackList<void(TrackedElement*)> shown_callbacks_;
base::RepeatingCallbackList<void(TrackedElement*)> activated_callbacks_;
base::RepeatingCallbackList<void(TrackedElement*)> hidden_callbacks_;
base::RepeatingCallbackList<void(TrackedElement*)> custom_event_callbacks_;
};
// Ensures that ElementData objects get cleaned up, but only after all callbacks
// have returned. Otherwise a subscription could be canceled during a callback,
// resulting in the ElementData and the callback list being deleted before the
// callback has returned.
class ElementTracker::GarbageCollector {
public:
// Represents a call stack frame in which garbage collection can happen.
// Garbage collection doesn't actually occur until all nested Frames are
// destructed.
class Frame {
public:
explicit Frame(GarbageCollector* gc) : gc_(gc) {
gc_->IncrementFrameCount();
}
~Frame() { gc_->DecrementFrameCount(); }
void Add(ElementData* data) { gc_->AddCandidate(data); }
private:
const raw_ptr<GarbageCollector> gc_;
};
explicit GarbageCollector(ElementTracker* tracker) : tracker_(tracker) {}
private:
void AddCandidate(ElementData* data) {
DCHECK_GE(frame_count_, 0);
candidates_.insert(data);
}
void IncrementFrameCount() { ++frame_count_; }
void DecrementFrameCount() {
DCHECK_GE(frame_count_, 0);
if (--frame_count_ > 0)
return;
for (ElementData* data : candidates_) {
if (data->empty()) {
const auto result = tracker_->element_data_.erase(
LookupKey(data->identifier(), data->context()));
DCHECK(result);
}
}
candidates_.clear();
}
const raw_ptr<ElementTracker> tracker_;
std::set<raw_ptr<ElementData, SetExperimental>> candidates_;
int frame_count_ = 0;
};
TrackedElement::TrackedElement(ElementIdentifier id, ElementContext context)
: identifier_(id), context_(context) {
CHECK(id);
CHECK(context);
}
TrackedElement::~TrackedElement() = default;
gfx::Rect TrackedElement::GetScreenBounds() const {
return gfx::Rect();
}
gfx::NativeView TrackedElement::GetNativeView() const {
return gfx::NativeView();
}
std::string TrackedElement::ToString() const {
std::ostringstream oss;
oss << GetImplementationName() << "(" << identifier() << ", " << context()
<< ")";
return oss.str();
}
// static
ElementTracker* ElementTracker::GetElementTracker() {
static base::NoDestructor<ElementTracker> instance;
return instance.get();
}
// static
ElementTrackerFrameworkDelegate* ElementTracker::GetFrameworkDelegate() {
return static_cast<ElementTrackerFrameworkDelegate*>(GetElementTracker());
}
TrackedElement* ElementTracker::GetUniqueElement(ElementIdentifier id,
ElementContext context) {
const auto it = element_data_.find(LookupKey(id, context));
if (it == element_data_.end() || it->second.num_elements() == 0)
return nullptr;
DCHECK_EQ(1U, it->second.num_elements());
return it->second.elements().front();
}
TrackedElement* ElementTracker::GetFirstMatchingElement(
ElementIdentifier id,
ElementContext context) {
const auto it = element_data_.find(LookupKey(id, context));
if (it == element_data_.end() || it->second.num_elements() == 0)
return nullptr;
return it->second.elements().front();
}
TrackedElement* ElementTracker::GetElementInAnyContext(ElementIdentifier id) {
for (const auto& [key, data] : element_data_) {
if (key.first == id && !data.elements().empty())
return data.elements().front();
}
return nullptr;
}
ElementTracker::ElementList ElementTracker::GetAllMatchingElements(
ElementIdentifier id,
ElementContext context) {
const auto it = element_data_.find(LookupKey(id, context));
ElementList result;
if (it != element_data_.end()) {
std::ranges::copy(it->second.elements(), std::back_inserter(result));
}
return result;
}
ElementTracker::ElementList ElementTracker::GetAllMatchingElementsInAnyContext(
ElementIdentifier id) {
ElementList result;
for (const auto& [key, data] : element_data_) {
if (key.first == id) {
std::ranges::copy(data.elements(), std::back_inserter(result));
}
}
return result;
}
bool ElementTracker::IsElementVisible(ElementIdentifier id,
ElementContext context) {
const auto it = element_data_.find(LookupKey(id, context));
return it != element_data_.end() && it->second.num_elements() > 0;
}
ElementTracker::Contexts ElementTracker::GetAllContextsForTesting() const {
Contexts result;
for (const auto& [key, data] : element_data_) {
const ElementContext context = key.second;
// The null context is used for registering "in any context" callbacks, but
// is not actually a valid context.
if (context) {
result.insert(context);
}
}
return result;
}
ElementTracker::ElementList ElementTracker::GetAllElementsForTesting(
std::optional<ElementContext> in_context) {
ElementList result;
for (const auto& [key, data] : element_data_) {
if (!in_context.has_value() || in_context.value() == key.second) {
std::copy(data.elements().begin(), data.elements().end(),
std::back_inserter(result));
}
}
return result;
}
ElementTracker::Subscription
ElementTracker::AddAnyElementShownCallbackForTesting(Callback callback) {
return any_element_shown_callbacks_.Add(std::move(callback));
}
ElementTracker::Subscription ElementTracker::AddElementShownCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
DCHECK(id);
DCHECK(context);
return GetOrAddElementData(id, context)->AddElementShownCallback(callback);
}
ElementTracker::Subscription
ElementTracker::AddElementShownInAnyContextCallback(ElementIdentifier id,
Callback callback) {
DCHECK(id);
return GetOrAddElementData(id, ElementContext())
->AddElementShownCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddElementActivatedCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
DCHECK(id);
DCHECK(context);
return GetOrAddElementData(id, context)
->AddElementActivatedCallback(callback);
}
ElementTracker::Subscription
ElementTracker::AddElementActivatedInAnyContextCallback(ElementIdentifier id,
Callback callback) {
DCHECK(id);
return GetOrAddElementData(id, ElementContext())
->AddElementActivatedCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddElementHiddenCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
DCHECK(id);
DCHECK(context);
return GetOrAddElementData(id, context)->AddElementHiddenCallback(callback);
}
ElementTracker::Subscription
ElementTracker::AddElementHiddenInAnyContextCallback(ElementIdentifier id,
Callback callback) {
DCHECK(id);
return GetOrAddElementData(id, ElementContext())
->AddElementHiddenCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddCustomEventCallback(
CustomElementEventType event_type,
ElementContext context,
Callback callback) {
DCHECK(event_type);
DCHECK(context);
// Because custom event callbacks are indexed by event type (and because we
// use the same underlying type for both element ids and custom events), we
// can store both in the same lookup table.
return GetOrAddElementData(event_type, context)
->AddCustomEventCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddCustomEventInAnyContextCallback(
CustomElementEventType event_type,
Callback callback) {
DCHECK(event_type);
// Because custom event callbacks are indexed by event type (and because we
// use the same underlying type for both element ids and custom events), we
// can store both in the same lookup table.
return GetOrAddElementData(event_type, ElementContext())
->AddCustomEventCallback(callback);
}
ElementTracker::ElementTracker()
: gc_(std::make_unique<GarbageCollector>(this)) {}
ElementTracker::~ElementTracker() = default;
void ElementTracker::NotifyElementShown(TrackedElement* element) {
notification_elements_.push_back(element);
auto& safe_element = notification_elements_.back();
// Prevent garbage collection of dead entries until after we send
// notifications and all callbacks happen.
GarbageCollector::Frame gc_frame(gc_.get());
ElementData* const element_data =
GetOrAddElementData(element->identifier(), element->context());
DCHECK(!element_data->HasElement(element));
element_data->NotifyElementShown(safe_element);
// Do "all contexts" notification:
if (safe_element) {
const auto it =
element_data_.find(LookupKey(element->identifier(), ElementContext()));
if (it != element_data_.end())
it->second.NotifyElementShown(safe_element);
}
// Do the "all elements" notification:
if (safe_element)
any_element_shown_callbacks_.Notify(element);
notification_elements_.pop_back();
}
void ElementTracker::NotifyElementActivated(TrackedElement* element) {
notification_elements_.push_back(element);
auto& safe_element = notification_elements_.back();
// Prevent garbage collection of dead entries until after we send
// notifications and all callbacks happen.
GarbageCollector::Frame gc_frame(gc_.get());
const auto it =
element_data_.find(LookupKey(element->identifier(), element->context()));
CHECK(it != element_data_.end());
it->second.NotifyElementActivated(safe_element);
// Do "all contexts" notification:
if (safe_element) {
const auto all_it =
element_data_.find(LookupKey(element->identifier(), ElementContext()));
if (all_it != element_data_.end()) {
all_it->second.NotifyElementActivated(safe_element);
}
}
notification_elements_.pop_back();
}
void ElementTracker::NotifyElementHidden(TrackedElement* element) {
// Clear out any elements we're in the process of sending events for.
for (auto& safe_element : notification_elements_) {
if (safe_element == element)
safe_element = nullptr;
}
// Prevent garbage collection of dead entries until after we send
// notifications and all callbacks happen.
GarbageCollector::Frame gc_frame(gc_.get());
// Call context-specific callbacks and erase entry.
const auto it =
element_data_.find(LookupKey(element->identifier(), element->context()));
CHECK(it != element_data_.end());
ElementData* const data = &it->second;
data->NotifyElementHidden(element);
gc_frame.Add(data);
// Call "in any context" callbacks.
const auto all_it =
element_data_.find(LookupKey(element->identifier(), ElementContext()));
if (all_it != element_data_.end()) {
all_it->second.NotifyElementHidden(element);
}
}
void ElementTracker::NotifyCustomEvent(TrackedElement* element,
CustomElementEventType event_type) {
// Prevent garbage collection of dead entries until after we send
// notifications and all callbacks happen.
GarbageCollector::Frame gc_frame(gc_.get());
// We'd like to verify that this element is valid, but don't need to expend
// the effort on an extra lookup if we're not doing checks.
#if DCHECK_IS_ON()
const auto entry =
element_data_.find(LookupKey(element->identifier(), element->context()));
DCHECK(entry != element_data_.end() && entry->second.HasElement(element));
#endif
notification_elements_.push_back(element);
auto& safe_element = notification_elements_.back();
// Since event types are identifiers, we store callbacks by event type rather
// than element identifier.
const auto it = element_data_.find(LookupKey(event_type, element->context()));
// If we don't find a match, that's fine; it means nobody was listening for
// that event type.
if (it != element_data_.end()) {
it->second.NotifyCustomEvent(safe_element);
}
// Do "all contexts" notification:
const auto all_it =
element_data_.find(LookupKey(event_type, ElementContext()));
if (all_it != element_data_.end()) {
all_it->second.NotifyCustomEvent(safe_element);
}
notification_elements_.pop_back();
}
ElementTracker::ElementData* ElementTracker::GetOrAddElementData(
ElementIdentifier id,
ElementContext context) {
const LookupKey key(id, context);
const auto [it, added] = element_data_.try_emplace(key, this, id, context);
// This might be the first time we've referenced this identifier, so make
// sure it's registered.
if (added)
ElementIdentifier::RegisterKnownIdentifier(id);
return &it->second;
}
void ElementTracker::MaybeCleanup(ElementData* data) {
GarbageCollector::Frame gc_frame(gc_.get());
gc_frame.Add(data);
}
SafeElementReference::SafeElementReference() = default;
SafeElementReference::SafeElementReference(TrackedElement* element)
: element_(element) {
Subscribe();
}
SafeElementReference::SafeElementReference(SafeElementReference&& other)
: element_(other.element_) {
// Have to rebind instead of moving the subscription since the other
// reference's this pointer is bound.
Subscribe();
other.subscription_ = ElementTracker::Subscription();
other.element_ = nullptr;
}
SafeElementReference::SafeElementReference(const SafeElementReference& other)
: element_(other.element_) {
Subscribe();
}
SafeElementReference& SafeElementReference::operator=(TrackedElement* el) {
if (element_ != el) {
element_ = el;
Subscribe();
}
return *this;
}
SafeElementReference& SafeElementReference::operator=(
SafeElementReference&& other) {
if (&other != this) {
element_ = other.element_;
// Have to rebind instead of moving the subscription since the other
// reference's this pointer is bound.
Subscribe();
other.subscription_ = ElementTracker::Subscription();
other.element_ = nullptr;
}
return *this;
}
SafeElementReference& SafeElementReference::operator=(
const SafeElementReference& other) {
if (&other != this) {
element_ = other.element_;
Subscribe();
}
return *this;
}
SafeElementReference::~SafeElementReference() = default;
void SafeElementReference::Subscribe() {
if (!element_) {
if (subscription_)
subscription_ = ElementTracker::Subscription();
return;
}
subscription_ = ElementTracker::GetElementTracker()->AddElementHiddenCallback(
element_->identifier(), element_->context(),
base::BindRepeating(&SafeElementReference::OnElementHidden,
base::Unretained(this)));
}
void SafeElementReference::OnElementHidden(TrackedElement* element) {
if (element != element_)
return;
subscription_ = ElementTracker::Subscription();
element_ = nullptr;
}
} // namespace ui