Skip to content

Commit 537a262

Browse files
committed
Native IntersectionObserver
1 parent 7741de7 commit 537a262

File tree

4 files changed

+156
-1
lines changed

4 files changed

+156
-1
lines changed

src/browser/browser.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,7 @@ const FlatRenderer = struct {
834834
}
835835

836836
pub fn width(self: *const FlatRenderer) u32 {
837-
return @intCast(self.elements.items.len);
837+
return @intCast(self.elements.items.len + 1); // +1 since x starts at 1 (use len after append)
838838
}
839839

840840
pub fn height(_: *const FlatRenderer) u32 {

src/browser/dom/dom.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DOMTokenList = @import("token_list.zig");
2424
const NodeList = @import("nodelist.zig");
2525
const Node = @import("node.zig");
2626
const MutationObserver = @import("mutation_observer.zig");
27+
const IntersectionObserver = @import("intersection_observer.zig");
2728

2829
pub const Interfaces = .{
2930
DOMException,
@@ -35,4 +36,5 @@ pub const Interfaces = .{
3536
Node.Node,
3637
Node.Interfaces,
3738
MutationObserver.Interfaces,
39+
IntersectionObserver.Interfaces,
3840
};

src/browser/dom/element.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,14 @@ pub const Element = struct {
348348
return state.renderer.getRect(self);
349349
}
350350

351+
// returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
352+
// We do not render so just always return the element's rect.
353+
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![]DOMRect {
354+
var heap = try state.arena.create(DOMRect);
355+
heap.* = try state.renderer.getRect(self);
356+
return heap[0..1];
357+
}
358+
351359
pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
352360
return state.renderer.width();
353361
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const Allocator = std.mem.Allocator;
21+
22+
const parser = @import("../netsurf.zig");
23+
const SessionState = @import("../env.zig").SessionState;
24+
25+
const Env = @import("../env.zig").Env;
26+
const Element = @import("element.zig").Element;
27+
const Document = @import("document.zig").Document;
28+
29+
pub const Interfaces = .{
30+
IntersectionObserver,
31+
IntersectionObserverEntry,
32+
};
33+
34+
const log = std.log.scoped(.events);
35+
36+
// This is supposed to listen to change between the root and observation targets.
37+
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
38+
// As such, there are no changes to intersections between the root and any target.
39+
// Instead we keep a list of all entries that are being observed.
40+
// The callback is called with all entries everytime a new entry is added(observed).
41+
// Potentially we should also call the callback at a regular interval.
42+
// The returned Entries are phony, they always indicate full intersection.
43+
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
44+
pub const IntersectionObserver = struct {
45+
callback: Env.Callback,
46+
options: IntersectionObserverOptions,
47+
state: *SessionState,
48+
49+
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
50+
51+
// new IntersectionObserver(callback)
52+
// new IntersectionObserver(callback, options) [not supported yet]
53+
pub fn constructor(callback: Env.Callback, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver {
54+
if (options_ != null) return error.IntersectionObserverOptionsNotYetSupported;
55+
const options = IntersectionObserverOptions{
56+
.root = parser.documentToNode(parser.documentHTMLToDocument(state.document.?)),
57+
.rootMargin = "0px 0px 0px 0px",
58+
.threshold = &[_]f32{0.0},
59+
};
60+
61+
return .{
62+
.callback = callback,
63+
.options = options,
64+
.state = state,
65+
.observed_entries = .{},
66+
};
67+
}
68+
69+
pub fn _disconnect(self: *IntersectionObserver) !void {
70+
self.observed_entries = .{}; // We don't free as it is on an arena
71+
}
72+
73+
pub fn _observe(self: *IntersectionObserver, targetElement: *parser.Element) !void {
74+
try self.observed_entries.append(self.state.arena, .{
75+
.state = self.state,
76+
.target = targetElement,
77+
.options = &self.options,
78+
});
79+
80+
var result: Env.Callback.Result = undefined;
81+
self.callback.tryCall(.{self.observed_entries.items}, &result) catch {
82+
log.err("intersection observer callback error: {s}", .{result.exception});
83+
log.debug("stack:\n{s}", .{result.stack orelse "???"});
84+
};
85+
}
86+
87+
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
88+
for (self.observed_entries.items, 0..) |*observer, index| {
89+
if (observer.target == target) {
90+
_ = self.observed_entries.swapRemove(index);
91+
break;
92+
}
93+
}
94+
}
95+
96+
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
97+
return self.observed_entries.items;
98+
}
99+
};
100+
101+
const IntersectionObserverOptions = struct {
102+
root: ?*parser.Node, // Element or Document
103+
rootMargin: ?[]const u8,
104+
threshold: ?[]const f32,
105+
};
106+
107+
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
108+
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
109+
pub const IntersectionObserverEntry = struct {
110+
state: *SessionState,
111+
target: *parser.Element,
112+
options: *IntersectionObserverOptions,
113+
114+
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
115+
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
116+
return self.state.renderer.getRect(self.target); // Does this ever change?
117+
}
118+
119+
// Returns the ratio of the intersectionRect to the boundingClientRect.
120+
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
121+
return 1.0;
122+
}
123+
124+
// Returns a DOMRectReadOnly representing the target's visible area.
125+
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
126+
return self.state.renderer.getRect(self.target);
127+
}
128+
129+
// A Boolean value which is true if the target element intersects with the intersection observer's root. If this is true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, then you know the transition is from intersecting to not-intersecting.
130+
pub fn isIntersecting(_: *const IntersectionObserverEntry) bool {
131+
return true;
132+
}
133+
134+
// Returns a DOMRectReadOnly for the intersection observer's root.
135+
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
136+
// TODO self.options.root can be an Element or a Document when Options are supported
137+
const element = (try parser.documentGetDocumentElement(parser.documentHTMLToDocument(self.state.document.?))).?;
138+
return try self.state.renderer.getRect(element);
139+
}
140+
141+
// The Element whose intersection with the root changed.
142+
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
143+
return self.target;
144+
}
145+
};

0 commit comments

Comments
 (0)