diff --git a/clay/ui/component/base_view.cc b/clay/ui/component/base_view.cc index d0409c1b8b..fd13fb45bd 100644 --- a/clay/ui/component/base_view.cc +++ b/clay/ui/component/base_view.cc @@ -383,6 +383,15 @@ void BaseView::AddChild(BaseView* child, int index) { if (attach_to_tree_) { child->OnAttachToTree(); } + + if (this == page_view_ && + !child->direct_page_child_first_add_exposure_attempted_) { + child->direct_page_child_first_add_exposure_attempted_ = true; + if (page_view_->HasIntersectionObserverManager()) { + page_view_->intersection_observer_manager() + ->TryNotifyDirectPageChildOnFirstAdd(child); + } + } } void BaseView::OnLayoutFinish(BaseView* view) {} diff --git a/clay/ui/component/base_view.h b/clay/ui/component/base_view.h index d85302eca9..e1bd0f65c9 100644 --- a/clay/ui/component/base_view.h +++ b/clay/ui/component/base_view.h @@ -749,6 +749,7 @@ class BaseView : public TypeIdentifiable, // FIXME(baiqiang): remove focus&text list then move to component std::string item_key_; bool attach_to_tree_ = false; + bool direct_page_child_first_add_exposure_attempted_ = false; std::optional ignore_focus_; BaseView* parent_ = nullptr; PageView* page_view_ = nullptr; diff --git a/clay/ui/component/expose_manager/expose_observer.cc b/clay/ui/component/expose_manager/expose_observer.cc index 09bf787ecb..80980bcfe5 100644 --- a/clay/ui/component/expose_manager/expose_observer.cc +++ b/clay/ui/component/expose_manager/expose_observer.cc @@ -88,6 +88,16 @@ void ExposeObserver::SetExposureHostVisible(bool visible) { exposure_host_visible_ = visible; } +bool ExposeObserver::TryNotifyAppearWithoutGeometry() { + if (!available_ || !root_ || expose_attrs_.exposure_stoped || + !exposure_host_visible_ || + !expose_attrs_.exposure_should_notify_appear_ || IsExposed()) { + return false; + } + NotifyExposureEvent(true); + return true; +} + void ExposeObserver::NotifyAppearEvent(bool appear) { if (attached_view_ && attached_view_->page_view()) { const char* event_name = appear ? "uiappear" : "uidisappear"; diff --git a/clay/ui/component/expose_manager/expose_observer.h b/clay/ui/component/expose_manager/expose_observer.h index 5f3e93b20a..d63d00b0c2 100644 --- a/clay/ui/component/expose_manager/expose_observer.h +++ b/clay/ui/component/expose_manager/expose_observer.h @@ -65,6 +65,19 @@ class ExposeObserver : public IntersectionObserver { void CheckForIntersectionWithTarget() override; + bool HasUIAppearCallback() const { + return expose_attrs_.exposure_should_notify_appear_; + } + + bool IsExposed() const { + return expose_attrs_.expose_state == ExposureState::kExposed; + } + + // This fast path is restricted to a PageView's direct child on its first + // insertion. It bypasses geometry while preserving the normal exposure + // state machine and event payload. + bool TryNotifyAppearWithoutGeometry(); + protected: bool IsOfType(ObserverType type) const override { return type == IntersectionObserver::kExposeObserver; diff --git a/clay/ui/component/intersection_observer_manager.cc b/clay/ui/component/intersection_observer_manager.cc index 20b7e25464..d96b31d553 100644 --- a/clay/ui/component/intersection_observer_manager.cc +++ b/clay/ui/component/intersection_observer_manager.cc @@ -5,6 +5,7 @@ #include "clay/ui/component/intersection_observer_manager.h" #include +#include #include #include @@ -16,6 +17,22 @@ namespace clay { +namespace { + +constexpr double kLargeExposureTargetViewportAreaRatio = 0.5; + +bool HasValidPositiveArea(const BaseView* view) { + return view && std::isfinite(view->Width()) && + std::isfinite(view->Height()) && view->Width() > 0 && + view->Height() > 0; +} + +double Area(const BaseView* view) { + return static_cast(view->Width()) * view->Height(); +} + +} // namespace + void IntersectionObserverManager::StopExposure(bool send_event) { // Do not return only because exposure is already stopped. stop(false) keeps // an active exposure open, so a later stop(true) must still be allowed to @@ -73,20 +90,43 @@ void IntersectionObserverManager::SetExposureHostVisible(bool visible) { it.second->SetExposureHostVisible(visible); } + bool deferred_target_notified = false; + if (visible && !host_hidden_first_add_pending_views_.empty()) { + const std::vector pending_targets( + host_hidden_first_add_pending_views_.begin(), + host_hidden_first_add_pending_views_.end()); + host_hidden_first_add_pending_views_.clear(); + for (auto* target : pending_targets) { + if (TryReconcileDeferredDirectPageChildAfterHostVisible(target)) { + deferred_target_notified = true; + } + } + } + if (visible && !expose_observers_map_.empty()) { last_expose_time_ = -1; page_view_->page_view()->RequestPaint(); } + if (deferred_target_notified) { + page_view_->page_view()->SendGlobalExposureEvent(); + } } void IntersectionObserverManager::EraseExposeObserver( - const BaseView* view, const ExposeObserver* target) { + BaseView* view, const ExposeObserver* target) { if (view) { + layout_fast_path_candidate_views_.erase(view); + layout_fast_path_attempted_views_.erase(view); + host_hidden_first_add_pending_views_.erase(view); expose_observers_map_.erase(view); } else if (target) { for (auto iter = expose_observers_map_.begin(); iter != expose_observers_map_.end();) { if ((iter->second).get() == target) { + auto* attached_view = iter->second->GetAttachedView(); + layout_fast_path_candidate_views_.erase(attached_view); + layout_fast_path_attempted_views_.erase(attached_view); + host_hidden_first_add_pending_views_.erase(attached_view); iter = expose_observers_map_.erase(iter); return; } else { @@ -101,8 +141,7 @@ void IntersectionObserverManager::RemoveObserver( if (target->IsOfType(IntersectionObserver::kIntersectionObserver)) { EraseObserver(intersection_observers_, nullptr, target); } else if (target->IsOfType(IntersectionObserver::kExposeObserver)) { - expose_observers_map_.erase(target->GetAttachedView()); - EraseExposeObserver(nullptr, static_cast(target)); + EraseExposeObserver(target->GetAttachedView()); } } @@ -163,10 +202,17 @@ void IntersectionObserverManager::NotifyTargetAttached(BaseView* view) { } void IntersectionObserverManager::NotifyTargetDetached(BaseView* view) { + layout_fast_path_candidate_views_.erase(view); + layout_fast_path_attempted_views_.erase(view); + host_hidden_first_add_pending_views_.erase(view); NotifyAllObserver(&IntersectionObserver::OnDetach, view); } void IntersectionObserverManager::ReconcileExposureForTarget(BaseView* view) { + if (view) { + layout_fast_path_candidate_views_.erase(view); + layout_fast_path_attempted_views_.insert(view); + } if (exposure_stopped_ || !exposure_host_visible_ || !view || !view->attach_to_tree()) { return; @@ -174,6 +220,164 @@ void IntersectionObserverManager::ReconcileExposureForTarget(BaseView* view) { NotifyExposures(&IntersectionObserver::CheckForIntersectionWithTarget, view); } +bool IntersectionObserverManager::TryNotifyDirectPageChildOnFirstAdd( + BaseView* target) { + const bool direct_page_child = + target && page_view_ && target->Parent() == page_view_; + auto observer_it = expose_observers_map_.find(target); + auto* observer = observer_it == expose_observers_map_.end() + ? nullptr + : observer_it->second.get(); + const bool has_uiappear = observer && observer->HasUIAppearCallback(); + + if (!direct_page_child) { + return false; + } + if (!observer) { + return false; + } + if (!has_uiappear) { + return false; + } + if (!target->attach_to_tree()) { + return false; + } + if (exposure_stopped_) { + return false; + } + if (!exposure_host_visible_) { + host_hidden_first_add_pending_views_.insert(target); + return false; + } + host_hidden_first_add_pending_views_.erase(target); + layout_fast_path_candidate_views_.erase(target); + layout_fast_path_attempted_views_.insert(target); + if (observer->IsExposed()) { + page_view_->page_view()->SendGlobalExposureEvent(); + return true; + } + if (!observer->TryNotifyAppearWithoutGeometry()) { + return false; + } + + page_view_->page_view()->SendGlobalExposureEvent(); + return true; +} + +bool IntersectionObserverManager:: + TryReconcileDeferredDirectPageChildAfterHostVisible(BaseView* target) { + const bool direct_page_child = + target && page_view_ && target->Parent() == page_view_; + auto observer_it = expose_observers_map_.find(target); + auto* observer = observer_it == expose_observers_map_.end() + ? nullptr + : observer_it->second.get(); + const bool has_uiappear = observer && observer->HasUIAppearCallback(); + + if (!direct_page_child) { + return false; + } + if (!observer) { + return false; + } + if (!has_uiappear) { + return false; + } + if (!target->attach_to_tree()) { + return false; + } + if (exposure_stopped_) { + return false; + } + if (!exposure_host_visible_) { + return false; + } + layout_fast_path_candidate_views_.erase(target); + layout_fast_path_attempted_views_.insert(target); + if (observer->IsExposed()) { + return false; + } + + if (!observer->TryNotifyAppearWithoutGeometry()) { + return false; + } + + return true; +} + +bool IntersectionObserverManager::TryReconcileLargeExposureTargetAfterLayout( + BaseView* layout_view) { + if (!layout_view || !page_view_ || expose_observers_map_.empty() || + !HasValidPositiveArea(page_view_)) { + return false; + } + + const double page_area = Area(page_view_); + if (HasExposeObserver(layout_view) && + layout_fast_path_attempted_views_.count(layout_view) == 0 && + HasValidPositiveArea(layout_view) && + Area(layout_view) > page_area * kLargeExposureTargetViewportAreaRatio) { + layout_fast_path_candidate_views_.insert(layout_view); + } + if (layout_fast_path_candidate_views_.empty()) { + return false; + } + + // An observed target may receive layout before a full-screen wrapper. Only + // recheck the target when its own or one of its ancestors' bounds changes; + // unrelated layout updates do not scan the pending candidates. + bool reconciled = false; + const std::vector candidates( + layout_fast_path_candidate_views_.begin(), + layout_fast_path_candidate_views_.end()); + for (auto* target : candidates) { + if (!HasExposeObserver(target) || + layout_fast_path_attempted_views_.count(target) != 0) { + layout_fast_path_candidate_views_.erase(target); + continue; + } + + bool relevant_layout_update = layout_view == target; + for (auto* ancestor = target->Parent(); !relevant_layout_update && ancestor; + ancestor = ancestor->Parent()) { + relevant_layout_update = ancestor == layout_view; + if (ancestor == page_view_) { + break; + } + } + if (!relevant_layout_update) { + continue; + } + + const bool attached = target->attach_to_tree(); + bool large_ready_ancestor_chain = attached; + BaseView* current = target; + while (large_ready_ancestor_chain && current && current != page_view_) { + if (!HasValidPositiveArea(current) || + Area(current) <= page_area * kLargeExposureTargetViewportAreaRatio) { + large_ready_ancestor_chain = false; + break; + } + current = current->Parent(); + } + + const bool page_descendant = current == page_view_; + const bool eligible = !exposure_stopped_ && exposure_host_visible_ && + large_ready_ancestor_chain && page_descendant; + if (!eligible) { + continue; + } + + ReconcileExposureForTarget(target); + reconciled = true; + } + + if (reconciled) { + page_view_->page_view()->SendGlobalExposureEvent(); + } + return reconciled; +} + void IntersectionObserverManager::NotifyExposures( void (IntersectionObserver::*ptr)(), BaseView* view) { for (auto& it : expose_observers_map_) { diff --git a/clay/ui/component/intersection_observer_manager.h b/clay/ui/component/intersection_observer_manager.h index 157deaa3c8..f83d19e1dd 100644 --- a/clay/ui/component/intersection_observer_manager.h +++ b/clay/ui/component/intersection_observer_manager.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "clay/ui/component/base_view.h" @@ -34,6 +35,8 @@ class IntersectionObserverManager { void NotifyTargetAttached(BaseView* view); void NotifyTargetDetached(BaseView* view); void ReconcileExposureForTarget(BaseView* view); + bool TryNotifyDirectPageChildOnFirstAdd(BaseView* view); + bool TryReconcileLargeExposureTargetAfterLayout(BaseView* view); void RemoveExposeObserver(BaseView* view); bool UpdateExposeData(const char* attr_key, const clay::Value& value, @@ -61,9 +64,13 @@ class IntersectionObserverManager { std::list> intersection_observers_; std::unordered_map> expose_observers_map_; + std::unordered_set layout_fast_path_candidate_views_; + std::unordered_set layout_fast_path_attempted_views_; + std::unordered_set host_hidden_first_add_pending_views_; - void EraseExposeObserver(const BaseView* view = nullptr, + void EraseExposeObserver(BaseView* view = nullptr, const ExposeObserver* target = nullptr); + bool TryReconcileDeferredDirectPageChildAfterHostVisible(BaseView* view); template void EraseObserver(std::list& container, BaseView* attached_view, diff --git a/clay/ui/component/view_context.cc b/clay/ui/component/view_context.cc index 8939b037fa..6af6218a27 100644 --- a/clay/ui/component/view_context.cc +++ b/clay/ui/component/view_context.cc @@ -454,6 +454,10 @@ void ViewContext::SetBounds(int id, float left, float top, float width, view->SetBound( GetPageView()->RoundPixels(left), GetPageView()->RoundPixels(top), GetPageView()->RoundPixels(width), GetPageView()->RoundPixels(height)); + if (page_view_->HasIntersectionObserverManager()) { + page_view_->intersection_observer_manager() + ->TryReconcileLargeExposureTargetAfterLayout(view); + } } void ViewContext::SetPaddings(int id, float padding_left, float padding_top,