Merge pull request #2787 from lightpanda-io/frame_breakup

cleanup: Break various frame behavior into their own little utility f…
This commit is contained in:
Karl Seguin
2026-06-20 07:09:00 +08:00
committed by GitHub
23 changed files with 1977 additions and 1749 deletions

View File

@@ -200,11 +200,11 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
if (event._prevent_default) {
// can't return in a defer (╯°□°)╯︵ ┻━┻
} else if (event._type_string.eql(comptime .wrap("click"))) {
frame.handleClick(target) catch |err| {
Frame.user_input.handleClick(frame, target) catch |err| {
log.warn(.event, "frame.click", .{ .err = err });
};
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
frame.handleKeydown(target, event) catch |err| {
Frame.user_input.handleKeydown(frame, target, event) catch |err| {
log.warn(.event, "frame.keydown", .{ .err = err });
};
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,258 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Per-frame MutationObserver and IntersectionObserver bookkeeping: registration,
// the scheduling of the microtask deliveries, and broadcasting DOM mutations to
// the registered observers. The state lives on the Frame (frame._mutation /
// frame._intersection); these functions operate on it.
const std = @import("std");
const lp = @import("lightpanda");
const Frame = @import("../Frame.zig");
const Page = @import("../Page.zig");
const Node = @import("../webapi/Node.zig");
const Event = @import("../webapi/Event.zig");
const Element = @import("../webapi/Element.zig");
const MutationObserver = @import("../webapi/MutationObserver.zig");
const IntersectionObserver = @import("../webapi/IntersectionObserver.zig");
const log = lp.log;
const String = lp.String;
// MutationObserver bookkeeping for a frame.
pub const Mutation = struct {
// List of active MutationObservers
observers: std.DoublyLinkedList = .{},
delivery_scheduled: bool = false,
delivery_depth: u32 = 0,
};
// IntersectionObserver bookkeeping for a frame.
pub const Intersection = struct {
// List of active IntersectionObservers
observers: std.ArrayList(*IntersectionObserver) = .{},
check_scheduled: bool = false,
delivery_scheduled: bool = false,
};
// Releases the frame's references to its registered observers. Called from
// Frame.deinit.
pub fn deinit(frame: *Frame, page: *Page) void {
var node: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
while (node) |n| {
node = n.next; // capture before we potentially delete observer
const observer: *MutationObserver = @fieldParentPtr("node", n);
observer.releaseRef(page);
}
for (frame._intersection.observers.items) |observer| {
observer.releaseRef(page);
}
}
pub fn registerMutationObserver(frame: *Frame, observer: *MutationObserver) !void {
observer.acquireRef();
frame._mutation.observers.append(&observer.node);
}
pub fn unregisterMutationObserver(frame: *Frame, observer: *MutationObserver) void {
observer.releaseRef(frame._page);
frame._mutation.observers.remove(&observer.node);
}
pub fn registerIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) !void {
observer.acquireRef();
try frame._intersection.observers.append(frame.arena, observer);
}
pub fn unregisterIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) void {
for (frame._intersection.observers.items, 0..) |obs, i| {
if (obs == observer) {
observer.releaseRef(frame._page);
_ = frame._intersection.observers.swapRemove(i);
return;
}
}
}
pub fn hasMutationObservers(frame: *const Frame) bool {
return frame._mutation.observers.first != null;
}
pub fn checkIntersections(frame: *Frame) !void {
for (frame._intersection.observers.items) |observer| {
try observer.checkIntersections(frame);
}
}
pub fn scheduleMutationDelivery(frame: *Frame) !void {
if (frame._mutation.delivery_scheduled) {
return;
}
frame._mutation.delivery_scheduled = true;
try frame.js.queueMutationDelivery();
}
pub fn scheduleIntersectionDelivery(frame: *Frame) !void {
if (frame._intersection.delivery_scheduled) {
return;
}
frame._intersection.delivery_scheduled = true;
try frame.js.queueIntersectionDelivery();
}
pub fn performScheduledIntersectionChecks(frame: *Frame) void {
if (!frame._intersection.check_scheduled) {
return;
}
frame._intersection.check_scheduled = false;
checkIntersections(frame) catch |err| {
log.err(.frame, "frame.schedIntersectChecks", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
pub fn deliverIntersections(frame: *Frame) void {
if (!frame._intersection.delivery_scheduled) {
return;
}
frame._intersection.delivery_scheduled = false;
// Iterate backwards to handle observers that disconnect during their callback
var i = frame._intersection.observers.items.len;
while (i > 0) {
i -= 1;
const observer = frame._intersection.observers.items[i];
observer.deliverEntries(frame) catch |err| {
log.err(.frame, "frame.deliverIntersections", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
}
pub fn deliverMutations(frame: *Frame) void {
if (!frame._mutation.delivery_scheduled) {
return;
}
frame._mutation.delivery_scheduled = false;
frame._mutation.delivery_depth += 1;
defer if (!frame._mutation.delivery_scheduled) {
// reset the depth once nothing is left to be scheduled
frame._mutation.delivery_depth = 0;
};
if (frame._mutation.delivery_depth > 100) {
log.err(.frame, "frame.MutationLimit", .{ .type = frame._type, .url = frame.url });
frame._mutation.delivery_depth = 0;
return;
}
// snapshot the pending slots to deliver. We'll deliver these AFTER the mutation
// but new pending slots that land during mutation should only be delivered
// on the microtask tick.
const slots = frame.call_arena.dupe(*Element.Html.Slot, frame._slots_pending_slotchange.keys()) catch |err| blk: {
log.err(.frame, "deliverMutations.slots", .{ .err = err, .type = frame._type, .url = frame.url });
break :blk &.{};
};
frame._slots_pending_slotchange.clearRetainingCapacity();
// We only deliver notifications for observers that have records BEFORE
// we started the delivery. So we need to snapshot this. Any observers which
// get records during this phase will only be processed on the next microtask tick.
var notify: std.ArrayList(*MutationObserver) = .empty;
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node);
if (observer._pending_records.items.len == 0) {
continue;
}
notify.append(frame.call_arena, observer) catch |err| {
log.err(.frame, "deliverMutations.notify", .{ .err = err, .type = frame._type, .url = frame.url });
break;
};
}
for (notify.items) |observer| {
observer.deliverRecords(frame) catch |err| {
log.err(.frame, "frame.deliverMutations", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
// slotchange events fire after the observer callbacks (spec step order)
for (slots) |slot| {
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, frame._page) catch |err| {
log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = frame._type, .url = frame.url });
continue;
};
const target = slot.asNode().asEventTarget();
frame._event_manager.dispatch(target, event) catch |err| {
log.err(.frame, "deliverSlotchange.dispatch", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
}
// Broadcast an attribute change to every registered MutationObserver. The
// caller (Frame.attributeChange / attributeRemove) handles the non-observer
// side effects (build hooks, custom-element callbacks, slot/popover updates).
pub fn notifyAttributeChange(frame: *Frame, element: *Element, name: String, old_value: ?String) void {
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyAttributeChange(element, name, old_value, frame) catch |err| {
log.err(.frame, "attributeChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
}
pub fn notifyCharacterDataChange(frame: *Frame, target: *Node, old_value: String) void {
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyCharacterDataChange(target, old_value, frame) catch |err| {
log.err(.frame, "cdataChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
}
pub fn notifyChildListChange(
frame: *Frame,
target: *Node,
added_nodes: []const *Node,
removed_nodes: []const *Node,
previous_sibling: ?*Node,
next_sibling: ?*Node,
) void {
// Filter out HTML wrapper element during fragment parsing (html5ever quirk)
if (frame._parse_mode == .fragment and added_nodes.len == 1) {
if (added_nodes[0].is(Element.Html.Html) != null) {
// This is the temporary HTML wrapper, added by html5ever
// that will be unwrapped, see:
// https://github.com/servo/html5ever/issues/583
return;
}
}
var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first;
while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, frame) catch |err| {
log.err(.frame, "childListChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url });
};
}
}

View File

@@ -0,0 +1,477 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Synthetic user input driving the DOM: mouse, wheel, keyboard, focus
// navigation and text insertion. These are mostly fed by CDP's Input domain
// (src/cdp/domains/input.zig) and by EventManager's default activation
// behavior. Form submission itself lives on the Frame (it's a navigation
// concern); the activation paths here call into it.
const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin");
const Frame = @import("../Frame.zig");
const Node = @import("../webapi/Node.zig");
const Event = @import("../webapi/Event.zig");
const Element = @import("../webapi/Element.zig");
const TreeWalker = @import("../webapi/TreeWalker.zig");
const MouseEvent = @import("../webapi/event/MouseEvent.zig");
const WheelEvent = @import("../webapi/event/WheelEvent.zig");
const KeyboardEvent = @import("../webapi/event/KeyboardEvent.zig");
const log = lp.log;
const IS_DEBUG = builtin.mode == .Debug;
// DOM MouseEvent.button values.
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
pub const mouse_button = struct {
pub const main: i32 = 0; // left
pub const auxiliary: i32 = 1; // middle
pub const secondary: i32 = 2; // right
pub const fourth: i32 = 3; // back
pub const fifth: i32 = 4; // forward
};
// Dispatch a single trusted mouse event of the given type on `target`, carrying
// the pressed button and pointer position. `detail` is the click count (used for
// click/dblclick); 0 for events where it does not apply.
fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void {
const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
.button = button,
.detail = detail,
}, frame);
try frame._event_manager.dispatch(target.asEventTarget(), event.asEvent());
}
pub fn triggerMousePress(frame: *Frame, x: f64, y: f64, button: i32) !void {
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse press", .{
.url = frame.url,
.node = target,
.x = x,
.y = y,
.button = button,
.type = frame._type,
});
}
try dispatchMouseEventOn(frame, target, "mousedown", x, y, button, 0);
}
pub fn triggerMouseMove(frame: *Frame, x: f64, y: f64) !void {
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse move", .{
.url = frame.url,
.node = target,
.x = x,
.y = y,
.type = frame._type,
});
}
const move_event: *MouseEvent = try .initTrusted(comptime .wrap("mousemove"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, frame);
try frame._event_manager.dispatch(target.asEventTarget(), move_event.asEvent());
const over_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, frame);
try frame._event_manager.dispatch(target.asEventTarget(), over_event.asEvent());
const enter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{
.composed = true,
.clientX = x,
.clientY = y,
}, frame);
try frame._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent());
}
pub fn triggerMouseRelease(frame: *Frame, x: f64, y: f64, button: i32, click_count: i32) !void {
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse release", .{
.url = frame.url,
.node = target,
.x = x,
.y = y,
.button = button,
.type = frame._type,
});
}
const detail: u32 = if (click_count > 0) @intCast(click_count) else 1;
try dispatchMouseEventOn(frame, target, "mouseup", x, y, button, detail);
// After mouseup, the activation event depends on the button.
switch (button) {
mouse_button.main => {
try dispatchMouseEventOn(frame, target, "click", x, y, button, detail);
// A second click in quick succession also fires dblclick.
if (click_count == 2) {
try dispatchMouseEventOn(frame, target, "dblclick", x, y, button, detail);
}
},
mouse_button.auxiliary => try dispatchMouseEventOn(frame, target, "auxclick", x, y, button, detail),
mouse_button.secondary => try dispatchMouseEventOn(frame, target, "contextmenu", x, y, button, detail),
else => {},
}
}
pub fn triggerMouseWheel(frame: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void {
const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse wheel", .{
.url = frame.url,
.node = target,
.x = x,
.y = y,
.delta_x = delta_x,
.delta_y = delta_y,
.type = frame._type,
});
}
const wheel_event: *WheelEvent = try .initTrusted("wheel", .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
.deltaX = delta_x,
.deltaY = delta_y,
}, frame);
// Keep the event alive past dispatch so we can read _prevent_default.
wheel_event.asEvent().acquireRef();
defer _ = wheel_event.asEvent().releaseRef(frame._page);
try frame._event_manager.dispatch(target.asEventTarget(), wheel_event.asEvent());
if (wheel_event.asEvent()._prevent_default) {
return;
}
// Apply the scroll and fire a trusted scroll event, mirroring WebDriver wheel.
// CDP deltas are untrusted, so guard NaN and saturate the addition.
const new_left: i32 = @as(i32, @intCast(target.getScrollLeft(frame))) +| deltaToScroll(delta_x);
const new_top: i32 = @as(i32, @intCast(target.getScrollTop(frame))) +| deltaToScroll(delta_y);
try target.setScrollLeft(new_left, frame);
try target.setScrollTop(new_top, frame);
const scroll_event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._page);
try frame._event_manager.dispatch(target.asEventTarget(), scroll_event);
}
fn deltaToScroll(d: f64) i32 {
if (std.math.isNan(d)) return 0;
return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32)));
}
// callback when the "click" event reaches the frame.
pub fn handleClick(frame: *Frame, target: *Node) !void {
// TODO: Also support <area> elements when implement
const element = target.is(Element) orelse return;
const html_element = element.is(Element.Html) orelse return;
switch (html_element._type) {
.anchor => |anchor| {
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
if (href.len == 0) {
return;
}
if (std.mem.startsWith(u8, href, "javascript:")) {
return;
}
if (try element.hasAttribute(comptime .wrap("download"), frame)) {
log.warn(.browser, "a.download", .{ .type = frame._type, .url = frame.url });
return;
}
const target_frame = blk: {
const target_name = anchor.getTarget();
if (target_name.len == 0) {
break :blk target.ownerFrame(frame);
}
break :blk frame.resolveTargetFrame(target_name) orelse {
log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name });
return;
};
};
try element.focus(frame);
try frame.scheduleNavigation(href, .{
.reason = .script,
.kind = .{ .push = null },
}, .{ .anchor = target_frame });
},
.input => |input| {
try element.focus(frame);
// Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an
// image button submits its form. The form-data set already gets the
// submitter's coordinate fields appended via FormData.collectForm
// (see src/browser/webapi/net/FormData.zig).
if (input._input_type == .submit or input._input_type == .image) {
return frame.submitForm(element, input.getForm(frame), .{});
}
},
.button => |button| {
try element.focus(frame);
if (std.mem.eql(u8, button.getType(), "submit")) {
return frame.submitForm(element, button.getForm(frame), .{});
}
},
.select, .textarea => try element.focus(frame),
.label => |label| {
// Per HTML §4.10.4 "The label element", a label's activation
// behavior is to run the synthetic click activation steps on the
// labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler.
const control = label.getControl(frame) orelse return;
const control_html = control.is(Element.Html) orelse return;
try control_html.click(frame);
},
.generic => |generic| {
switch (generic._tag) {
.summary => {
const parent_el = target.parentElement() orelse return;
const details = parent_el.is(Element.Html.Details) orelse return;
var maybe_prev = element.previousElementSibling();
while (maybe_prev) |prev| {
if (prev.getTag() == .summary) {
// we found a summary element before the clicked one
return;
}
maybe_prev = prev.previousElementSibling();
}
try details.setOpen(!details.getOpen(), frame);
},
else => {},
}
},
else => {},
}
}
pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent();
// Dispatch to the effective active element. When nothing is explicitly
// focused this resolves to <body> (matching `document.activeElement`), so
// the keydown still fires and its default action — e.g. sequential focus
// navigation on Tab — can run.
const element = frame.window._document.getActiveElement() orelse {
event.deinit(frame._page);
return;
};
if (comptime IS_DEBUG) {
log.debug(.frame, "frame keydown", .{
.url = frame.url,
.node = element,
.key = keyboard_event._key,
.type = frame._type,
});
}
try frame._event_manager.dispatch(element.asEventTarget(), event);
}
pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void {
const keyboard_event = event.is(KeyboardEvent) orelse return;
const key = keyboard_event.getKey();
if (key == .Dead) {
return;
}
if (key == .Tab) {
// tab -> forward, shift+tab -> backwards
return moveFocus(frame, keyboard_event.getShiftKey() == false);
}
if (target.is(Element.Html.Input)) |input| {
if (key == .Enter) {
return frame.submitForm(input.asElement(), input.getForm(frame), .{});
}
// 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()) {
try input.innerInsert(key.asString(), frame);
}
return;
}
if (target.is(Element.Html.TextArea)) |textarea| {
// zig fmt: off
const append =
if (key == .Enter) "\n"
else if (key.isPrintable()) key.asString()
else return
;
// zig fmt: on
return textarea.innerInsert(append, frame);
}
}
// Sequential focus navigation: move `document.activeElement` to the next (Tab)
// or previous (Shift+Tab) focusable element, firing the usual blur/focus events
// via `Element.focus`. The order is fully determined by tabindex + document
// position, so no layout is needed:
// 1. elements with a positive tabindex, in ascending tabindex order;
// 2. then elements with tabindex 0 (or a natively-focusable default), in
// document order.
// Ties within a group break on document order, and Tab wraps around at the ends.
// https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation
fn moveFocus(frame: *Frame, forward: bool) !void {
const document = frame.document;
const current = document._active_element;
const current_tab_index = blk: {
const cur = current orelse break :blk 0;
const current_html = cur.is(Element.Html) orelse break :blk 0;
break :blk current_html.getTabIndex();
};
// Single document-order pass tracking two candidates:
// edge — the global first (forward) / last (backward) focusable element,
// used to wrap around when `current` is at an end, or as the
// landing spot when nothing is focused yet.
// chosen — the closest focusable element strictly past `current` in the
// travel direction.
var edge: ?*Element = null;
var edge_tab_index: i32 = 0;
var chosen: ?*Element = null;
var chosen_tab_index: i32 = 0;
var tw = TreeWalker.Full.Elements.init(document.asNode(), .{});
while (tw.next()) |candidate| {
if (candidate.isDisabled()) {
continue;
}
if (candidate.is(Element.Html) == null) {
continue;
}
const candidate_tab_index = blk: {
if (candidate.getAttributeSafe(comptime .wrap("tabindex"))) |attr| {
if (Element.Html.parseInteger(attr)) |tab_index| {
if (tab_index < 0) {
continue;
}
break :blk tab_index;
}
break :blk 0;
}
// no tab index, maybe this item isn't focusable..
const focusable = switch (candidate.getTag()) {
.button, .select, .textarea, .iframe => true,
.input => candidate.as(Element.Html.Input)._input_type != .hidden,
.anchor, .area => candidate.getAttributeSafe(comptime .wrap("href")) != null,
else => false,
};
if (focusable == false) {
continue;
}
break :blk 0;
};
if (edge == null or focusOrderBefore(candidate, candidate_tab_index, edge.?, edge_tab_index) == forward) {
edge = candidate;
edge_tab_index = candidate_tab_index;
}
const cur = current orelse continue;
if (candidate == cur) {
continue;
}
const past = if (forward) focusOrderBefore(cur, current_tab_index, candidate, candidate_tab_index) else focusOrderBefore(candidate, candidate_tab_index, cur, current_tab_index);
if (!past) {
continue;
}
if (chosen == null or focusOrderBefore(candidate, candidate_tab_index, chosen.?, chosen_tab_index) == forward) {
chosen = candidate;
chosen_tab_index = candidate_tab_index;
}
}
const next = chosen orelse edge orelse return;
try next.focus(frame);
}
// Orders two focusable elements by sequential focus navigation order: positive
// tabindex first (ascending), then tabindex 0, ties broken by document order.
fn focusOrderBefore(a: *Element, a_tab_index: i32, b: *Element, b_tab_index: i32) bool {
if (a_tab_index == b_tab_index) {
// Equal tabindex → document order: `a` precedes `b` when `b` follows `a`.
const FOLLOWING: u16 = 0x04;
return (a.asNode().compareDocumentPosition(b.asNode()) & FOLLOWING) != 0;
}
const group_a: u8 = if (a_tab_index > 0) 0 else 1;
const group_b: u8 = if (b_tab_index > 0) 0 else 1;
if (group_a != group_b) {
return group_a < group_b;
}
return a_tab_index < b_tab_index;
}
// insertText is a shortcut to insert text into the active element.
pub fn insertText(frame: *Frame, v: []const u8) !void {
const html_element = frame.document._active_element orelse return;
if (html_element.is(Element.Html.Input)) |input| {
const input_type = input._input_type;
if (input_type == .radio or input_type == .checkbox) {
return;
}
return input.innerInsert(v, frame);
}
if (html_element.is(Element.Html.TextArea)) |textarea| {
return textarea.innerInsert(v, frame);
}
}

View File

@@ -1039,7 +1039,7 @@ pub fn queueMutationDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.frame => |frame| frame.deliverMutations(),
.frame => |frame| Frame.observers.deliverMutations(frame),
.worker => unreachable,
}
}
@@ -1050,7 +1050,7 @@ pub fn queueIntersectionChecks(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.frame => |frame| frame.performScheduledIntersectionChecks(),
.frame => |frame| Frame.observers.performScheduledIntersectionChecks(frame),
.worker => unreachable,
}
}
@@ -1061,7 +1061,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.frame => |frame| frame.deliverIntersections(),
.frame => |frame| Frame.observers.deliverIntersections(frame),
.worker => unreachable,
}
}

View File

@@ -142,7 +142,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void {
// Fresh text run: the first chunk lives on _data only. buf stays empty
// until (and unless) a second chunk arrives.
const new_text = try self.frame.createTextNode(txt);
const new_text = try Frame.node_factory.createTextNode(self.frame, txt);
try self.frame.appendNew(parent, new_text);
self.pending_text = .{
.parent = parent,
@@ -446,7 +446,7 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
const name = qname.local.slice();
const namespace_string = qname.ns.slice();
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
const node = try frame.createElementNS(namespace, name, attributes);
const node = try Frame.node_factory.createElementNS(frame, namespace, name, attributes);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
@@ -467,7 +467,7 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a
}
fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
const frame = self.frame;
const node = try frame.createComment(str);
const node = try Frame.node_factory.createComment(frame, str);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -487,7 +487,7 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h
}
fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque {
const frame = self.frame;
const node = try frame.createProcessingInstruction(target, data);
const node = try Frame.node_factory.createProcessingInstruction(frame, target, data);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
@@ -697,7 +697,7 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
}
break :blk child;
},
.text => |txt| try self.frame.createTextNode(txt),
.text => |txt| try Frame.node_factory.createTextNode(self.frame, txt),
};
try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
}

View File

@@ -235,7 +235,7 @@ pub fn setData(self: *CData, value: ?[]const u8, frame: *Frame) !void {
self._data = .empty;
}
frame.characterDataChange(self.asNode(), old_value);
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
}
/// JS bridge wrapper for `data` setter.
@@ -305,7 +305,7 @@ pub fn deleteData(self: *CData, offset: usize, count: usize, frame: *Frame) !voi
old_value[range.end..],
});
}
frame.characterDataChange(self.asNode(), old_data);
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_data);
}
pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame) !void {
@@ -321,7 +321,7 @@ pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame)
data,
existing[byte_offset..],
});
frame.characterDataChange(self.asNode(), old_value);
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
}
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, frame: *Frame) !void {
@@ -340,7 +340,7 @@ pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8,
data,
existing[range.end..],
});
frame.characterDataChange(self.asNode(), old_value);
Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value);
}
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {

View File

@@ -44,20 +44,20 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString
_ = try document.asNode().appendChild(doctype.asNode(), frame);
}
const html_node = try frame.createElementNS(.html, "html", null);
const html_node = try Frame.node_factory.createElementNS(frame, .html, "html", null);
_ = try document.asNode().appendChild(html_node, frame);
const head_node = try frame.createElementNS(.html, "head", null);
const head_node = try Frame.node_factory.createElementNS(frame, .html, "head", null);
_ = try html_node.appendChild(head_node, frame);
if (title) |t| {
const title_node = try frame.createElementNS(.html, "title", null);
const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null);
_ = try head_node.appendChild(title_node, frame);
const text_node = try frame.createTextNode(t.value);
const text_node = try Frame.node_factory.createTextNode(frame, t.value);
_ = try title_node.appendChild(text_node, frame);
}
const body_node = try frame.createElementNS(.html, "body", null);
const body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null);
_ = try html_node.appendChild(body_node, frame);
return document;
@@ -77,7 +77,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qual
if (qualified_name) |qname| {
if (qname.len > 0) {
const namespace = Node.Element.Namespace.parse(namespace_);
const root = try frame.createElementNS(namespace, qname, null);
const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null);
_ = try document.asNode().appendChild(root, frame);
}
}

View File

@@ -165,7 +165,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
};
// HTML documents are case-insensitive - lowercase the tag name
const node = try frame.createElementNS(ns, normalized_name, null);
const node = try Frame.node_factory.createElementNS(frame, ns, normalized_name, null);
const element = node.as(Element);
// Track owner document if it's not the main document
@@ -186,7 +186,7 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
try validateElementName(name);
const ns = Element.Namespace.parse(namespace);
// Per spec, createElementNS does NOT lowercase (unlike createElement).
const node = try frame.createElementNS(ns, name, null);
const node = try Frame.node_factory.createElementNS(frame, ns, name, null);
// Store original URI for unknown namespaces so lookupNamespaceURI can return it
if (ns == .unknown) {
@@ -317,7 +317,7 @@ pub fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFra
}
pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createComment(data);
const node = try Frame.node_factory.createComment(frame, data);
// Track owner document if it's not the main document
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
@@ -326,7 +326,7 @@ pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node {
}
pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createTextNode(data);
const node = try Frame.node_factory.createTextNode(frame, data);
// Track owner document if it's not the main document
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
@@ -337,8 +337,8 @@ pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node {
pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*Node {
const node = switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => try frame.createCDATASection(data),
.generic => try frame.createCDATASection(data),
.xml => try Frame.node_factory.createCDATASection(frame, data),
.generic => try Frame.node_factory.createCDATASection(frame, data),
};
// Track owner document if it's not the main document
if (self != frame.document) {
@@ -348,7 +348,7 @@ pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*No
}
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, frame: *Frame) !*Node {
const node = try frame.createProcessingInstruction(target, data);
const node = try Frame.node_factory.createProcessingInstruction(frame, target, data);
// Track owner document if it's not the main document
if (self != frame.document) {
try frame.setNodeOwnerDocument(node, self);
@@ -1129,9 +1129,9 @@ fn _injectBlank(self: *Document, frame: *Frame) !void {
std.debug.assert(self.asNode()._children == null);
}
const html = try frame.createElementNS(.html, "html", null);
const head = try frame.createElementNS(.html, "head", null);
const body = try frame.createElementNS(.html, "body", null);
const html = try Frame.node_factory.createElementNS(frame, .html, "html", null);
const head = try Frame.node_factory.createElementNS(frame, .html, "head", null);
const body = try Frame.node_factory.createElementNS(frame, .html, "body", null);
try frame.appendNode(html, head, .{});
try frame.appendNode(html, body, .{});
try frame.appendNode(self.asNode(), html, .{});

View File

@@ -797,7 +797,7 @@ pub fn insertAdjacentText(
data: []const u8,
frame: *Frame,
) !void {
const text_node = try frame.createTextNode(data);
const text_node = try Frame.node_factory.createTextNode(frame, data);
const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);
_ = try target_node.insertBefore(text_node, prev_node, frame);
}
@@ -1494,7 +1494,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Fr
pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
const tag_name = self.getTagNameDump();
const node = try frame.createElementNS(self._namespace, tag_name, self._attributes);
const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| {

View File

@@ -66,7 +66,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void {
// Build a fresh <body> holding the parsed HTML as its children. Fragment
// parsing strips any <html>/<body>/<head> wrappers the author included.
const new_body_node = try frame.createElementNS(.html, "body", null);
const new_body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null);
if (html.len > 0) {
try frame.parseHtmlAsChildren(new_body_node, html);
}
@@ -153,7 +153,7 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, frame: *Frame) !void {
}
// No title element found, create one
const title_node = try frame.createElementNS(.html, "title", null);
const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null);
const title_element = title_node.as(Element);
// Only add text if non-empty

View File

@@ -139,7 +139,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo
try self._observing.append(self._arena, target);
if (self._observing.items.len == 1) {
try frame.registerIntersectionObserver(self);
try Frame.observers.registerIntersectionObserver(frame, self);
}
try self._tracked.put(self._arena, target, {});
@@ -147,7 +147,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo
// Check intersection for this new target and schedule delivery
try self.checkIntersection(target, frame);
if (self._pending_entries.items.len > 0) {
try frame.scheduleIntersectionDelivery();
try Frame.observers.scheduleIntersectionDelivery(frame);
}
}
@@ -174,7 +174,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v
}
if (original_length > 0 and self._observing.items.len == 0) {
frame.unregisterIntersectionObserver(self);
Frame.observers.unregisterIntersectionObserver(frame, self);
}
}
@@ -186,7 +186,7 @@ pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void {
self._tracked.clearRetainingCapacity();
if (self._observing.items.len > 0) {
frame.unregisterIntersectionObserver(self);
Frame.observers.unregisterIntersectionObserver(frame, self);
}
self._observing.clearRetainingCapacity();
}
@@ -298,7 +298,7 @@ pub fn checkIntersections(self: *IntersectionObserver, frame: *Frame) !void {
}
if (self._pending_entries.items.len > 0) {
try frame.scheduleIntersectionDelivery();
try Frame.observers.scheduleIntersectionDelivery(frame);
}
}

View File

@@ -171,7 +171,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
});
if (self._observing.items.len == 1) {
try frame.registerMutationObserver(self);
try Frame.observers.registerMutationObserver(frame, self);
}
}
@@ -182,7 +182,7 @@ pub fn disconnect(self: *MutationObserver, frame: *Frame) void {
self._pending_records.clearRetainingCapacity();
if (self._observing.items.len > 0) {
frame.unregisterMutationObserver(self);
Frame.observers.unregisterMutationObserver(frame, self);
}
self._observing.clearRetainingCapacity();
}
@@ -244,7 +244,7 @@ pub fn notifyAttributeChange(
try self._pending_records.append(self._arena, record);
try frame.scheduleMutationDelivery();
try Frame.observers.scheduleMutationDelivery(frame);
break;
}
}
@@ -288,7 +288,7 @@ pub fn notifyCharacterDataChange(
try self._pending_records.append(self._arena, record);
try frame.scheduleMutationDelivery();
try Frame.observers.scheduleMutationDelivery(frame);
break;
}
}
@@ -332,7 +332,7 @@ pub fn notifyChildListChange(
try self._pending_records.append(self._arena, record);
try frame.scheduleMutationDelivery();
try Frame.observers.scheduleMutationDelivery(frame);
break;
}
}

View File

@@ -622,12 +622,12 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
if (new_node == ref_node_) {
frame.domChanged();
if (frame.hasMutationObservers()) {
if (Frame.observers.hasMutationObservers(frame)) {
const parent = new_node._parent.?;
const previous_sibling = new_node.previousSibling();
const next_sibling = new_node.nextSibling();
const replaced = [_]*Node{new_node};
frame.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);
Frame.observers.notifyChildListChange(frame, parent, &replaced, &replaced, previous_sibling, next_sibling);
}
return new_node;
@@ -832,10 +832,10 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node {
.cdata => |cd| {
const data = cd.getData().str();
return switch (cd._type) {
.text => frame.createTextNode(data),
.cdata_section => frame.createCDATASection(data),
.comment => frame.createComment(data),
.processing_instruction => |pi| frame.createProcessingInstruction(pi._target, data),
.text => Frame.node_factory.createTextNode(frame, data),
.cdata_section => Frame.node_factory.createCDATASection(frame, data),
.comment => Frame.node_factory.createComment(frame, data),
.processing_instruction => |pi| Frame.node_factory.createProcessingInstruction(frame, pi._target, data),
};
},
.element => |el| return el.clone(deep, frame),
@@ -1314,7 +1314,7 @@ pub const NodeOrText = union(enum) {
pub fn toNode(self: *const NodeOrText, frame: *Frame) !*Node {
return switch (self.*) {
.node => |n| n,
.text => |txt| frame.createTextNode(txt),
.text => |txt| Frame.node_factory.createTextNode(frame, txt),
};
}

View File

@@ -353,8 +353,8 @@ pub fn insertNode(self: *Range, node: *Node, frame: *Frame) !void {
const before_text = text_data[0..offset];
const after_text = text_data[offset..];
const before = try frame.createTextNode(before_text);
const after = try frame.createTextNode(after_text);
const before = try Frame.node_factory.createTextNode(frame, before_text);
const after = try Frame.node_factory.createTextNode(frame, after_text);
_ = try parent.replaceChild(before, container, frame);
_ = try parent.insertBefore(node, before.nextSibling(), frame);
@@ -390,7 +390,7 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void {
frame.arena,
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
);
frame.characterDataChange(self._proto._start_container, old_value);
Frame.observers.notifyCharacterDataChange(frame, self._proto._start_container, old_value);
} else {
// Delete child nodes in range.
// Capture count before the loop: removeChild triggers live range
@@ -459,7 +459,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
const text_data = self._proto._start_container.getData().str();
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
const text_node = try frame.createTextNode(cloned_text);
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
} else {
@@ -481,7 +481,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
if (self._proto._start_offset < text_data.len) {
// Clone from start_offset to end of text
const cloned_text = text_data[self._proto._start_offset..];
const text_node = try frame.createTextNode(cloned_text);
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
}
@@ -504,7 +504,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
// Clone from start to end_offset
const cloned_text = text_data[0..self._proto._end_offset];
const text_node = try frame.createTextNode(cloned_text);
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
_ = try fragment.asNode().appendChild(text_node, frame);
}
}
@@ -550,9 +550,9 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Fr
// Create a temporary element of the same type as the context for parsing
// This preserves the parsing context without modifying the original node
const temp_node = if (context_node.is(Node.Element)) |el|
try frame.createElementNS(el._namespace, el.getTagNameLower(), null)
try Frame.node_factory.createElementNS(frame, el._namespace, el.getTagNameLower(), null)
else
try frame.createElementNS(.html, "div", null);
try Frame.node_factory.createElementNS(frame, .html, "div", null);
try frame.parseContextualFragment(temp_node, html);

View File

@@ -26,7 +26,7 @@ const Comment = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment {
const node = try frame.createComment(if (str) |s| s.value else "");
const node = try Frame.node_factory.createComment(frame, if (str) |s| s.value else "");
return node.as(Comment);
}

View File

@@ -27,7 +27,7 @@ const Text = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, frame: *Frame) !*Text {
const node = try frame.createTextNode(if (str) |s| s.value else "");
const node = try Frame.node_factory.createTextNode(frame, if (str) |s| s.value else "");
return node.as(Text);
}
@@ -45,7 +45,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
const new_data = data[byte_offset..];
const new_node = try frame.createTextNode(new_data);
const new_node = try Frame.node_factory.createTextNode(frame, new_data);
const new_text = new_node.as(Text);
const node = self._proto.asNode();

View File

@@ -116,7 +116,7 @@ pub fn construct(new_target: js.Function, frame: *Frame) !*Element {
if (frame._upgrading_element) |node| {
return node.is(Element) orelse return error.IllegalConstructor;
}
return frame.constructCustomElement(new_target);
return Frame.node_factory.constructCustomElement(frame, new_target);
}
pub const Type = union(enum) {
@@ -288,7 +288,7 @@ pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void {
}
// Create and append text node
const text_node = try frame.createTextNode(text);
const text_node = try Frame.node_factory.createTextNode(frame, text);
try frame.appendNode(parent, text_node, .{ .child_already_connected = false });
}

View File

@@ -32,7 +32,7 @@ const Audio = @This();
_proto: *Media,
pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media {
const node = try frame.createElementNS(.html, "audio", null);
const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null);
const el = node.as(Element);
const list = try el.getOrCreateAttributeList(frame);

View File

@@ -9,7 +9,7 @@ const Image = @This();
_proto: *HtmlElement,
pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image {
const node = try frame.createElementNS(.html, "img", null);
const node = try Frame.node_factory.createElementNS(frame, .html, "img", null);
const el = node.as(Element);
if (w_) |w| blk: {

View File

@@ -106,7 +106,7 @@ pub fn setDefaultValue(self: *TextArea, value: []const u8, frame: *Frame) !void
}
// No text child exists, create one
const text_node = try frame.createTextNode(value);
const text_node = try Frame.node_factory.createTextNode(frame, value);
_ = try node.appendChild(text_node, frame);
}

View File

@@ -18,8 +18,9 @@
const std = @import("std");
const CDP = @import("../CDP.zig");
const Frame = @import("../../browser/Frame.zig");
const dom_button = @import("../../browser/Frame.zig").mouse_button;
const dom_button = Frame.user_input.mouse_button;
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
@@ -74,7 +75,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
.metaKey = params.modifiers & 4 == 4,
.shiftKey = params.modifiers & 8 == 8,
}, frame);
try frame.triggerKeyboard(keyboard_event);
try Frame.user_input.triggerKeyboard(frame, keyboard_event);
// result already sent
}
@@ -124,10 +125,10 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
};
switch (params.type) {
.mousePressed => try frame.triggerMousePress(params.x, params.y, button),
.mouseReleased => try frame.triggerMouseRelease(params.x, params.y, button, params.clickCount),
.mouseMoved => try frame.triggerMouseMove(params.x, params.y),
.mouseWheel => try frame.triggerMouseWheel(params.x, params.y, params.deltaX, params.deltaY),
.mousePressed => try Frame.user_input.triggerMousePress(frame, params.x, params.y, button),
.mouseReleased => try Frame.user_input.triggerMouseRelease(frame, params.x, params.y, button, params.clickCount),
.mouseMoved => try Frame.user_input.triggerMouseMove(frame, params.x, params.y),
.mouseWheel => try Frame.user_input.triggerMouseWheel(frame, params.x, params.y, params.deltaX, params.deltaY),
}
// result already sent
}
@@ -141,7 +142,7 @@ fn insertText(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return;
const frame = bc.session.currentFrame() orelse return;
try frame.insertText(params.text);
try Frame.user_input.insertText(frame, params.text);
try cmd.sendResult(null, .{});
}