Skip to content

Commit 46546de

Browse files
authored
Merge pull request #638 from lightpanda-io/DOM-scoll-and-quads
scrollIntoViewIfNeeded, getContentQuads, innerWidth/Heigh
2 parents 6f9dd8d + 48de14a commit 46546de

File tree

3 files changed

+125
-13
lines changed

3 files changed

+125
-13
lines changed

src/browser/dom/element.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ pub const Element = struct {
389389
const s = try cssParse(state.call_arena, selectors, .{});
390390
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
391391
}
392+
393+
pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void {
394+
_ = center_if_needed;
395+
}
392396
};
393397

394398
// Tests
@@ -575,6 +579,12 @@ test "Browser.DOM.Element" {
575579
.{ "el.matches('.notok')", "false" },
576580
}, .{});
577581

582+
try runner.testCases(&.{
583+
.{ "const el3 = document.createElement('div');", "undefined" },
584+
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
585+
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
586+
}, .{});
587+
578588
// before
579589
try runner.testCases(&.{
580590
.{ "const before_container = document.createElement('div');", "undefined" },

src/browser/html/window.zig

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ pub const Window = struct {
120120
return &self.history;
121121
}
122122

123+
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
124+
pub fn get_innerHeight(_: *Window, state: *SessionState) u32 {
125+
// We do not have scrollbars or padding so this is the same as Element.clientHeight
126+
return state.renderer.height();
127+
}
128+
129+
// The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present.
130+
pub fn get_innerWidth(_: *Window, state: *SessionState) u32 {
131+
// We do not have scrollbars or padding so this is the same as Element.clientWidth
132+
return state.renderer.width();
133+
}
134+
123135
pub fn get_name(self: *Window) []const u8 {
124136
return self.target;
125137
}
@@ -281,14 +293,23 @@ test "Browser.HTML.Window" {
281293
\\ }
282294
\\ }
283295
,
284-
"undefined",
296+
null,
285297
},
286-
.{ "let id = requestAnimationFrame(step);", "undefined" },
298+
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
287299
}, .{});
288300

289301
// cancelAnimationFrame should be able to cancel a request with the given id
290302
try runner.testCases(&.{
291-
.{ "let request_id = requestAnimationFrame(timestamp => {});", "undefined" },
303+
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
292304
.{ "cancelAnimationFrame(request_id);", "undefined" },
293305
}, .{});
306+
307+
try runner.testCases(&.{
308+
.{ "innerHeight", "1" },
309+
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
310+
.{ "document.createElement('div').getClientRects()", null },
311+
.{ "document.createElement('div').getClientRects()", null },
312+
.{ "innerHeight", "1" },
313+
.{ "innerWidth", "2" },
314+
}, .{});
294315
}

src/cdp/domains/dom.zig

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const Allocator = std.mem.Allocator;
2021
const Node = @import("../Node.zig");
2122
const css = @import("../../browser/dom/css.zig");
2223
const parser = @import("../../browser/netsurf.zig");
2324
const dom_node = @import("../../browser/dom/node.zig");
25+
const DOMRect = @import("../../browser/dom/element.zig").Element.DOMRect;
2426

2527
pub fn processMessage(cmd: anytype) !void {
2628
const action = std.meta.stringToEnum(enum {
@@ -31,6 +33,8 @@ pub fn processMessage(cmd: anytype) !void {
3133
discardSearchResults,
3234
resolveNode,
3335
describeNode,
36+
scrollIntoViewIfNeeded,
37+
getContentQuads,
3438
}, cmd.input.action) orelse return error.UnknownMethod;
3539

3640
switch (action) {
@@ -41,6 +45,8 @@ pub fn processMessage(cmd: anytype) !void {
4145
.discardSearchResults => return discardSearchResults(cmd),
4246
.resolveNode => return resolveNode(cmd),
4347
.describeNode => return describeNode(cmd),
48+
.scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),
49+
.getContentQuads => return getContentQuads(cmd),
4450
}
4551
}
4652

@@ -233,25 +239,100 @@ fn describeNode(cmd: anytype) !void {
233239
depth: u32 = 1,
234240
pierce: bool = false,
235241
})) orelse return error.InvalidParams;
236-
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
237-
return error.NotYetImplementedParams;
238-
}
239242

243+
if (params.depth != 1 or params.pierce) return error.NotYetImplementedParams;
240244
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
241245

242-
if (params.nodeId != null) {
243-
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
244-
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
246+
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
247+
248+
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
249+
}
250+
251+
// An array of quad vertices, x immediately followed by y for each point, points clock-wise.
252+
// Note Y points downward
253+
// We are assuming the start/endpoint is not repeated.
254+
const Quad = [8]f64;
255+
256+
fn rectToQuad(rect: DOMRect) Quad {
257+
return Quad{
258+
rect.x,
259+
rect.y,
260+
rect.x + rect.width,
261+
rect.y,
262+
rect.x + rect.width,
263+
rect.y + rect.height,
264+
rect.x,
265+
rect.y + rect.height,
266+
};
267+
}
268+
269+
fn scrollIntoViewIfNeeded(cmd: anytype) !void {
270+
const params = (try cmd.params(struct {
271+
nodeId: ?Node.Id = null,
272+
backendNodeId: ?u32 = null,
273+
objectId: ?[]const u8 = null,
274+
rect: ?DOMRect = null,
275+
})) orelse return error.InvalidParams;
276+
// Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null
277+
278+
// We retrieve the node to at least check if it exists and is valid.
279+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
280+
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
281+
282+
const node_type = parser.nodeType(node._node) catch return error.InvalidNode;
283+
switch (node_type) {
284+
.element => {},
285+
.document => {},
286+
.text => {},
287+
else => return error.NodeDoesNotHaveGeometry,
245288
}
246-
if (params.objectId != null) {
289+
290+
return cmd.sendResult(null, .{});
291+
}
292+
293+
fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
294+
const input_node_id = node_id orelse backend_node_id;
295+
if (input_node_id) |input_node_id_| {
296+
return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
297+
}
298+
if (object_id) |object_id_| {
247299
// Retrieve the object from which ever context it is in.
248-
const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?);
249-
const node = try bc.node_registry.register(@ptrCast(parser_node));
250-
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
300+
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_);
301+
return try browser_context.node_registry.register(@ptrCast(parser_node));
251302
}
252303
return error.MissingParams;
253304
}
254305

306+
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
307+
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
308+
fn getContentQuads(cmd: anytype) !void {
309+
const params = (try cmd.params(struct {
310+
nodeId: ?Node.Id = null,
311+
backendNodeId: ?Node.Id = null,
312+
objectId: ?[]const u8 = null,
313+
})) orelse return error.InvalidParams;
314+
315+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
316+
317+
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
318+
319+
// TODO likely if the following CSS properties are set the quads should be empty
320+
// visibility: hidden
321+
// display: none
322+
323+
if (try parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement;
324+
// TODO implement for document or text
325+
// Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example.
326+
// Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""?
327+
// Elements like SVGElement may have multiple quads.
328+
329+
const element = parser.nodeToElement(node._node);
330+
const rect = try bc.session.page.?.state.renderer.getRect(element);
331+
const quad = rectToQuad(rect);
332+
333+
return cmd.sendResult(.{ .quads = &.{quad} }, .{});
334+
}
335+
255336
const testing = @import("../testing.zig");
256337

257338
test "cdp.dom: getSearchResults unknown search id" {

0 commit comments

Comments
 (0)