diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index f48451a8..187fd50f 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -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; + } }; // Tests @@ -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" }, diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index a594da03..73eab497 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -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; } @@ -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" }, + }, .{}); } diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 6613e373..49a4863c 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -17,10 +17,12 @@ // along with this program. If not, see . 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 { @@ -31,6 +33,8 @@ pub fn processMessage(cmd: anytype) !void { discardSearchResults, resolveNode, describeNode, + scrollIntoViewIfNeeded, + getContentQuads, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -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), } } @@ -233,23 +239,98 @@ 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; + + 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, } + 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; + } + if (object_id) |object_id_| { + // Retrieve the object from which ever context it is in. + const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); + 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; - 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, .{}) }, .{}); - } - if (params.objectId != null) { - // 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, .{}) }, .{}); - } - return error.MissingParams; + 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");