diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig
index f4bbdd44..1f91885b 100644
--- a/src/browser/EventManager.zig
+++ b/src/browser/EventManager.zig
@@ -191,6 +191,19 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
const ShadowRoot = @import("webapi/ShadowRoot.zig");
+ // Defer runs even on early return - ensures event phase is reset
+ // and default actions execute (unless prevented)
+ defer {
+ event._event_phase = .none;
+
+ // Execute default action if not prevented
+ if (!event._prevent_default and event._type_string.eqlSlice("click")) {
+ self.page.handleClick(target) catch |err| {
+ log.warn(.event, "page.click", .{ .err = err });
+ };
+ }
+ }
+
var path_len: usize = 0;
var path_buffer: [128]*EventTarget = undefined;
@@ -236,7 +249,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
try self.dispatchPhase(list, current_target, event, was_handled, true);
if (event._stop_propagation) {
- event._event_phase = .none;
return;
}
}
@@ -248,7 +260,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
try self.dispatchPhase(list, target_et, event, was_handled, null);
if (event._stop_propagation) {
- event._event_phase = .none;
return;
}
}
@@ -266,8 +277,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
}
}
}
-
- event._event_phase = .none;
}
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 69713ff9..5729bd00 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -59,6 +59,7 @@ const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
+const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -2270,19 +2271,6 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
}
}
-fn asUint(comptime string: anytype) std.meta.Int(
- .unsigned,
- @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
-) {
- const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
- const expectedType = *const [byteLength:0]u8;
- if (@TypeOf(string) != expectedType) {
- @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
- }
-
- return @bitCast(@as(*const [byteLength]u8, string).*);
-}
-
const ParseState = union(enum) {
pre,
complete,
@@ -2413,6 +2401,112 @@ const QueuedNavigation = struct {
priority: NavigationPriority,
};
+pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
+ const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
+ if (comptime IS_DEBUG) {
+ log.debug(.page, "page mouse click", .{
+ .url = self.url,
+ .node = target,
+ .x = x,
+ .y = y,
+ });
+ }
+ const event = try @import("webapi/event/MouseEvent.zig").init("click", .{
+ .bubbles = true,
+ .cancelable = true,
+ .clientX = x,
+ .clientY = y,
+ }, self);
+ try self._event_manager.dispatch(target.asEventTarget(), event.asEvent());
+}
+
+// callback when the "click" event reaches the pages.
+pub fn handleClick(self: *Page, target: *Node) !void {
+ // TODO: Also support elements when implement
+ const element = target.is(Element) orelse return;
+ const anchor = element.is(Element.Html.Anchor) orelse return;
+
+ const href = element.getAttributeSafe("href") orelse return;
+ if (href.len == 0) {
+ return;
+ }
+
+ if (std.mem.startsWith(u8, href, "#")) {
+ // Hash-only links (#foo) should be handled as same-document navigation
+ return;
+ }
+
+ if (std.mem.startsWith(u8, href, "javascript:")) {
+ return;
+ }
+
+ // Check target attribute - don't navigate if opening in new window/tab
+ const target_val = anchor.getTarget();
+ if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
+ log.warn(.browser, "not implemented", .{
+ .feature = "anchor with target attribute click",
+ });
+ return;
+ }
+
+ if (try element.hasAttribute("download", self)) {
+ log.warn(.browser, "not implemented", .{
+ .feature = "anchor with download attribute click",
+ });
+ return;
+ }
+
+ try self.scheduleNavigation(href, .{
+ .reason = .script,
+ .kind = .{ .push = null },
+ }, .anchor);
+}
+
+pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
+ const element = self.window._document._active_element orelse return;
+ try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
+}
+
+pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEvent) !void {
+ const key = keyboard_event.getKey();
+
+ if (key == .Dead) {
+ return;
+ }
+
+ if (target.is(Element.Html.Input)) |input| {
+ if (key == .Enter) {
+ if (input.getForm(self)) |form| {
+ // TODO: Implement form submission
+ _ = form;
+ }
+ return;
+ }
+
+ // Don't handle text input for radio/checkbox
+ const input_type = input._input_type;
+ if (input_type == .radio or input_type == .checkbox) {
+ return;
+ }
+
+ // Handle printable characters
+ if (key.isPrintable()) {
+ const current_value = input.getValue();
+ const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, key.asString() });
+ try input.setValue(new_value, self);
+ }
+ return;
+ }
+
+ if (target.is(Element.Html.TextArea)) |textarea| {
+ const append =
+ if (key == .Enter) "\n" else if (key.isPrintable()) key.asString() else return;
+ const current_value = textarea.getValue();
+ const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, append });
+ return textarea.setValue(new_value, self);
+ }
+}
+
const RequestCookieOpts = struct {
is_http: bool = true,
is_navigation: bool = false,
@@ -2426,6 +2520,19 @@ pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.Req
};
}
+fn asUint(comptime string: anytype) std.meta.Int(
+ .unsigned,
+ @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
+) {
+ const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
+ const expectedType = *const [byteLength:0]u8;
+ if (@TypeOf(string) != expectedType) {
+ @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
+ }
+
+ return @bitCast(@as(*const [byteLength]u8, string).*);
+}
+
const testing = @import("../testing.zig");
test "WebApi: Page" {
try testing.htmlRunner("page", .{});
diff --git a/src/browser/tests/document/element_from_point.html b/src/browser/tests/document/element_from_point.html
index d3ea7da9..0ee07deb 100644
--- a/src/browser/tests/document/element_from_point.html
+++ b/src/browser/tests/document/element_from_point.html
@@ -46,11 +46,13 @@
element.tagName === 'BODY' || element.tagName === 'HTML');
}
-
-
+ -->
-
+ -->
diff --git a/src/browser/tests/element/bounding_rect.html b/src/browser/tests/element/bounding_rect.html
new file mode 100644
index 00000000..86888dd9
--- /dev/null
+++ b/src/browser/tests/element/bounding_rect.html
@@ -0,0 +1,38 @@
+
+
+ Lightpanda Browser demo
+
+
+
+ Lightpanda Browser Demo
+
+
+
+
+ Demo of an e-commerce product offer page with data loaded from XHR request.
+
+ All images and texts have been generated with AI.
+ Template by Sunil Pradhan
+
+
+
+
+
+ Pages of Amiibo characters generated from
+ Amiibo API.
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index c62437c1..c9a382f7 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -922,7 +922,8 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
const y = calculateDocumentPosition(self.asNode());
const dims = try self.getElementDimensions(page);
- const x: f64 = 0.0;
+ // Use sibling position for x coordinate to ensure siblings have different x values
+ const x = calculateSiblingPosition(self.asNode());
const top = y;
const left = x;
const right = x + dims.width;
@@ -948,51 +949,87 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
return ptr[0..1];
}
-// Calculates a pseudo-position in the document using linear depth scaling.
+// Calculates document position by counting all nodes that appear before this one
+// in tree order, but only traversing the "left side" of the tree.
//
-// This approach uses a fixed pixel offset per depth level (100px) plus sibling
-// position within that level. This keeps positions reasonable even for very deep
-// DOM trees (e.g., Amazon product pages can be 36+ levels deep).
+// This walks up from the target node to the root, and at each level counts:
+// 1. All previous siblings and their descendants
+// 2. The parent itself
//
// Example:
-// → position 0 (depth 0)
-// → position 100 (depth 1, 0 siblings)
-// → position 200 (depth 2, 0 siblings)
-// → position 201 (depth 2, 1 sibling)
-//
-// → position 101 (depth 1, 1 sibling)
-//
→ position 200 (depth 2, 0 siblings)
-//
+// → y=0
+// Text
→ y=1 (body=1)
+// → y=2 (body=1 + h1=1)
+// Link1 → y=3 (body=1 + h1=1 + h2=1)
+//
+// Text
→ y=5 (body=1 + h1=1 + h2=2)
+// → y=6 (body=1 + h1=1 + h2=2 + p=1)
+// Link2 → y=7 (body=1 + h1=1 + h2=2 + p=1 + h2=1)
+//
//
//
// Trade-offs:
-// - O(depth) complexity, very fast
-// - Linear scaling: 36 levels ≈ 3,600px, 100 levels ≈ 10,000px
-// - Rough document order preserved (depth dominates, siblings differentiate)
-// - Fits comfortably in realistic document heights
+// - O(depth × siblings × subtree_height) - only left-side traversal
+// - Linear scaling: 5px per node
+// - Perfect document order, guaranteed unique positions
+// - Compact coordinates (1000 nodes ≈ 5,000px)
fn calculateDocumentPosition(node: *Node) f64 {
- var depth: f64 = 0.0;
- var sibling_offset: f64 = 0.0;
+ var position: f64 = 0.0;
var current = node;
- // Count siblings at the immediate level
- if (current.parentNode()) |parent| {
+ // Walk up to root, counting preceding nodes
+ while (current.parentNode()) |parent| {
+ // Count all previous siblings and their descendants
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
- sibling_offset += 1.0;
+ position += countSubtreeNodes(s);
sibling = s.nextSibling();
}
- }
- // Count depth from root
- while (current.parentNode()) |parent| {
- depth += 1.0;
+ // Count the parent itself
+ position += 1.0;
current = parent;
}
- // Each depth level = 100px, siblings add within that level
- return (depth * 100.0) + sibling_offset;
+ return position * 5.0; // 5px per node
+}
+
+// Counts total nodes in a subtree (node + all descendants)
+fn countSubtreeNodes(node: *Node) f64 {
+ var count: f64 = 1.0; // Count this node
+
+ var child = node.firstChild();
+ while (child) |c| {
+ count += countSubtreeNodes(c);
+ child = c.nextSibling();
+ }
+
+ return count;
+}
+
+// Calculates horizontal position using the same approach as y,
+// just scaled differently for visual distinction
+fn calculateSiblingPosition(node: *Node) f64 {
+ var position: f64 = 0.0;
+ var current = node;
+
+ // Walk up to root, counting preceding nodes (same as y)
+ while (current.parentNode()) |parent| {
+ // Count all previous siblings and their descendants
+ var sibling = parent.firstChild();
+ while (sibling) |s| {
+ if (s == current) break;
+ position += countSubtreeNodes(s);
+ sibling = s.nextSibling();
+ }
+
+ // Count the parent itself
+ position += 1.0;
+ current = parent;
+ }
+
+ return position * 5.0; // 5px per node
}
const GetElementsByTagNameResult = union(enum) {
diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig
index 199d1214..6a92295a 100644
--- a/src/browser/webapi/css/CSSStyleProperties.zig
+++ b/src/browser/webapi/css/CSSStyleProperties.zig
@@ -37,6 +37,51 @@ pub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {
return self._proto;
}
+pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
+ if (method_names.has(name)) {
+ return error.NotHandled;
+ }
+
+ const dash_case = camelCaseToDashCase(name, &page.buf);
+
+ // Only apply vendor prefix filtering for camelCase access (no dashes in input)
+ // Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value
+ const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;
+ if (is_camelcase_access and std.mem.startsWith(u8, dash_case, "-")) {
+ // We only support -webkit-, other vendor prefixes return undefined for camelCase access
+ const is_webkit = std.mem.startsWith(u8, dash_case, "-webkit-");
+ const is_moz = std.mem.startsWith(u8, dash_case, "-moz-");
+ const is_ms = std.mem.startsWith(u8, dash_case, "-ms-");
+ const is_o = std.mem.startsWith(u8, dash_case, "-o-");
+
+ if ((is_moz or is_ms or is_o) and !is_webkit) {
+ return error.NotHandled;
+ }
+ }
+
+ const value = self._proto.getPropertyValue(dash_case, page);
+
+ // Property accessors have special handling for empty values:
+ // - Known CSS properties return '' when not set
+ // - Vendor-prefixed properties return undefined when not set
+ // - Unknown properties return undefined
+ if (value.len == 0) {
+ // Vendor-prefixed properties always return undefined when not set
+ if (std.mem.startsWith(u8, dash_case, "-")) {
+ return error.NotHandled;
+ }
+
+ // Known CSS properties return '', unknown properties return undefined
+ if (!isKnownCSSProperty(dash_case)) {
+ return error.NotHandled;
+ }
+
+ return "";
+ }
+
+ return value;
+}
+
fn isKnownCSSProperty(dash_case: []const u8) bool {
// List of common/known CSS properties
// In a full implementation, this would include all standard CSS properties
@@ -131,6 +176,16 @@ fn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 {
return buf[0..write_pos];
}
+const method_names = std.StaticStringMap(void).initComptime(.{
+ .{ "getPropertyValue", {} },
+ .{ "setProperty", {} },
+ .{ "removeProperty", {} },
+ .{ "getPropertyPriority", {} },
+ .{ "item", {} },
+ .{ "cssText", {} },
+ .{ "length", {} },
+});
+
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSStyleProperties);
@@ -140,60 +195,5 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
- pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, null, null, .{});
-
- const method_names = std.StaticStringMap(void).initComptime(.{
- .{ "getPropertyValue", {} },
- .{ "setProperty", {} },
- .{ "removeProperty", {} },
- .{ "getPropertyPriority", {} },
- .{ "item", {} },
- .{ "cssText", {} },
- .{ "length", {} },
- });
-
- fn _getPropertyIndexed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
- if (method_names.has(name)) {
- return error.NotHandled;
- }
-
- const dash_case = camelCaseToDashCase(name, &page.buf);
-
- // Only apply vendor prefix filtering for camelCase access (no dashes in input)
- // Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value
- const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;
- if (is_camelcase_access and std.mem.startsWith(u8, dash_case, "-")) {
- // We only support -webkit-, other vendor prefixes return undefined for camelCase access
- const is_webkit = std.mem.startsWith(u8, dash_case, "-webkit-");
- const is_moz = std.mem.startsWith(u8, dash_case, "-moz-");
- const is_ms = std.mem.startsWith(u8, dash_case, "-ms-");
- const is_o = std.mem.startsWith(u8, dash_case, "-o-");
-
- if ((is_moz or is_ms or is_o) and !is_webkit) {
- return error.NotHandled;
- }
- }
-
- const value = self._proto.getPropertyValue(dash_case, page);
-
- // Property accessors have special handling for empty values:
- // - Known CSS properties return '' when not set
- // - Vendor-prefixed properties return undefined when not set
- // - Unknown properties return undefined
- if (value.len == 0) {
- // Vendor-prefixed properties always return undefined when not set
- if (std.mem.startsWith(u8, dash_case, "-")) {
- return error.NotHandled;
- }
-
- // Known CSS properties return '', unknown properties return undefined
- if (!isKnownCSSProperty(dash_case)) {
- return error.NotHandled;
- }
-
- return "";
- }
-
- return value;
- }
+ pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, null, null, .{});
};
diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig
index 288a5ccb..80ae94b8 100644
--- a/src/browser/webapi/event/KeyboardEvent.zig
+++ b/src/browser/webapi/event/KeyboardEvent.zig
@@ -28,6 +28,7 @@ const KeyboardEvent = @This();
_proto: *UIEvent,
_key: Key,
+_code: []const u8,
_ctrl_key: bool,
_shift_key: bool,
_alt_key: bool,
@@ -41,6 +42,9 @@ pub const Key = union(enum) {
// Special Key Values
Dead,
Undefined,
+ Unidentified,
+
+ // Modifier Keys
Alt,
AltGraph,
CapsLock,
@@ -55,6 +59,68 @@ pub const Key = union(enum) {
Super,
Symbol,
SymbolLock,
+
+ // Whitespace Keys
+ Enter,
+ Tab,
+
+ // Navigation Keys
+ ArrowDown,
+ ArrowLeft,
+ ArrowRight,
+ ArrowUp,
+ End,
+ Home,
+ PageDown,
+ PageUp,
+
+ // Editing Keys
+ Backspace,
+ Clear,
+ Copy,
+ CrSel,
+ Cut,
+ Delete,
+ EraseEof,
+ ExSel,
+ Insert,
+ Paste,
+ Redo,
+ Undo,
+
+ // UI Keys
+ Accept,
+ Again,
+ Attn,
+ Cancel,
+ ContextMenu,
+ Escape,
+ Execute,
+ Find,
+ Finish,
+ Help,
+ Pause,
+ Play,
+ Props,
+ Select,
+ ZoomIn,
+ ZoomOut,
+
+ // Function Keys
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+
+ // Printable keys (single character, space, etc.)
standard: []const u8,
pub fn fromString(allocator: std.mem.Allocator, str: []const u8) !Key {
@@ -70,6 +136,26 @@ pub const Key = union(enum) {
const duped = try allocator.dupe(u8, str);
return .{ .standard = duped };
}
+
+ /// Returns true if this key represents a printable character that should be
+ /// inserted into text input elements. This includes alphanumeric characters,
+ /// punctuation, symbols, and space.
+ pub fn isPrintable(self: Key) bool {
+ return switch (self) {
+ .standard => |s| s.len > 0,
+ else => false,
+ };
+ }
+
+ /// Returns the string representation that should be inserted into text input.
+ /// For most keys this is just the key itself, but some keys like Enter need
+ /// special handling (e.g., newline for textarea, form submission for input).
+ pub fn asString(self: Key) []const u8 {
+ return switch (self) {
+ .standard => |s| s,
+ else => |k| @tagName(k),
+ };
+ }
};
pub const Location = enum(i32) {
@@ -81,7 +167,7 @@ pub const Location = enum(i32) {
pub const KeyboardEventOptions = struct {
key: []const u8 = "",
- // TODO: code but it is not baseline.
+ code: ?[]const u8 = null,
location: i32 = 0,
repeat: bool = false,
isComposing: bool = false,
@@ -105,6 +191,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
._proto = undefined,
._key = try Key.fromString(page.arena, opts.key),
._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError,
+ ._code = if (opts.code) |c| try page.dupeString(c) else "",
._repeat = opts.repeat,
._is_composing = opts.isComposing,
._ctrl_key = opts.ctrlKey,
@@ -134,11 +221,12 @@ pub fn getIsComposing(self: *const KeyboardEvent) bool {
return self._is_composing;
}
-pub fn getKey(self: *const KeyboardEvent) []const u8 {
- return switch (self._key) {
- .standard => |key| key,
- else => |x| @tagName(x),
- };
+pub fn getKey(self: *const KeyboardEvent) Key {
+ return self._key;
+}
+
+pub fn getCode(self: *const KeyboardEvent) []const u8 {
+ return self._code;
}
pub fn getLocation(self: *const KeyboardEvent) i32 {
@@ -182,7 +270,12 @@ pub const JsApi = struct {
pub const altKey = bridge.accessor(KeyboardEvent.getAltKey, null, .{});
pub const ctrlKey = bridge.accessor(KeyboardEvent.getCtrlKey, null, .{});
pub const isComposing = bridge.accessor(KeyboardEvent.getIsComposing, null, .{});
- pub const key = bridge.accessor(KeyboardEvent.getKey, null, .{});
+ pub const key = bridge.accessor(struct {
+ fn keyAsString(self: *const KeyboardEvent) []const u8 {
+ return self._key.asString();
+ }
+ }.keyAsString, null, .{});
+ pub const code = bridge.accessor(KeyboardEvent.getCode, null, .{});
pub const location = bridge.accessor(KeyboardEvent.getLocation, null, .{});
pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{});
pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{});
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index 15f7a849..78aa87f3 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -201,8 +201,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
},
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
- // @ZIGDOM
- // asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
+ asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
else => {},
},
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig
index feac20b7..5d0c1147 100644
--- a/src/cdp/domains/dom.zig
+++ b/src/cdp/domains/dom.zig
@@ -689,7 +689,7 @@ test "cdp.dom: getBoxModel" {
.params = .{ .nodeId = 6 },
});
try ctx.expectSentResult(.{ .model = BoxModel{
- .content = Quad{ 0.0, 200.0, 5.0, 200.0, 5.0, 205.0, 0.0, 205.0 },
+ .content = Quad{ 10.0, 10.0, 15.0, 10.0, 15.0, 15.0, 10.0, 15.0 },
.padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
.border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
.margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig
index b4f2990a..388d986b 100644
--- a/src/cdp/domains/input.zig
+++ b/src/cdp/domains/input.zig
@@ -36,7 +36,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
type: Type,
key: []const u8 = "",
- code: []const u8 = "",
+ code: ?[]const u8 = null,
modifiers: u4 = 0,
// Many optional parameters are not implemented yet, see documentation url.
@@ -59,28 +59,25 @@ fn dispatchKeyEvent(cmd: anytype) !void {
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
- const keyboard_event = Page.KeyboardEvent{
+ const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig");
+ const keyboard_event = try KeyboardEvent.init("keydown", .{
.key = params.key,
.code = params.code,
- .type = switch (params.type) {
- .keyDown => .keydown,
- else => unreachable,
- },
- .alt = params.modifiers & 1 == 1,
- .ctrl = params.modifiers & 2 == 2,
- .meta = params.modifiers & 4 == 4,
- .shift = params.modifiers & 8 == 8,
- };
- try page.keyboardEvent(keyboard_event);
+ .altKey = params.modifiers & 1 == 1,
+ .ctrlKey = params.modifiers & 2 == 2,
+ .metaKey = params.modifiers & 4 == 4,
+ .shiftKey = params.modifiers & 8 == 8,
+ }, page);
+ try page.triggerKeyboard(keyboard_event);
// result already sent
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
- type: Type, // Type of the mouse event.
- x: f32, // X coordinate of the event relative to the main frame's viewport.
- y: f32, // Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport.
+ x: f64,
+ y: f64,
+ type: Type,
// Many optional parameters are not implemented yet, see documentation url.
const Type = enum {
@@ -95,23 +92,13 @@ fn dispatchMouseEvent(cmd: anytype) !void {
// quickly ignore types we know we don't handle
switch (params.type) {
- .mouseMoved, .mouseWheel => return,
+ .mouseMoved, .mouseWheel, .mouseReleased => return,
else => {},
}
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
-
- const mouse_event = Page.MouseEvent{
- .x = @intFromFloat(@floor(params.x)), // Decimal pixel values are not understood by netsurf or our renderer
- .y = @intFromFloat(@floor(params.y)), // So we convert them once at intake here. Using floor such that -0.5 becomes -1 and 0.5 becomes 0.
- .type = switch (params.type) {
- .mousePressed => .pressed,
- .mouseReleased => .released,
- else => unreachable,
- },
- };
- try page.mouseEvent(mouse_event);
+ try page.triggerMouseClick(params.x, params.y);
// result already sent
}