diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index b63cf703c8..c85c5feeca 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -446,6 +446,10 @@ if (${WEBF_JS_ENGINE} MATCHES "quickjs") core/dom/legacy/bounding_client_rect.cc core/input/touch.cc core/input/touch_list.cc + + #IntersectionObserver + core/dom/intersection_observer.cc + core/dom/intersection_observer_entry.cc ) # Gen sources. @@ -588,6 +592,11 @@ if (${WEBF_JS_ENGINE} MATCHES "quickjs") out/element_attribute_names.cc out/element_namespace_uris.cc + # IntersectionObserver + out/qjs_intersection_observer.cc + out/qjs_intersection_observer_entry.cc + out/qjs_intersection_observer_init.cc + # SVG generated out/svg_names.cc out/svg_element_factory.cc diff --git a/bridge/bindings/qjs/binding_initializer.cc b/bridge/bindings/qjs/binding_initializer.cc index 6614749aaa..e90b815fb7 100644 --- a/bridge/bindings/qjs/binding_initializer.cc +++ b/bridge/bindings/qjs/binding_initializer.cc @@ -60,6 +60,8 @@ #include "qjs_inline_css_style_declaration.h" #include "qjs_input_event.h" #include "qjs_intersection_change_event.h" +#include "qjs_intersection_observer.h" +#include "qjs_intersection_observer_entry.h" #include "qjs_keyboard_event.h" #include "qjs_location.h" #include "qjs_message_event.h" @@ -208,6 +210,10 @@ void InstallBindings(ExecutingContext* context) { QJSSVGLineElement::Install(context); QJSNativeLoader::Install(context); + // IntersectionObserver + QJSIntersectionObserver::Install(context); + QJSIntersectionObserverEntry::Install(context); + // Legacy bindings, not standard. QJSElementAttributes::Install(context); } diff --git a/bridge/bindings/qjs/wrapper_type_info.h b/bridge/bindings/qjs/wrapper_type_info.h index b15b2efd30..a1a8a3b1a5 100644 --- a/bridge/bindings/qjs/wrapper_type_info.h +++ b/bridge/bindings/qjs/wrapper_type_info.h @@ -120,6 +120,10 @@ enum { JS_CLASS_SVG_LENGTH, JS_CLASS_SVG_ANIMATED_LENGTH, + // IntersectionObserver + JS_CLASS_INTERSECTION_OBSERVER, + JS_CLASS_INTERSECTION_OBSERVER_ENTRY, + JS_CLASS_CUSTOM_CLASS_INIT_COUNT /* last entry for predefined classes */ }; diff --git a/bridge/core/binding_object.cc b/bridge/core/binding_object.cc index 3d32da15d3..f45cb8f958 100644 --- a/bridge/core/binding_object.cc +++ b/bridge/core/binding_object.cc @@ -60,10 +60,12 @@ void NativeBindingObject::HandleCallFromDartSide(const DartIsolateContext* dart_ dart_isolate_context->profiler()->StartTrackEvaluation(profile_id); - const AtomicString method = - AtomicString(binding_object->binding_target_->ctx(), - std::unique_ptr(static_cast(native_method->u.ptr))); - const NativeValue result = binding_object->binding_target_->HandleCallFromDartSide(method, argc, argv, dart_object); + auto context = binding_object->binding_target_->ctx(); + AtomicString method = native_method != nullptr + ? AtomicString(context, std::unique_ptr( + reinterpret_cast(native_method->u.ptr))) + : AtomicString(context, ""); + NativeValue result = binding_object->binding_target_->HandleCallFromDartSide(method, argc, argv, dart_object); auto* return_value = new NativeValue(); std::memcpy(return_value, &result, sizeof(NativeValue)); diff --git a/bridge/core/binding_object.h b/bridge/core/binding_object.h index c308b0fe18..5dd9d9bfd1 100644 --- a/bridge/core/binding_object.h +++ b/bridge/core/binding_object.h @@ -74,6 +74,7 @@ enum CreateBindingObjectType { kCreateDOMMatrix = 0, kCreatePath2D = 1, kCreateDOMPoint = 2, + kCreateIntersectionObserver = 3, }; struct BindingObjectPromiseContext : public DartReadable { diff --git a/bridge/core/dom/dom_string_map.cc b/bridge/core/dom/dom_string_map.cc index 552ee4116f..1266045306 100644 --- a/bridge/core/dom/dom_string_map.cc +++ b/bridge/core/dom/dom_string_map.cc @@ -150,7 +150,8 @@ bool DOMStringMap::SetItem(const webf::AtomicString& key, } auto attribute_name = AtomicString(ctx(), ConvertPropertyNameToAttributeName(key.ToStdString(ctx()))); - return owner_element_->attributes()->setAttribute(attribute_name, value, exception_state); + owner_element_->setAttribute(attribute_name, value, exception_state); + return true; } bool DOMStringMap::DeleteItem(const webf::AtomicString& key, webf::ExceptionState& exception_state) { diff --git a/bridge/core/dom/intersection_observer.cc b/bridge/core/dom/intersection_observer.cc new file mode 100644 index 0000000000..cd39f99a07 --- /dev/null +++ b/bridge/core/dom/intersection_observer.cc @@ -0,0 +1,145 @@ +// 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. + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +#include "core/dom/intersection_observer.h" + +#include +#include + +#include +#include "bindings/qjs/converter_impl.h" +#include "core/dom/element.h" +#include "core/dom/intersection_observer_entry.h" +#include "core/dom/node.h" +#include "core/executing_context.h" +#include "foundation/logging.h" +#include "qjs_intersection_observer_init.h" + +namespace webf { + +IntersectionObserver* IntersectionObserver::Create(ExecutingContext* context, + const std::shared_ptr& function, + ExceptionState& exception_state) { + return MakeGarbageCollected(context, function); +} + +IntersectionObserver* IntersectionObserver::Create(ExecutingContext* context, + const std::shared_ptr& function, + const std::shared_ptr& observer_init, + ExceptionState& exception_state) { + return MakeGarbageCollected(context, function, observer_init); +} + +IntersectionObserver::IntersectionObserver(ExecutingContext* context, const std::shared_ptr& function) + : BindingObject(context->ctx()), function_(function) { + GetExecutingContext()->dartMethodPtr()->createBindingObject( + GetExecutingContext()->isDedicated(), GetExecutingContext()->contextId(), bindingObject(), + CreateBindingObjectType::kCreateIntersectionObserver, nullptr, 0); +} + +IntersectionObserver::IntersectionObserver(ExecutingContext* context, + const std::shared_ptr& function, + const std::shared_ptr& observer_init) + : BindingObject(context->ctx()), function_(function) { + if (observer_init && observer_init->hasRoot()) { + root_ = observer_init->root(); + } + NativeValue arguments[1]; + if (observer_init && observer_init->hasThreshold()) { +#if ENABLE_LOG + WEBF_LOG(DEBUG) << "[IntersectionObserver]: Constructor threshold.size = " << observer_init->threshold().size() + << std::endl; +#endif + thresholds_ = std::move(observer_init->threshold()); + std::sort(thresholds_.begin(), thresholds_.end()); + arguments[0] = NativeValueConverter>::ToNativeValue(thresholds_); + } + GetExecutingContext()->dartMethodPtr()->createBindingObject( + GetExecutingContext()->isDedicated(), GetExecutingContext()->contextId(), bindingObject(), + CreateBindingObjectType::kCreateIntersectionObserver, arguments, 1); +} + +bool IntersectionObserver::RootIsValid() const { + return RootIsImplicit() || root(); +} + +void IntersectionObserver::observe(Element* target, ExceptionState& exception_state) { + if (!RootIsValid() || !target) { + WEBF_LOG(ERROR) << "[IntersectionObserver]: observe valid:" << std::endl; + return; + } + +#if ENABLE_LOG + WEBF_LOG(DEBUG) << "[IntersectionObserver]: observe target=" << target << ",tagName=" << target->nodeName() + << std::endl; +#endif + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kAddIntersectionObserver, nullptr, bindingObject(), + target->bindingObject()); +} + +void IntersectionObserver::unobserve(Element* target, ExceptionState& exception_state) { + if (!target) { + WEBF_LOG(ERROR) << "[IntersectionObserver]: unobserve valid:" << std::endl; + return; + } + + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kRemoveIntersectionObserver, nullptr, bindingObject(), + target->bindingObject()); +} + +void IntersectionObserver::disconnect(ExceptionState& exception_state) { + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kDisconnectIntersectionObserver, nullptr, + bindingObject(), nullptr); +} + +NativeValue IntersectionObserver::HandleCallFromDartSide(const AtomicString& method, + int32_t argc, + const NativeValue* argv, + Dart_Handle dart_object) { + if (!GetExecutingContext() || !GetExecutingContext()->IsContextValid()) { + WEBF_LOG(ERROR) << "[IntersectionObserver]: HandleCallFromDartSide Context Valid" << std::endl; + return Native_NewNull(); + } + + MemberMutationScope scope{GetExecutingContext()}; + + NativeIntersectionObserverEntry* native_entry = + NativeValueConverter>::FromNativeValue(argv[0]); + size_t length = NativeValueConverter::FromNativeValue(argv[1]); + + if (length > 0) { + assert(function_ != nullptr); + JSValue js_array = JS_NewArray(ctx()); + for (int i = 0; i < length; i++) { + auto* entry = MakeGarbageCollected( + GetExecutingContext(), native_entry[i].is_intersecting, native_entry[i].intersectionRatio, + DynamicTo(BindingObject::From(native_entry[i].target))); + JS_SetPropertyUint32(ctx(), js_array, i, entry->ToQuickJS()); + } + ScriptValue arguments[] = {ScriptValue(ctx(), js_array), ToValue()}; + +#if ENABLE_LOG + WEBF_LOG(DEBUG) << "[IntersectionObserver]: HandleCallFromDartSide length=" << length << ",JS function_ Invoke" + << std::endl; +#endif + + function_->Invoke(ctx(), ToValue(), 2, arguments); + + JS_FreeValue(ctx(), js_array); + } else { + WEBF_LOG(ERROR) << "[IntersectionObserver]: HandleCallFromDartSide entries is empty"; + } + + return Native_NewNull(); +} + +void IntersectionObserver::Trace(GCVisitor* visitor) const { + BindingObject::Trace(visitor); + + function_->Trace(visitor); +} + +} // namespace webf diff --git a/bridge/core/dom/intersection_observer.d.ts b/bridge/core/dom/intersection_observer.d.ts new file mode 100644 index 0000000000..acb226ff2a --- /dev/null +++ b/bridge/core/dom/intersection_observer.d.ts @@ -0,0 +1,28 @@ +// 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. + +// https://www.w3.org/TR/intersection-observer/#intersection-observer-interface + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +import {IntersectionObserverInit} from "./intersection_observer_init"; +import {IntersectionObserverEntry} from "./intersection_observer_entry"; +import {Node} from "./node"; +import {Element} from "./element"; + +interface IntersectionObserver { + new(callback: Function, options?: IntersectionObserverInit): IntersectionObserver; + + //readonly root: Node | null; + //readonly rootMargin: string; + //readonly scrollMargin: string; + //readonly thresholds: number[]; + //readonly delay: number; + //readonly trackVisibility: boolean; + + observe(target: Element): void; + unobserve(target: Element): void; + disconnect(): void; + //takeRecords(): IntersectionObserverEntry[]; +} diff --git a/bridge/core/dom/intersection_observer.h b/bridge/core/dom/intersection_observer.h new file mode 100644 index 0000000000..bc143e073c --- /dev/null +++ b/bridge/core/dom/intersection_observer.h @@ -0,0 +1,209 @@ +// 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. + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +#ifndef WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_H_ +#define WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_H_ + +#include +#include +#include "bindings/qjs/atomic_string.h" +#include "bindings/qjs/cppgc/garbage_collected.h" +#include "bindings/qjs/cppgc/member.h" +#include "bindings/qjs/exception_state.h" +#include "core/binding_object.h" +#include "out/qjs_intersection_observer_init.h" + +namespace webf { + +class Element; +class ExceptionState; +class IntersectionObserverEntry; +class Node; +class ScriptState; + +class IntersectionObserver final : public BindingObject { + DEFINE_WRAPPERTYPEINFO(); + + public: + // The IntersectionObserver can be configured to notify based on changes to + // how much of the target element's area intersects with the root, or based on + // changes to how much of the root element's area intersects with the + // target. Examples illustrating the distinction: + // + // 1.0 of target, 0.5 of target, 1.0 of target, + // 0.25 of root 0.5 of root 1.0 of root + // +------------------+ +------------------+ *~~~~~~~~~~~~~~~~~~* + // | ////////// | | | ;//////////////////; + // | ////////// | | | ;//////////////////; + // | ////////// | ;//////////////////; ;//////////////////; + // | | ;//////////////////; ;//////////////////; + // +------------------+ *~~~~~~~~~~~~~~~~~~* *~~~~~~~~~~~~~~~~~~* + // //////////////////// + // //////////////////// + // //////////////////// + // enum ThresholdInterpretation { kFractionOfTarget, kFractionOfRoot }; + + // This value can be used to detect transitions between non-intersecting or + // edge-adjacent (i.e., zero area) state, and intersecting by any non-zero + // number of pixels. + // static constexpr float kMinimumThreshold = + // IntersectionGeometry::kMinimumThreshold; + + // Used to specify when callbacks should be invoked with new notifications. + // Blink-internal users of IntersectionObserver will have their callbacks + // invoked synchronously either at the end of a lifecycle update or in the + // middle of the lifecycle post layout. Javascript observers will PostTask to + // invoke their callbacks. + // enum DeliveryBehavior { kDeliverDuringPostLayoutSteps, kDeliverDuringPostLifecycleSteps, kPostTaskToDeliver }; + + // Used to specify whether the margins apply to the root element or the source + // element. The effect of the root element margins is that intermediate + // scrollers clip content by its bounding box without considering margins. + // That is, margins only apply to the last scroller (root). The effect of + // source element margins is that the margins apply to the first / deepest + // clipper, but do not apply to any other clippers. Note that in a case of a + // single clipper, the two approaches are equivalent. + // + // Note that the percentage margin is resolved against the root rect, even + // when the margin is applied to the target. + // enum MarginTarget { kApplyMarginToRoot, kApplyMarginToTarget }; + + static IntersectionObserver* Create(ExecutingContext* context, + const std::shared_ptr& function, + ExceptionState& exception_state); + + static IntersectionObserver* Create(ExecutingContext* context, + const std::shared_ptr& function, + const std::shared_ptr& observer_init, + ExceptionState& exception_state); + + IntersectionObserver(ExecutingContext* context, const std::shared_ptr& function); + IntersectionObserver(ExecutingContext* context, + const std::shared_ptr& function, + const std::shared_ptr& observer_init); + + // TODO(pengfei12.guo): Params not supported + // struct Params { + // WEBF_STACK_ALLOCATED(); + // + // public: + // Node* root; + // Vector margin; + // MarginTarget margin_target = kApplyMarginToRoot; + // Vector scroll_margin; + // + // // Elements should be in the range [0,1], and are interpreted according to + // // the given `semantics`. + // Vector thresholds; + // ThresholdInterpretation semantics = kFractionOfTarget; + // + // DeliveryBehavior behavior = kDeliverDuringPostLifecycleSteps; + // // Specifies the minimum period between change notifications. + // base::TimeDelta delay; + // bool track_visibility = false; + // bool always_report_root_bounds = false; + // // Indicates whether the overflow clip edge should be used instead of the + // // bounding box if appropriate. + // bool use_overflow_clip_edge = false; + // bool needs_initial_observation_with_detached_target = true; + // }; + + NativeValue HandleCallFromDartSide(const AtomicString& method, + int32_t argc, + const NativeValue* argv, + Dart_Handle dart_object) override; + + // API methods. + void observe(Element*, ExceptionState&); + void unobserve(Element*, ExceptionState&); + void disconnect(ExceptionState&); + // TODO(pengfei12.guo): not supported + // std::vector> takeRecords(ExceptionState&); + + // API attributes. + [[nodiscard]] Node* root() const { return root_; } + // TODO(pengfei12.guo): not supported + // AtomicString rootMargin() const; + // TODO(pengfei12.guo): not supported + // AtomicString scrollMargin() const; + + [[nodiscard]] const std::vector& thresholds() const { return thresholds_; } + // TODO(pengfei12.guo): not supported + // DOMHighResTimeStamp delay() const { + // if (delay_ != std::numeric_limits::min() && delay_ != std::numeric_limits::max()) { + // return delay_ / 1000; + // } + // return (delay_ < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); + // } + + // An observer can either track intersections with an explicit root Node, + // or with the the top-level frame's viewport (the "implicit root"). When + // tracking the implicit root, root_ will be null, but because root_ is a + // weak pointer, we cannot surmise that this observer tracks the implicit + // root just because root_ is null. Hence root_is_implicit_. + [[nodiscard]] bool RootIsImplicit() const { + // return root_is_implicit_; + // If the root option is not specified, the viewport is used as the root element by default. + return root_ == nullptr; + } + + // TODO(pengfei12.guo): TimeDelta not support + // base::TimeDelta GetEffectiveDelay() const; + + // TODO(pengfei12.guo): RootMargin not support + // std::vector RootMargin() const { + // return margin_target_ == kApplyMarginToRoot ? margin_ : Vector(); + //} + + // TODO(pengfei12.guo): TargetMargin not support + // Vector TargetMargin() const { + // return margin_target_ == kApplyMarginToTarget ? margin_ : Vector(); + //} + + // TODO(pengfei12.guo): ScrollMargin not support + // Vector ScrollMargin() const { return scroll_margin_; } + + // TODO(pengfei12.guo): ComputeIntersections impl by dart + // Returns the number of IntersectionObservations that recomputed geometry. + // int64_t ComputeIntersections(unsigned flags, ComputeIntersectionsContext&); + + // TODO(pengfei12.guo): GetUkmMetricId not support + // bool IsInternal() const; + + // TODO(pengfei12.guo): GetUkmMetricId not support + // The metric id for tracking update time via UpdateTime metrics, or null for + // internal intersection observers without explicit metrics. + // std::optional GetUkmMetricId() const { + // return ukm_metric_id_; + //} + + // Returns false if this observer has an explicit root node which has been + // deleted; true otherwise. + bool RootIsValid() const; + + void Trace(GCVisitor*) const override; + + private: + // We use UntracedMember<> here to do custom weak processing. + Node* root_; + std::vector thresholds_; + + // TODO(pengfei12.guo): not support + // const std::vector margin_; + // const std::vector scroll_margin_; + // const MarginTarget margin_target_; + // const unsigned root_is_implicit_ : 1; + // const unsigned track_visibility_ : 1; + // const unsigned track_fraction_of_root_ : 1; + // const unsigned always_report_root_bounds_ : 1; + // const unsigned use_overflow_clip_edge_ : 1; + + std::shared_ptr function_; +}; + +} // namespace webf + +#endif // WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_H_ diff --git a/bridge/core/dom/intersection_observer_entry.cc b/bridge/core/dom/intersection_observer_entry.cc new file mode 100644 index 0000000000..a57022a85a --- /dev/null +++ b/bridge/core/dom/intersection_observer_entry.cc @@ -0,0 +1,25 @@ +// 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. + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +#include "core/dom/intersection_observer_entry.h" +#include "core/dom/element.h" + +namespace webf { + +IntersectionObserverEntry::IntersectionObserverEntry(ExecutingContext* context, + bool isIntersecting, + double intersectionRatio, + Element* target) + : ScriptWrappable(context->ctx()), + isIntersecting_(isIntersecting), + intersectionRatio_(intersectionRatio), + target_(target) {} + +void IntersectionObserverEntry::Trace(GCVisitor* visitor) const { + visitor->TraceMember(target_); +} + +} // namespace webf diff --git a/bridge/core/dom/intersection_observer_entry.d.ts b/bridge/core/dom/intersection_observer_entry.d.ts new file mode 100644 index 0000000000..f42ae84c26 --- /dev/null +++ b/bridge/core/dom/intersection_observer_entry.d.ts @@ -0,0 +1,29 @@ +// 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. + +// https://wicg.github.io/IntersectionObserver/#intersection-observer-entry + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +import {Element} from "./element"; + +export interface IntersectionObserverEntry { + // TODO(pengfei12.guo): not supported + // readonly time: DOMHighResTimeStamp; + // TODO(szager): |rootBounds| should not be nullable. + // readonly rootBounds: DOMRectReadOnly | null; + // readonly boundingClientRect: DOMRectReadOnly; + // readonly intersectionRect: DOMRectReadOnly; + // readonly isVisible: boolean; + + readonly isIntersecting: boolean; + + readonly intersectionRatio: number; + + readonly target: Element; + + new(): void; +} + + diff --git a/bridge/core/dom/intersection_observer_entry.h b/bridge/core/dom/intersection_observer_entry.h new file mode 100644 index 0000000000..b344e883ca --- /dev/null +++ b/bridge/core/dom/intersection_observer_entry.h @@ -0,0 +1,63 @@ +// 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. + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +#ifndef WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_ENTRY_H_ +#define WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_ENTRY_H_ + +#include "bindings/qjs/cppgc/member.h" +#include "bindings/qjs/script_wrappable.h" + +namespace webf { + +class Element; + +struct NativeIntersectionObserverEntry : public DartReadable { + int8_t is_intersecting; + double intersectionRatio; + NativeBindingObject* target; +}; + +class IntersectionObserverEntry final : public ScriptWrappable { + DEFINE_WRAPPERTYPEINFO(); + + public: + IntersectionObserverEntry() = delete; + explicit IntersectionObserverEntry(ExecutingContext* context, + bool isIntersecting, + double intersectionRatio, + Element* target); + + // TODO(pengfei12.guo): not supported + // IDL interface + // double time() const { return time_; } + double intersectionRatio() const { return intersectionRatio_; } + // DOMRectReadOnly* boundingClientRect() const; + // DOMRectReadOnly* rootBounds() const; + // DOMRectReadOnly* intersectionRect() const; + // bool isVisible() const { + // return geometry_.IsVisible(); + //} + + bool isIntersecting() const { return isIntersecting_; } + + Element* target() const { return target_.Get(); } + + // TODO(pengfei12.guo): IntersectionGeometry not supported + // blink-internal interface + // const IntersectionGeometry& GetGeometry() const { return geometry_; } + void Trace(GCVisitor*) const override; + + private: + // IntersectionGeometry geometry_; + double intersectionRatio_; + bool isIntersecting_; + // DOMHighResTimeStamp time_; + Member target_; +}; + +} // namespace webf + +#endif // WEBF_CORE_INTERSECTION_OBSERVER_INTERSECTION_OBSERVER_ENTRY_H_ diff --git a/bridge/core/dom/intersection_observer_init.d.ts b/bridge/core/dom/intersection_observer_init.d.ts new file mode 100644 index 0000000000..644858f3e4 --- /dev/null +++ b/bridge/core/dom/intersection_observer_init.d.ts @@ -0,0 +1,21 @@ +// 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. + +// https://wicg.github.io/IntersectionObserver/#intersection-observer-init + +// Copyright (C) 2024-present The WebF authors. All rights reserved. + +import {Node} from "./node"; + +// @ts-ignore +@Dictionary() +export interface IntersectionObserverInit { + root?: Node | null; + // TODO(pengfei12.guo): Just definition, no implementation. + rootMargin?: string; + threshold?: number[]; + // scrollMargin?: string; + // delay?: number; + // trackVisibility?: boolean; +} diff --git a/bridge/core/dom/node_test.cc b/bridge/core/dom/node_test.cc index 83ef523614..645689d4f8 100644 --- a/bridge/core/dom/node_test.cc +++ b/bridge/core/dom/node_test.cc @@ -74,6 +74,47 @@ Promise.resolve().then(() => { EXPECT_EQ(logCalled, true); } +TEST(Node, IntersectionObserver) { + bool static errorCalled = false; + bool static logCalled = false; + webf::WebFPage::consoleMessageHandler = [](void* ctx, const std::string& message, int logLevel) { logCalled = true; }; + auto env = TEST_init([](double contextId, const char* errmsg) { errorCalled = true; }); + auto context = env->page()->executingContext(); + const char* code = R"( + // Create the observed element + const container = document.createElement('div'); + document.body.appendChild(container); + // Callback function to execute when mutations are observed + const callback = (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { // Element is visible + console.log('Element is intersecting'); + } else { // Element is not visible + console.log('Element is not intersecting'); + } + }); + }; + + const options = { + root: null, // Use the viewport as the root + rootMargin: "0px", + threshold: [0, 1], // Trigger callback at 0% and 100% visibility + }; + + // Create an observer instance linked to the callback function + const observer = new IntersectionObserver(callback, options); + + // Start observing the target element + observer.observe(container); + )"; + env->page()->evaluateScript(code, strlen(code), "vm://", 0); + + TEST_runLoop(context); + + EXPECT_EQ(errorCalled, false); + EXPECT_EQ(logCalled, true); +} + TEST(Node, nodeName) { bool static errorCalled = false; bool static logCalled = false; @@ -367,4 +408,4 @@ console.assert(el.isConnected == false); EXPECT_EQ(errorCalled, false); EXPECT_EQ(logCalled, false); -} \ No newline at end of file +} diff --git a/bridge/core/executing_context.cc b/bridge/core/executing_context.cc index 9432aeebe9..c167bd507d 100644 --- a/bridge/core/executing_context.cc +++ b/bridge/core/executing_context.cc @@ -8,6 +8,7 @@ #include "bindings/qjs/converter_impl.h" #include "built_in_string.h" #include "core/dom/document.h" +#include "core/dom/intersection_observer.h" #include "core/dom/mutation_observer.h" #include "core/events/error_event.h" #include "core/events/promise_rejection_event.h" diff --git a/bridge/foundation/ui_command_buffer.cc b/bridge/foundation/ui_command_buffer.cc index 1761f4d219..eb92c330d8 100644 --- a/bridge/foundation/ui_command_buffer.cc +++ b/bridge/foundation/ui_command_buffer.cc @@ -40,6 +40,10 @@ UICommandKind GetKindFromUICommand(UICommand command) { case UICommand::kStartRecordingCommand: case UICommand::kFinishRecordingCommand: return UICommandKind::kOperation; + case UICommand::kAddIntersectionObserver: + case UICommand::kRemoveIntersectionObserver: + case UICommand::kDisconnectIntersectionObserver: + return UICommandKind::kIntersectionObserver; default: return UICommandKind::kUknownCommand; } diff --git a/bridge/foundation/ui_command_buffer.h b/bridge/foundation/ui_command_buffer.h index dcfdf0352f..8244ff5caf 100644 --- a/bridge/foundation/ui_command_buffer.h +++ b/bridge/foundation/ui_command_buffer.h @@ -21,7 +21,7 @@ enum UICommandKind : uint32_t { kAttributeUpdate = 1 << 5, kDisposeBindingObject = 1 << 6, kOperation = 1 << 7, - kUknownCommand = 1 << 8 + kIntersectionObserver = 1 << 8 kUknownCommand = 1 << 9 }; enum class UICommand { @@ -45,6 +45,9 @@ enum class UICommand { kCreateSVGElement, kCreateElementNS, kFinishRecordingCommand, + kAddIntersectionObserver, + kRemoveIntersectionObserver, + kDisconnectIntersectionObserver }; #define MAXIMUM_UI_COMMAND_SIZE 2048 diff --git a/bridge/foundation/ui_command_strategy.cc b/bridge/foundation/ui_command_strategy.cc index 8fddd6ca32..7b93079c5d 100644 --- a/bridge/foundation/ui_command_strategy.cc +++ b/bridge/foundation/ui_command_strategy.cc @@ -84,7 +84,10 @@ void UICommandSyncStrategy::RecordUICommand(UICommand type, case UICommand::kSetAttribute: case UICommand::kRemoveEvent: case UICommand::kAddEvent: - case UICommand::kDisposeBindingObject: { + case UICommand::kDisposeBindingObject: + case UICommand::kAddIntersectionObserver: + case UICommand::kRemoveIntersectionObserver: + case UICommand::kDisconnectIntersectionObserver: { host_->waiting_buffer_->addCommand(type, std::move(args_01), native_binding_object, native_ptr2, request_ui_update); break; @@ -141,4 +144,4 @@ void UICommandSyncStrategy::RecordOperationForPointer(NativeBindingObject* ptr) SyncToReserveIfNecessary(); } -} // namespace webf \ No newline at end of file +} // namespace webf diff --git a/bridge/scripts/code_generator/templates/idl_templates/dictionary.h.tpl b/bridge/scripts/code_generator/templates/idl_templates/dictionary.h.tpl index 15fd3d6a3d..ed193727df 100644 --- a/bridge/scripts/code_generator/templates/idl_templates/dictionary.h.tpl +++ b/bridge/scripts/code_generator/templates/idl_templates/dictionary.h.tpl @@ -7,6 +7,7 @@ namespace webf { class ExecutingContext; class ExceptionState; +class Node; class <%= className %> : public <%= object.parent ? object.parent : 'DictionaryBase' %> { public: diff --git a/bridge/third_party/quickjs/vendor/mimalloc b/bridge/third_party/quickjs/vendor/mimalloc index db3d8485d2..43ce4bd7fd 160000 --- a/bridge/third_party/quickjs/vendor/mimalloc +++ b/bridge/third_party/quickjs/vendor/mimalloc @@ -1 +1 @@ -Subproject commit db3d8485d2f45a6f179d784f602f9eff4f60795c +Subproject commit 43ce4bd7fd34bcc730c1c7471c99995597415488 diff --git a/integration_tests/specs/intersection-observer/intersection-observer.ts b/integration_tests/specs/intersection-observer/intersection-observer.ts new file mode 100644 index 0000000000..47cc0d1d62 --- /dev/null +++ b/integration_tests/specs/intersection-observer/intersection-observer.ts @@ -0,0 +1,42 @@ +describe('IntersectionObserver', () => { + let container: HTMLElement; + let observed: HTMLElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + + const spacer = document.createElement('div'); + spacer.style.height = '100px'; + container.appendChild(spacer); + + observed = document.createElement('div'); + observed.id = 'observed'; + observed.style.width = '200px'; + observed.style.height = '200px'; + observed.style.backgroundColor = 'red'; + container.appendChild(observed); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + it('should trigger callback when element intersects', (done) => { + const intersectionCallback = (entries: IntersectionObserverEntry[]) => { + entries.forEach(entry => { + expect(entry.target).toBe(observed); + done(); + }); + }; + + const intersectionOptions = { + root: null, + rootMargin: "0px", + threshold: [0, 1] + }; + + const observer = new IntersectionObserver(intersectionCallback, intersectionOptions); + observer.observe(observed); + }); +}); diff --git a/webf/lib/src/bridge/binding.dart b/webf/lib/src/bridge/binding.dart index deb63f072a..8be4212ace 100644 --- a/webf/lib/src/bridge/binding.dart +++ b/webf/lib/src/bridge/binding.dart @@ -16,6 +16,7 @@ import 'package:webf/foundation.dart'; import 'package:webf/launcher.dart'; import 'package:webf/src/geometry/dom_point.dart'; import 'package:webf/src/html/canvas/canvas_path_2d.dart'; +import 'package:webf/src/dom/intersection_observer.dart'; // We have some integrated built-in behavior starting with string prefix reuse the callNativeMethod implements. enum BindingMethodCallOperations { @@ -162,6 +163,7 @@ enum CreateBindingObjectType { createDOMMatrix, createPath2D, createDOMPoint, + createIntersectionObserver } abstract class BindingBridge { @@ -192,6 +194,10 @@ abstract class BindingBridge { controller.view.setBindingObject(pointer, domPoint); return; } + case CreateBindingObjectType.createIntersectionObserver: { + IntersectionObserver intersectionObserver = IntersectionObserver(BindingContext(controller.view, contextId, pointer), arguments); + controller.view.setBindingObject(pointer, intersectionObserver); + } } } diff --git a/webf/lib/src/bridge/to_native.dart b/webf/lib/src/bridge/to_native.dart index 609f0490f0..02942abe6a 100644 --- a/webf/lib/src/bridge/to_native.dart +++ b/webf/lib/src/bridge/to_native.dart @@ -688,6 +688,10 @@ enum UICommandType { createSVGElement, createElementNS, finishRecordingCommand, + // IntersectionObserver + addIntersectionObserver, + removeIntersectionObserver, + disconnectIntersectionObserver, } class UICommandItem extends Struct { diff --git a/webf/lib/src/bridge/ui_command.dart b/webf/lib/src/bridge/ui_command.dart index 98e93b6dc3..6708604d72 100644 --- a/webf/lib/src/bridge/ui_command.dart +++ b/webf/lib/src/bridge/ui_command.dart @@ -312,6 +312,39 @@ void execUICommands(WebFViewController view, List commands) { WebFProfiler.instance.finishTrackUICommandStep(); } break; + case UICommandType.addIntersectionObserver: + if (enableWebFProfileTracking) { + WebFProfiler.instance.startTrackUICommandStep('FlushUICommand.addIntersectionObserver'); + } + + view.addIntersectionObserver( + nativePtr.cast(), command.nativePtr2.cast()); + if (enableWebFProfileTracking) { + WebFProfiler.instance.finishTrackUICommandStep(); + } + + break; + case UICommandType.removeIntersectionObserver: + if (enableWebFProfileTracking) { + WebFProfiler.instance.startTrackUICommandStep('FlushUICommand.removeIntersectionObserver'); + } + + view.removeIntersectionObserver( + nativePtr.cast(), command.nativePtr2.cast()); + if (enableWebFProfileTracking) { + WebFProfiler.instance.finishTrackUICommandStep(); + } + break; + case UICommandType.disconnectIntersectionObserver: + if (enableWebFProfileTracking) { + WebFProfiler.instance.startTrackUICommandStep('FlushUICommand.disconnectIntersectionObserver'); + } + + view.disconnectIntersectionObserver(nativePtr.cast()); + if (enableWebFProfileTracking) { + WebFProfiler.instance.finishTrackUICommandStep(); + } + break; default: break; } diff --git a/webf/lib/src/dom/document.dart b/webf/lib/src/dom/document.dart index 5f52b61af9..f777c268cc 100644 --- a/webf/lib/src/dom/document.dart +++ b/webf/lib/src/dom/document.dart @@ -18,6 +18,7 @@ import 'package:webf/rendering.dart'; import 'package:webf/src/css/query_selector.dart' as QuerySelector; import 'package:webf/src/dom/element_registry.dart' as element_registry; import 'package:webf/src/foundation/cookie_jar.dart'; +import 'package:webf/src/dom/intersection_observer.dart'; /// In the document tree, there may contains WidgetElement which connected to a Flutter Elements. /// And these flutter element will be unmounted in the end of this frame and their renderObject will call dispose() too. @@ -99,6 +100,8 @@ class Document extends ContainerNode { final Set _styleDirtyElements = {}; + final Set _intersectionObserverList = {}; + void markElementStyleDirty(Element element) { _styleDirtyElements.add(element.pointer!.address); } @@ -588,4 +591,30 @@ class Document extends ContainerNode { pendingPreloadingScriptCallbacks.clear(); super.dispose(); } + + void addIntersectionObserver(IntersectionObserver observer, Element element) { + observer.observe(element); + _intersectionObserverList.add(observer); + } + + void removeIntersectionObserver(IntersectionObserver observer, Element element) { + observer.unobserve(element); + if (!observer.HasObservations()) { + _intersectionObserverList.remove(observer); + } + } + + void disconnectIntersectionObserver(IntersectionObserver observer) { + observer.disconnect(); + _intersectionObserverList.remove(observer); + } + + void deliverIntersectionObserver() { + if (_intersectionObserverList.isEmpty) { + return; + } + for (var observer in _intersectionObserverList) { + observer.deliver(controller); + } + } } diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index 81b12ff8e3..82a21581ea 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:ui'; +import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -19,6 +20,8 @@ import 'package:webf/src/svg/rendering/container.dart'; import 'package:webf/svg.dart'; import 'package:webf/widget.dart'; import 'package:webf/src/css/query_selector.dart' as QuerySelector; +import 'intersection_observer.dart'; +import 'intersection_observer_entry.dart'; final RegExp classNameSplitRegExp = RegExp(r'\s+'); const String _ONE_SPACE = ' '; @@ -109,6 +112,9 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin // Default to unknown, assign by [createElement], used by inspector. String tagName = UNKNOWN; + final Set _intersectionObserverList = {}; + List _thresholds = [0.0]; + String? _id; String? get id => _id; @@ -406,6 +412,9 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin // Ensure that the event responder is bound. ensureEventResponderBound(); + + // Ensure IntersectionObserver when renderBoxModel change. + ensureAddIntersectionObserver(); } } @@ -1012,6 +1021,7 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin _beforeElement = null; _afterElement?.dispose(); _afterElement = null; + renderBoxModel?.removeIntersectionChangeListener(_handleIntersectionObserver); super.dispose(); } @@ -1992,6 +2002,43 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin } return style; } + + bool _handleIntersectionObserver(IntersectionObserverEntry entry) { + // If there are multiple IntersectionObservers, they cannot be distributed accurately + for (var observer in _intersectionObserverList) { + observer.addEntry(DartIntersectionObserverEntry(entry.isIntersecting, entry.intersectionRatio, this)); + } + + return _intersectionObserverList.isNotEmpty; + } + + bool addIntersectionObserver(IntersectionObserver observer, List thresholds) { + if (_intersectionObserverList.contains(observer)) { + return false; + } + if (renderBoxModel?.attached ?? false) { + renderBoxModel!.addIntersectionChangeListener(_handleIntersectionObserver, thresholds); + renderBoxModel!.markNeedsPaint(); + } + _intersectionObserverList.add(observer); + _thresholds = thresholds; + return true; + } + + void removeIntersectionObserver(IntersectionObserver observer) { + _intersectionObserverList.remove(observer); + + if (_intersectionObserverList.isEmpty) { + renderBoxModel?.removeIntersectionChangeListener(_handleIntersectionObserver); + } + } + + void ensureAddIntersectionObserver() { + if (_intersectionObserverList.isEmpty) { + return; + } + renderBoxModel?.addIntersectionChangeListener(_handleIntersectionObserver, _thresholds); + } } // https://www.w3.org/TR/css-position-3/#def-cb diff --git a/webf/lib/src/dom/event.dart b/webf/lib/src/dom/event.dart index 55d27399d9..a034539fe6 100644 --- a/webf/lib/src/dom/event.dart +++ b/webf/lib/src/dom/event.dart @@ -85,7 +85,7 @@ mixin ElementEventMixin on ElementBase { // Make sure pointer responder bind. renderBox.getEventTarget = getEventTarget; - if (_hasIntersectionObserverEvent()) { + if (hasIntersectionObserverEvent()) { renderBox.addIntersectionChangeListener(handleIntersectionChange); // Mark the compositing state for this render object as dirty // cause it will create new layer. @@ -102,7 +102,7 @@ mixin ElementEventMixin on ElementBase { } } - bool _hasIntersectionObserverEvent() { + bool hasIntersectionObserverEvent() { return hasEventListener(EVENT_APPEAR) || hasEventListener(EVENT_DISAPPEAR) || hasEventListener(EVENT_INTERSECTION_CHANGE); @@ -148,13 +148,15 @@ mixin ElementEventMixin on ElementBase { dispatchEvent(DisappearEvent()); } - void handleIntersectionChange(IntersectionObserverEntry entry) { - dispatchEvent(IntersectionChangeEvent(entry.intersectionRatio)); - if (entry.intersectionRatio > 0) { + bool handleIntersectionChange(IntersectionObserverEntry entry) { + final double intersectionRatio = entry.intersectionRatio; + dispatchEvent(IntersectionChangeEvent(intersectionRatio)); + if (intersectionRatio > 0) { handleAppear(); } else { handleDisappear(); } + return false; } void handleResizeChange(ResizeObserverEntry entry) { diff --git a/webf/lib/src/dom/intersection_observer.dart b/webf/lib/src/dom/intersection_observer.dart new file mode 100644 index 0000000000..f45b430652 --- /dev/null +++ b/webf/lib/src/dom/intersection_observer.dart @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2019-2022 The Kraken authors. All rights reserved. + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ +import 'dart:async'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:webf/foundation.dart'; +import 'package:webf/bridge.dart'; +import 'package:webf/launcher.dart'; +import 'element.dart'; +import 'intersection_observer_entry.dart'; +import 'package:flutter/foundation.dart'; + +class _IntersectionObserverDeliverContext { + Completer completer; + Stopwatch? stopwatch; + + Pointer allocatedNativeArguments; + Pointer rawEntries; + WebFController controller; + EvaluateOpItem? profileOp; + + _IntersectionObserverDeliverContext( + this.completer, + this.stopwatch, + this.allocatedNativeArguments, + this.rawEntries, + this.controller, + this.profileOp, + ); +} + +void _handleDeliverResult(Object handle, Pointer returnValue) { + _IntersectionObserverDeliverContext context = handle as _IntersectionObserverDeliverContext; + + if (enableWebFCommandLog && context.stopwatch != null) { + debugPrint('deliver IntersectionObserverEntry to native side, time: ${context.stopwatch!.elapsedMicroseconds}us'); + } + + // Free the allocated arguments. + malloc.free(context.allocatedNativeArguments); + malloc.free(context.rawEntries); + malloc.free(returnValue); + + if (enableWebFProfileTracking) { + WebFProfiler.instance.finishTrackEvaluate(context.profileOp!); + } + + context.completer.complete(); +} + +class IntersectionObserver extends DynamicBindingObject { + IntersectionObserver(BindingContext? context, List thresholds_) : super(context) { + if (null != thresholds_) { + debugPrint('Dom.IntersectionObserver.Constructor thresholds_:$thresholds_'); + _thresholds = thresholds_.map((e) => e as double).toList(); + } + } + + @override + void initializeMethods(Map methods) {} + + @override + void initializeProperties(Map properties) {} + + void observe(Element element) { + // debugPrint('Dom.IntersectionObserver.observe element:$element'); + if (!element.addIntersectionObserver(this, _thresholds)) { + return; + } + _elementList.add(element); + } + + void unobserve(Element element) { + // debugPrint('Dom.IntersectionObserver.unobserve'); + _elementList.remove(element); + element.removeIntersectionObserver(this); + } + + void disconnect() { + if (_elementList.isEmpty) return; + for (var element in _elementList) { + element!.removeIntersectionObserver(this); + } + _elementList.clear(); + } + + bool HasObservations() { + return _elementList.isNotEmpty; + } + + void addEntry(DartIntersectionObserverEntry entry) { + _entries.add(entry); + } + + List takeRecords() { + List entries = _entries.map((entry) => entry.copy()).toList(); + _entries.clear(); + return entries; + } + + Future deliver(WebFController controller) async { + if (pointer == null) return; + + List entries = takeRecords(); + if (entries.isEmpty) { + return; + } + // debugPrint('Dom.IntersectionObserver.deliver size:${entries.length}'); + Completer completer = Completer(); + + EvaluateOpItem? currentProfileOp; + if (enableWebFProfileTracking) { + currentProfileOp = WebFProfiler.instance.startTrackEvaluate('_dispatchEventToNative'); + } + + BindingObject bindingObject = controller.view.getBindingObject(pointer!); + // Call methods implements at C++ side. + DartInvokeBindingMethodsFromDart? invokeBindingMethodsFromDart = + pointer!.ref.invokeBindingMethodFromDart.asFunction(); + + Stopwatch? stopwatch; + if (enableWebFCommandLog) { + stopwatch = Stopwatch()..start(); + } + + // Allocate an chunk of memory for an list of NativeIntersectionObserverEntry + Pointer nativeEntries = + malloc.allocate(sizeOf() * entries.length); + + // Write the native memory from dart objects. + for (int i = 0; i < entries.length; i++) { + (nativeEntries + i).ref.isIntersecting = entries[i].isIntersecting ? 1 : 0; + (nativeEntries + i).ref.intersectionRatio = entries[i].intersectionRatio; + (nativeEntries + i).ref.element = entries[i].element.pointer!; + } + + List dispatchEntryArguments = [nativeEntries, entries.length]; + Pointer allocatedNativeArguments = makeNativeValueArguments(bindingObject, dispatchEntryArguments); + + _IntersectionObserverDeliverContext context = _IntersectionObserverDeliverContext( + completer, stopwatch, allocatedNativeArguments, nativeEntries, controller, currentProfileOp); + + Pointer> resultCallback = Pointer.fromFunction(_handleDeliverResult); + + Future.microtask(() { + invokeBindingMethodsFromDart(pointer!, currentProfileOp?.hashCode ?? 0, nullptr, dispatchEntryArguments.length, + allocatedNativeArguments, context, resultCallback); + }); + // debugPrint('Dom.IntersectionObserver.deliver this:$pointer end'); + return completer.future; + } + + final List _entries = []; + final List _elementList = []; + List _thresholds = [0.0]; +} diff --git a/webf/lib/src/dom/intersection_observer_entry.dart b/webf/lib/src/dom/intersection_observer_entry.dart new file mode 100644 index 0000000000..1e2cb4b3bf --- /dev/null +++ b/webf/lib/src/dom/intersection_observer_entry.dart @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019-2022 The Kraken authors. All rights reserved. + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +import 'dart:ffi'; +import 'package:webf/bridge.dart'; +import 'element.dart'; + +class DartIntersectionObserverEntry { + //final DOMHighResTimeStamp time; + //final DOMRectReadOnly? rootBounds; + //final DOMRectReadOnly boundingClientRect; + //final DOMRectReadOnly intersectionRect; + final bool isIntersecting; + + //final bool isVisible; + final double intersectionRatio; + final Element element; + + DartIntersectionObserverEntry(this.isIntersecting, this.intersectionRatio, this.element); + + DartIntersectionObserverEntry copy() { + return DartIntersectionObserverEntry(isIntersecting, intersectionRatio, element); + } +} + +class NativeIntersectionObserverEntry extends Struct { + @Int8() + external int isIntersecting; + + @Double() + external double intersectionRatio; + + external Pointer element; +} diff --git a/webf/lib/src/html/img.dart b/webf/lib/src/html/img.dart index e957468cfe..f43a98f07a 100644 --- a/webf/lib/src/html/img.dart +++ b/webf/lib/src/html/img.dart @@ -353,7 +353,7 @@ class ImageElement extends Element { // The getter must be called after image had loaded, otherwise will return 0. int naturalHeight = 0; - void _handleIntersectionChange(IntersectionObserverEntry entry) async { + bool _handleIntersectionChange(IntersectionObserverEntry entry) { // When appear if (entry.isIntersecting) { _updateImageDataLazyCompleter?.complete(); @@ -361,6 +361,7 @@ class ImageElement extends Element { } else { _stopListeningStream(keepStreamAlive: true); } + return false; } // To prevent trigger load event more than once. diff --git a/webf/lib/src/launcher/controller.dart b/webf/lib/src/launcher/controller.dart index bb564ea8a3..06e86e7812 100644 --- a/webf/lib/src/launcher/controller.dart +++ b/webf/lib/src/launcher/controller.dart @@ -26,6 +26,7 @@ import 'package:webf/gesture.dart'; import 'package:webf/rendering.dart'; import 'package:webf/devtools.dart'; import 'package:webf/webf.dart'; +import 'package:webf/src/dom/intersection_observer.dart'; // Error handler when load bundle failed. typedef LoadErrorHandler = void Function(FlutterError error, StackTrace stack); @@ -223,6 +224,7 @@ class WebFViewController implements WidgetsBindingObserver { if (disposed && _isFrameBindingAttached) return; _isFrameBindingAttached = true; flushUICommand(this, window.pointer!); + deliverIntersectionObserver(); SchedulerBinding.instance.addPostFrameCallback((_) => flushPendingCommandsPerFrame()); } @@ -499,6 +501,50 @@ class WebFViewController implements WidgetsBindingObserver { document.createDocumentFragment(BindingContext(document.controller.view, _contextId, nativePtr)); } + void addIntersectionObserver( + Pointer observerPointer, Pointer elementPointer) { + debugPrint('Dom.IntersectionObserver.observe'); + assert(hasBindingObject(observerPointer), 'observer: $observerPointer'); + assert(hasBindingObject(elementPointer), 'element: $elementPointer'); + + IntersectionObserver? observer = getBindingObject(observerPointer); + Element? element = getBindingObject(elementPointer); + if (nullptr == observer || nullptr == element) { + return; + } + + document.addIntersectionObserver(observer!, element!); + } + + void removeIntersectionObserver( + Pointer observerPointer, Pointer elementPointer) { + assert(hasBindingObject(observerPointer), 'observer: $observerPointer'); + assert(hasBindingObject(elementPointer), 'element: $elementPointer'); + + IntersectionObserver? observer = getBindingObject(observerPointer); + Element? element = getBindingObject(elementPointer); + if (nullptr == observer || nullptr == element) { + return; + } + + document.removeIntersectionObserver(observer!, element!); + } + + void disconnectIntersectionObserver(Pointer observerPointer) { + assert(hasBindingObject(observerPointer), 'observer: $observerPointer'); + + IntersectionObserver? observer = getBindingObject(observerPointer); + if (nullptr == observer) { + return; + } + + document.disconnectIntersectionObserver(observer!); + } + + void deliverIntersectionObserver() { + document.deliverIntersectionObserver(); + } + void addEvent(Pointer nativePtr, String eventType, {Pointer? addEventListenerOptions}) { if (!hasBindingObject(nativePtr)) return; diff --git a/webf/lib/src/rendering/intersection_observer.dart b/webf/lib/src/rendering/intersection_observer.dart index 1e7214fd12..3f16c785cd 100644 --- a/webf/lib/src/rendering/intersection_observer.dart +++ b/webf/lib/src/rendering/intersection_observer.dart @@ -20,7 +20,8 @@ Iterable _getLayerChain(Layer start) { return layerChain.reversed; } -typedef IntersectionChangeCallback = void Function(IntersectionObserverEntry info); +// Call the callback function and decide whether to deliver IntersectionObserver based on its return value +typedef IntersectionChangeCallback = bool Function(IntersectionObserverEntry info); mixin RenderIntersectionObserverMixin on RenderBox { static copyTo(RenderIntersectionObserverMixin from, RenderIntersectionObserverMixin to) { @@ -40,6 +41,12 @@ mixin RenderIntersectionObserverMixin on RenderBox { /// A list of event handlers List? _listeners; + List _thresholds = [0.0]; + int _lastThresholdsIndex = 0; + bool _lastIsIntersecting = false; + + bool _clearIntersectionListeners = false; + void disposeIntersectionObserverLayer() { _intersectionObserverLayer.layer = null; } @@ -50,7 +57,7 @@ mixin RenderIntersectionObserverMixin on RenderBox { return _listeners?.isNotEmpty == true; } - void addIntersectionChangeListener(IntersectionChangeCallback callback) { + void addIntersectionChangeListener(IntersectionChangeCallback callback, [List thresholds = const [0.0]]) { // Init things if (_listeners == null) { _listeners = List.empty(growable: true); @@ -60,13 +67,22 @@ mixin RenderIntersectionObserverMixin on RenderBox { // Avoid same listener added twice. if (!_listeners!.contains(callback)) { _listeners!.add(callback); + _thresholds = thresholds; } } void clearIntersectionChangeListeners() { + _clearIntersectionListeners = true; + } + + void _clearIntersectionChangeListeners() { + debugPrint('RenderBox._clearIntersectionChangeListeners this:$this'); _listeners?.clear(); _listeners = null; _onIntersectionChange = null; + _clearIntersectionListeners = false; + _lastThresholdsIndex = 0; + _lastIsIntersecting = false; } void removeIntersectionChangeListener(IntersectionChangeCallback callback) { @@ -83,13 +99,30 @@ mixin RenderIntersectionObserverMixin on RenderBox { markNeedsPaint(); } - void _dispatchChange(IntersectionObserverEntry info) { - // Not use for-in, and not cache length, due to callback call stack may - // clear [_listeners], which case concurrent exception. - for (int i = 0; i < (_listeners == null ? 0 : _listeners!.length); i++) { - IntersectionChangeCallback callback = _listeners![i]; - callback(info); + bool _dispatchChange(IntersectionObserverEntry info) { + bool deliverIntersectionObserver = false; + // Limit frequency + int index = firstThresholdGreaterThan(info.intersectionRatio, _thresholds!); + bool isIntersecting = info.isIntersecting; + if (index != _lastThresholdsIndex || isIntersecting != _lastIsIntersecting) { + _lastThresholdsIndex = index; + _lastIsIntersecting = isIntersecting; + + // Not use for-in, and not cache length, due to callback call stack may + // clear [_listeners], which case concurrent exception. + for (int i = 0; i < (_listeners == null ? 0 : _listeners!.length); i++) { + IntersectionChangeCallback callback = _listeners![i]; + if (callback(info)) { + deliverIntersectionObserver = true; + } + } } + + if (_clearIntersectionListeners) { + _clearIntersectionChangeListeners(); + } + + return deliverIntersectionObserver; } void paintIntersectionObserver(PaintingContext context, Offset offset, PaintingContextCallback callback) { @@ -112,6 +145,14 @@ mixin RenderIntersectionObserverMixin on RenderBox { context.pushLayer(_intersectionObserverLayer.layer!, callback, offset); } + + int firstThresholdGreaterThan(double ratio, List thresholds) { + int result = 0; + while (result < thresholds.length && thresholds[result] <= ratio) { + result++; + } + return result; + } } class IntersectionObserverLayer extends ContainerLayer { @@ -139,6 +180,8 @@ class IntersectionObserverLayer extends ContainerLayer { /// 300ms delay compute layer offset static final Duration _updateInterval = Duration(milliseconds: 300); + static Timer? _timer; + /// Offset to the start of the element, in local coordinates. final Offset _elementOffset; @@ -239,10 +282,12 @@ class IntersectionObserverLayer extends ContainerLayer { final isUpdateScheduled = _updated.isNotEmpty; _updated[id] = this; - if (!isUpdateScheduled) { + if (!isUpdateScheduled && null == _timer) { // We use a normal [Timer] instead of a [RestartableTimer] so that changes // to the update duration will be picked up automatically. - Timer(_updateInterval, _handleUpdateTimer); + _timer = Timer.periodic(_updateInterval, (Timer timer) { + SchedulerBinding.instance.scheduleTask(_processCallbacks, Priority.touch); + }); } } @@ -297,17 +342,17 @@ class IntersectionObserverLayer extends ContainerLayer { /// Invokes the visibility callback if [IntersectionObserverEntry] hasn't meaningfully /// changed since the last time we invoked it. - void _fireCallback(IntersectionObserverEntry info) { + bool _fireCallback(IntersectionObserverEntry info) { final oldInfo = _lastIntersectionInfo; // If isIntersecting is true maybe not visible when element size is 0 final isIntersecting = info.isIntersecting; if (oldInfo == null) { if (!isIntersecting) { - return; + return false; } } else if (info.matchesIntersecting(oldInfo)) { - return; + return false; } if (isIntersecting) { @@ -317,7 +362,7 @@ class IntersectionObserverLayer extends ContainerLayer { _lastIntersectionInfo = null; } // Notify visibility changed event - onIntersectionChange!(info); + return onIntersectionChange!(info); } Rect? _rootBounds; @@ -326,10 +371,16 @@ class IntersectionObserverLayer extends ContainerLayer { /// Executes visibility callbacks for all updated. static void _processCallbacks() { + if (_updated.isEmpty) { + return; + } + + bool deliverIntersectionObserver = false; for (final layer in _updated.values) { - if (layer.onIntersectionChange == null) return; if (!layer.attached) { - layer._fireCallback(IntersectionObserverEntry(size: Size.zero)); + if (layer._fireCallback(IntersectionObserverEntry(size: Size.zero))) { + deliverIntersectionObserver = true; + } continue; } @@ -342,7 +393,14 @@ class IntersectionObserverLayer extends ContainerLayer { final info = IntersectionObserverEntry.fromRects( boundingClientRect: paddingAroundElementBounds, rootBounds: rootBounds); - layer._fireCallback(info); + if (layer._fireCallback(info)) { + deliverIntersectionObserver = true; + } + } + + // for dom IntersectionObserver + if (deliverIntersectionObserver) { + SchedulerBinding.instance.scheduleFrame(); } _updated.clear(); _layerTransformCache.clear();