mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 23:38:57 +00:00
tweak timing of intersection observer and how it handles disconnected nodes
This commit is contained in:
@@ -106,6 +106,7 @@ _mutation_delivery_depth: u32 = 0,
|
||||
|
||||
// List of active IntersectionObservers
|
||||
_intersection_observers: std.ArrayList(*IntersectionObserver) = .{},
|
||||
_intersection_check_scheduled: bool = false,
|
||||
_intersection_delivery_scheduled: bool = false,
|
||||
|
||||
// Lookup for customized built-in elements. Maps element pointer to definition.
|
||||
@@ -250,6 +251,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._mutation_delivery_scheduled = false;
|
||||
self._mutation_delivery_depth = 0;
|
||||
self._intersection_observers = .{};
|
||||
self._intersection_check_scheduled = false;
|
||||
self._intersection_delivery_scheduled = false;
|
||||
self._customized_builtin_definitions = .{};
|
||||
self._customized_builtin_connected_callback_invoked = .{};
|
||||
@@ -781,6 +783,15 @@ pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void {
|
||||
|
||||
pub fn domChanged(self: *Page) void {
|
||||
self.version += 1;
|
||||
|
||||
if (self._intersection_check_scheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._intersection_check_scheduled = true;
|
||||
self.js.queueIntersectionChecks() catch |err| {
|
||||
log.err(.page, "page.schedIntersectChecks", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getElementIdMap(page: *Page, node: *Node) *std.StringHashMapUnmanaged(*Element) {
|
||||
@@ -849,27 +860,31 @@ pub fn checkIntersections(self: *Page) !void {
|
||||
}
|
||||
|
||||
pub fn scheduleMutationDelivery(self: *Page) !void {
|
||||
// Only queue if not already scheduled
|
||||
if (self._mutation_delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._mutation_delivery_scheduled = true;
|
||||
|
||||
// Queue mutation delivery as a microtask
|
||||
try self.js.queueMutationDelivery();
|
||||
}
|
||||
|
||||
pub fn scheduleIntersectionDelivery(self: *Page) !void {
|
||||
// Only queue if not already scheduled
|
||||
if (self._intersection_delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._intersection_delivery_scheduled = true;
|
||||
|
||||
// Queue intersection delivery as a microtask
|
||||
try self.js.queueIntersectionDelivery();
|
||||
}
|
||||
|
||||
pub fn performScheduledIntersectionChecks(self: *Page) void {
|
||||
if (!self._intersection_check_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._intersection_check_scheduled = false;
|
||||
self.checkIntersections() catch |err| {
|
||||
log.err(.page, "page.schedIntersectChecks", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deliverIntersections(self: *Page) void {
|
||||
if (!self._intersection_delivery_scheduled) {
|
||||
return;
|
||||
|
||||
@@ -1973,6 +1973,15 @@ pub fn queueMutationDelivery(self: *Context) !void {
|
||||
}.run, self.page);
|
||||
}
|
||||
|
||||
pub fn queueIntersectionChecks(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.performScheduledIntersectionChecks();
|
||||
}
|
||||
}.run, self.page);
|
||||
}
|
||||
|
||||
pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
|
||||
@@ -29,3 +29,33 @@
|
||||
observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=detached>
|
||||
{
|
||||
// never attached
|
||||
let count = 0;
|
||||
const div = document.createElement('div');
|
||||
new IntersectionObserver((entries) => {
|
||||
count += 1;
|
||||
}).observe(div);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(0, count);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// not connected, but has parent
|
||||
let count = 0;
|
||||
const div1 = document.createElement('div');
|
||||
const div2 = document.createElement('div');
|
||||
new IntersectionObserver((entries) => {
|
||||
count += 1;
|
||||
}).observe(div1);
|
||||
|
||||
div2.appendChild(div1);
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, count);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
const div1 = document.createElement('div');
|
||||
const div2 = document.createElement('div');
|
||||
new IntersectionObserver((entries) => {
|
||||
console.log(entries[0]);
|
||||
count += 1;
|
||||
}).observe(div1);
|
||||
|
||||
@@ -33,7 +32,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reobserve>
|
||||
<!-- <script id=reobserve>
|
||||
{
|
||||
let count = 0;
|
||||
let observer = new IntersectionObserver(entries => {
|
||||
@@ -160,4 +159,4 @@
|
||||
], capture)
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script> -->
|
||||
|
||||
@@ -161,11 +161,13 @@ fn calculateIntersection(
|
||||
// For a headless browser without real layout, we treat all elements as fully visible.
|
||||
// This avoids fingerprinting issues (massive viewports) and matches the behavior
|
||||
// scripts expect when querying element visibility.
|
||||
const is_intersecting = true;
|
||||
const intersection_ratio: f64 = 1.0;
|
||||
// However, elements without a parent cannot intersect (they have no containing block).
|
||||
const has_parent = target.asNode().parentNode() != null;
|
||||
const is_intersecting = has_parent;
|
||||
const intersection_ratio: f64 = if (has_parent) 1.0 else 0.0;
|
||||
|
||||
// Intersection rect is the same as the target rect (fully visible)
|
||||
const intersection_rect = target_rect;
|
||||
// Intersection rect is the same as the target rect if visible, otherwise zero rect
|
||||
const intersection_rect = if (has_parent) target_rect else &zero_rect;
|
||||
|
||||
return .{
|
||||
.is_intersecting = is_intersecting,
|
||||
@@ -199,11 +201,10 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
||||
const is_now_intersecting = data.is_intersecting and self.meetsThreshold(data.intersection_ratio);
|
||||
|
||||
// Create entry if:
|
||||
// 1. First time observing this target (was_intersecting_opt == null)
|
||||
// 1. First time observing this target AND it's intersecting
|
||||
// 2. State changed
|
||||
// 3. Currently intersecting
|
||||
const should_report = was_intersecting_opt == null or
|
||||
was_intersecting_opt.? != is_now_intersecting;
|
||||
const should_report = (was_intersecting_opt == null and is_now_intersecting) or
|
||||
(was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);
|
||||
|
||||
if (should_report) {
|
||||
const entry = try page.arena.create(IntersectionObserverEntry);
|
||||
@@ -218,8 +219,11 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
||||
};
|
||||
|
||||
try self._pending_entries.append(page.arena, entry);
|
||||
try self._previous_states.put(page.arena, target, is_now_intersecting);
|
||||
}
|
||||
|
||||
// Always update the previous state, even if we didn't report
|
||||
// This ensures we can detect state changes on subsequent checks
|
||||
try self._previous_states.put(page.arena, target, is_now_intersecting);
|
||||
}
|
||||
|
||||
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
|
||||
|
||||
Reference in New Issue
Block a user