Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ pub const Element = struct {
const s = try cssParse(state.call_arena, selectors, .{});
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
}

pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void {
_ = center_if_needed;
Copy link
Contributor Author

@sjorsdonkers sjorsdonkers May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could register the element in the renderer..

}
};

// Tests
Expand Down Expand Up @@ -575,6 +579,12 @@ test "Browser.DOM.Element" {
.{ "el.matches('.notok')", "false" },
}, .{});

try runner.testCases(&.{
.{ "const el3 = document.createElement('div');", "undefined" },
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
}, .{});

// before
try runner.testCases(&.{
.{ "const before_container = document.createElement('div');", "undefined" },
Expand Down
27 changes: 24 additions & 3 deletions src/browser/html/window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ pub const Window = struct {
return &self.history;
}

// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
pub fn get_innerHeight(_: *Window, state: *SessionState) u32 {
// We do not have scrollbars or padding so this is the same as Element.clientHeight
return state.renderer.height();
}

// The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present.
pub fn get_innerWidth(_: *Window, state: *SessionState) u32 {
// We do not have scrollbars or padding so this is the same as Element.clientWidth
return state.renderer.width();
}

pub fn get_name(self: *Window) []const u8 {
return self.target;
}
Expand Down Expand Up @@ -281,14 +293,23 @@ test "Browser.HTML.Window" {
\\ }
\\ }
,
"undefined",
null,
},
.{ "let id = requestAnimationFrame(step);", "undefined" },
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
}, .{});

// cancelAnimationFrame should be able to cancel a request with the given id
try runner.testCases(&.{
.{ "let request_id = requestAnimationFrame(timestamp => {});", "undefined" },
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
.{ "cancelAnimationFrame(request_id);", "undefined" },
}, .{});

try runner.testCases(&.{
.{ "innerHeight", "1" },
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
.{ "document.createElement('div').getClientRects()", null },
.{ "document.createElement('div').getClientRects()", null },
.{ "innerHeight", "1" },
.{ "innerWidth", "2" },
}, .{});
}
101 changes: 91 additions & 10 deletions src/cdp/domains/dom.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const Allocator = std.mem.Allocator;
const Node = @import("../Node.zig");
const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig");
const DOMRect = @import("../../browser/dom/element.zig").Element.DOMRect;

pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
Expand All @@ -31,6 +33,8 @@ pub fn processMessage(cmd: anytype) !void {
discardSearchResults,
resolveNode,
describeNode,
scrollIntoViewIfNeeded,
getContentQuads,
}, cmd.input.action) orelse return error.UnknownMethod;

switch (action) {
Expand All @@ -41,6 +45,8 @@ pub fn processMessage(cmd: anytype) !void {
.discardSearchResults => return discardSearchResults(cmd),
.resolveNode => return resolveNode(cmd),
.describeNode => return describeNode(cmd),
.scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),
.getContentQuads => return getContentQuads(cmd),
}
}

Expand Down Expand Up @@ -233,25 +239,100 @@ fn describeNode(cmd: anytype) !void {
depth: u32 = 1,
pierce: bool = false,
})) orelse return error.InvalidParams;
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
return error.NotYetImplementedParams;
}

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

if (params.nodeId != null) {
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);

return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
}

// An array of quad vertices, x immediately followed by y for each point, points clock-wise.
// Note Y points downward
// We are assuming the start/endpoint is not repeated.
const Quad = [8]f64;

fn rectToQuad(rect: DOMRect) Quad {
return Quad{
rect.x,
rect.y,
rect.x + rect.width,
rect.y,
rect.x + rect.width,
rect.y + rect.height,
rect.x,
rect.y + rect.height,
};
}

fn scrollIntoViewIfNeeded(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
objectId: ?[]const u8 = null,
rect: ?DOMRect = null,
})) orelse return error.InvalidParams;
// Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null

// We retrieve the node to at least check if it exists and is valid.
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);

const node_type = parser.nodeType(node._node) catch return error.InvalidNode;
switch (node_type) {
.element => {},
.document => {},
.text => {},
else => return error.NodeDoesNotHaveGeometry,
}
if (params.objectId != null) {

return cmd.sendResult(null, .{});
}

fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
const input_node_id = node_id orelse backend_node_id;
if (input_node_id) |input_node_id_| {
return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a getById function on registry be useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce: .lookup_ _. I personally am not a fan of convenience functions.

}
if (object_id) |object_id_| {
// Retrieve the object from which ever context it is in.
const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?);
const node = try bc.node_registry.register(@ptrCast(parser_node));
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know what getNodePtr is doing. It's ok that its memory (from the arena) will be gone after this function returns?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the object_group may be returned on the allocator, which is not used. (Arena is also used for temporary error strings, but not returned (returned as error enum))

return try browser_context.node_registry.register(@ptrCast(parser_node));
}
return error.MissingParams;
}

// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
fn getContentQuads(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
})) orelse return error.InvalidParams;

const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;

const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);

// TODO likely if the following CSS properties are set the quads should be empty
// visibility: hidden
// display: none

if (try parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement;
// TODO implement for document or text
// Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example.
// Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""?
// Elements like SVGElement may have multiple quads.

const element = parser.nodeToElement(node._node);
const rect = try bc.session.page.?.state.renderer.getRect(element);
const quad = rectToQuad(rect);

return cmd.sendResult(.{ .quads = &.{quad} }, .{});
}

const testing = @import("../testing.zig");

test "cdp.dom: getSearchResults unknown search id" {
Expand Down