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/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 }