blob: d32d7de30986a863365d6b76dd104efe57bf868d [file] [log] [blame]
// Copyright 2021 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 "ui/base/interaction/element_tracker.h"
#include <algorithm>
#include <iterator>
#include <list>
#include <map>
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/containers/contains.h"
#include "ui/base/interaction/element_identifier.h"
namespace ui {
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);
}
~ElementData() = default;
ElementIdentifier identifier() const { return identifier_; }
ElementContext context() const { return context_; }
bool empty() const {
return elements_.empty() && shown_callbacks_.empty() &&
activated_callbacks_.empty() && hidden_callbacks_.empty();
}
size_t num_elements() const {
// Guaranteed O(1) in C++11 and later.
return elements_.size();
}
const std::list<TrackedElement*>& 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);
}
void NotifyElementShown(TrackedElement* element) {
DCHECK_EQ(identifier().raw_value(), element->identifier().raw_value());
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(TrackedElement* element) {
DCHECK(base::Contains(element_lookup_, element));
activated_callbacks_.Notify(element);
}
void NotifyElementHidden(TrackedElement* element) {
const auto it = element_lookup_.find(element);
DCHECK(it != element_lookup_.end());
elements_.erase(it->second);
element_lookup_.erase(it);
hidden_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<TrackedElement*> 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<TrackedElement*, std::list<TrackedElement*>::iterator>
element_lookup_;
base::RepeatingCallbackList<void(TrackedElement*)> shown_callbacks_;
base::RepeatingCallbackList<void(TrackedElement*)> activated_callbacks_;
base::RepeatingCallbackList<void(TrackedElement*)> hidden_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:
GarbageCollector* const 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();
}
ElementTracker* const tracker_;
std::set<ElementData*> candidates_;
int frame_count_ = 0;
};
TrackedElement::TrackedElement(ElementIdentifier id, ElementContext context)
: identifier_(id), context_(context) {}
TrackedElement::~TrackedElement() = default;
// 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();
}
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::copy(it->second->elements().begin(), it->second->elements().end(),
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::Subscription ElementTracker::AddElementShownCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
return GetOrAddElementData(id, context)->AddElementShownCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddElementActivatedCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
return GetOrAddElementData(id, context)
->AddElementActivatedCallback(callback);
}
ElementTracker::Subscription ElementTracker::AddElementHiddenCallback(
ElementIdentifier id,
ElementContext context,
Callback callback) {
return GetOrAddElementData(id, context)->AddElementHiddenCallback(callback);
}
ElementTracker::ElementTracker()
: gc_(std::make_unique<GarbageCollector>(this)) {}
ElementTracker::~ElementTracker() {
NOTREACHED();
}
void ElementTracker::NotifyElementShown(TrackedElement* element) {
GarbageCollector::Frame gc_frame(gc_.get());
DCHECK(!base::Contains(element_to_data_lookup_, element));
ElementData* const element_data =
GetOrAddElementData(element->identifier(), element->context());
element_to_data_lookup_.emplace(element, element_data);
element_data->NotifyElementShown(element);
}
void ElementTracker::NotifyElementActivated(TrackedElement* element) {
GarbageCollector::Frame gc_frame(gc_.get());
const auto it = element_to_data_lookup_.find(element);
DCHECK(it != element_to_data_lookup_.end());
it->second->NotifyElementActivated(element);
}
void ElementTracker::NotifyElementHidden(TrackedElement* element) {
GarbageCollector::Frame gc_frame(gc_.get());
const auto it = element_to_data_lookup_.find(element);
DCHECK(it != element_to_data_lookup_.end());
ElementData* const data = it->second;
element_to_data_lookup_.erase(it);
data->NotifyElementHidden(element);
gc_frame.Add(data);
}
ElementTracker::ElementData* ElementTracker::GetOrAddElementData(
ElementIdentifier id,
ElementContext context) {
const LookupKey key(id, context);
auto it = element_data_.find(key);
if (it == element_data_.end()) {
const auto result = element_data_.emplace(
key, std::make_unique<ElementData>(this, id, context));
DCHECK(result.second);
it = result.first;
}
return it->second.get();
}
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::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() = default;
void SafeElementReference::Subscribe() {
if (!element_)
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