Merge pull request #773 from lightpanda-io/keydown_handling
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled

Add basic support for key events
This commit is contained in:
Karl Seguin
2025-06-12 12:32:09 +08:00
committed by GitHub
5 changed files with 223 additions and 15 deletions

View File

@@ -243,17 +243,23 @@ pub const Document = struct {
return try TreeWalker.init(root, what_to_show, filter); return try TreeWalker.init(root, what_to_show, filter);
} }
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion { pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self))); if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
if (state.active_element) |ae| { if (state.active_element) |ae| {
return try Element.toInterface(ae); return ae;
}
} }
if (try parser.documentHTMLBody(page.window.document)) |body| { if (try parser.documentHTMLBody(page.window.document)) |body| {
return try Element.toInterface(@alignCast(@ptrCast(body))); return @alignCast(@ptrCast(body));
} }
return get_documentElement(self); return try parser.documentGetDocumentElement(self);
}
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
const ae = (try getActiveElement(self, page)) orelse return null;
return try Element.toInterface(ae);
} }
// TODO: some elements can't be focused, like if they're disabled // TODO: some elements can't be focused, like if they're disabled

View File

@@ -25,6 +25,7 @@ const c = @cImport({
@cInclude("events/event_target.h"); @cInclude("events/event_target.h");
@cInclude("events/event.h"); @cInclude("events/event.h");
@cInclude("events/mouse_event.h"); @cInclude("events/mouse_event.h");
@cInclude("events/keyboard_event.h");
@cInclude("utils/validate.h"); @cInclude("utils/validate.h");
@cInclude("html/html_element.h"); @cInclude("html/html_element.h");
@cInclude("html/html_document.h"); @cInclude("html/html_document.h");
@@ -864,6 +865,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
return eventDefaultPrevented(@ptrCast(evt)); return eventDefaultPrevented(@ptrCast(evt));
} }
// KeyboardEvent
pub const KeyboardEvent = c.dom_keyboard_event;
pub fn keyboardEventCreate() !*KeyboardEvent {
var evt: ?*KeyboardEvent = undefined;
const err = c._dom_keyboard_event_create(&evt);
try DOMErr(err);
return evt.?;
}
pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
c._dom_keyboard_event_destroy(evt);
}
const KeyboardEventOpts = struct {
key: []const u8,
code: []const u8,
bubbles: bool = false,
cancelable: bool = false,
ctrl: bool = false,
alt: bool = false,
shift: bool = false,
meta: bool = false,
};
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
const s = try strFromData(typ);
const err = c._dom_keyboard_event_init(
evt,
s,
opts.bubbles,
opts.cancelable,
null, // dom_abstract_view* ?
try strFromData(opts.key),
try strFromData(opts.code),
0, // location 0 == standard
opts.ctrl,
opts.shift,
opts.alt,
opts.meta,
false, // repease
false, // is_composiom
);
try DOMErr(err);
}
pub fn keyboardEventGetKey(evt: *KeyboardEvent) ![]const u8 {
var s: ?*String = undefined;
_ = c._dom_keyboard_event_get_key(evt, &s);
return strToData(s.?);
}
// NodeType // NodeType
pub const NodeType = enum(u4) { pub const NodeType = enum(u4) {
@@ -2393,6 +2447,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
return strToData(s); return strToData(s);
} }
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
try DOMErr(err);
}
// Select // Select
pub fn selectGetOptions(select: *Select) !*OptionCollection { pub fn selectGetOptions(select: *Select) !*OptionCollection {
var collection: ?*OptionCollection = null; var collection: ?*OptionCollection = null;
@@ -2775,3 +2834,11 @@ pub fn inputSetValue(input: *Input, value: []const u8) !void {
const err = c.dom_html_input_element_set_value(input, try strFromData(value)); const err = c.dom_html_input_element_set_value(input, try strFromData(value));
try DOMErr(err); try DOMErr(err);
} }
pub fn buttonGetType(button: *Button) ![]const u8 {
var s_: ?*String = null;
const err = c.dom_html_button_element_get_type(button, &s_);
try DOMErr(err);
const s = s_ orelse return "button";
return strToData(s);
}

View File

@@ -80,6 +80,7 @@ pub const Page = struct {
microtask_node: Loop.CallbackNode, microtask_node: Loop.CallbackNode,
keydown_event_node: parser.EventNode,
window_clicked_event_node: parser.EventNode, window_clicked_event_node: parser.EventNode,
// Our JavaScript context for this specific page. This is what we use to // Our JavaScript context for this specific page. This is what we use to
@@ -112,6 +113,7 @@ pub const Page = struct {
.state_pool = &browser.state_pool, .state_pool = &browser.state_pool,
.cookie_jar = &session.cookie_jar, .cookie_jar = &session.cookie_jar,
.microtask_node = .{ .func = microtaskCallback }, .microtask_node = .{ .func = microtaskCallback },
.keydown_event_node = .{ .func = keydownCallback },
.window_clicked_event_node = .{ .func = windowClicked }, .window_clicked_event_node = .{ .func = windowClicked },
.request_factory = browser.http_client.requestFactory(.{ .request_factory = browser.http_client.requestFactory(.{
.notification = browser.notification, .notification = browser.notification,
@@ -307,6 +309,12 @@ pub const Page = struct {
&self.window_clicked_event_node, &self.window_clicked_event_node,
false, false,
); );
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element),
"keydown",
&self.keydown_event_node,
false,
);
// https://html.spec.whatwg.org/#read-html // https://html.spec.whatwg.org/#read-html
@@ -574,14 +582,14 @@ pub const Page = struct {
}, },
.input => { .input => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const input_type = (try parser.elementGetAttribute(element, "type")) orelse return; const input_type = try parser.inputGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(input_type, "submit")) { if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
}, },
.button => { .button => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const button_type = (try parser.elementGetAttribute(element, "type")) orelse return; const button_type = try parser.buttonGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(button_type, "submit")) { if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
@@ -595,6 +603,88 @@ pub const Page = struct {
} }
} }
pub const KeyboardEvent = struct {
type: Type,
key: []const u8,
code: []const u8,
alt: bool,
ctrl: bool,
meta: bool,
shift: bool,
const Type = enum {
keydown,
};
};
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
if (kbe.type != .keydown) {
return;
}
const Document = @import("dom/document.zig").Document;
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
const event = try parser.keyboardEventCreate();
defer parser.keyboardEventDestroy(event);
try parser.keyboardEventInit(event, "keydown", .{
.bubbles = true,
.cancelable = true,
.key = kbe.key,
.code = kbe.code,
.alt = kbe.alt,
.ctrl = kbe.ctrl,
.meta = kbe.meta,
.shift = kbe.shift,
});
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
}
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("keydown_event_node", node);
self._keydownCallback(event) catch |err| {
log.err(.browser, "keydown handler error", .{ .err = err });
};
}
fn _keydownCallback(self: *Page, event: *parser.Event) !void {
const target = (try parser.eventTarget(event)) orelse return;
const node = parser.eventTargetToNode(target);
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
const kbe: *parser.KeyboardEvent = @ptrCast(event);
var new_key = try parser.keyboardEventGetKey(kbe);
if (std.mem.eql(u8, new_key, "Dead")) {
return;
}
switch (tag) {
.input => {
const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) {
if (std.mem.eql(u8, new_key, "Enter")) {
const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), null);
}
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.inputSetValue(@ptrCast(element), new_value);
}
},
.textarea => {
const value = try parser.textareaGetValue(@ptrCast(node));
if (std.mem.eql(u8, new_key, "Enter")) {
new_key = "\n";
}
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.textareaSetValue(@ptrCast(node), new_value);
},
else => {},
}
}
// As such we schedule the function to be called as soon as possible. // As such we schedule the function to be called as soon as possible.
// The page.arena is safe to use here, but the transfer_arena exists // The page.arena is safe to use here, but the transfer_arena exists
// specifically for this type of lifetime. // specifically for this type of lifetime.
@@ -656,7 +746,6 @@ pub const Page = struct {
} else { } else {
action = try URL.concatQueryString(transfer_arena, action, buf.items); action = try URL.concatQueryString(transfer_arena, action, buf.items);
} }
try self.navigateFromWebAPI(action, opts); try self.navigateFromWebAPI(action, opts);
} }

View File

@@ -137,7 +137,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element))); const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)));
switch (tag) { switch (tag) {
.input => { .input => {
const tpe = try parser.elementGetAttribute(element, "type") orelse ""; const tpe = try parser.inputGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(tpe, "image")) { if (std.ascii.eqlIgnoreCase(tpe, "image")) {
if (submitter_name_) |submitter_name| { if (submitter_name_) |submitter_name| {
if (std.mem.eql(u8, submitter_name, name)) { if (std.mem.eql(u8, submitter_name, name)) {
@@ -162,7 +162,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
} }
submitter_included = true; submitter_included = true;
} }
const value = (try parser.elementGetAttribute(element, "value")) orelse ""; const value = try parser.inputGetValue(@ptrCast(element));
try entries.appendOwned(arena, name, value); try entries.appendOwned(arena, name, value);
}, },
.select => { .select => {
@@ -189,11 +189,11 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
} }
if (submitter_included == false) { if (submitter_included == false) {
if (submitter_) |submitter| { if (submitter_name_) |submitter_name| {
// this can happen if the submitter is outside the form, but associated // this can happen if the submitter is outside the form, but associated
// with the form via a form=ID attribute // with the form via a form=ID attribute
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse ""; const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
try entries.appendOwned(arena, submitter_name_.?, value); try entries.appendOwned(arena, submitter_name, value);
} }
} }
@@ -249,7 +249,7 @@ fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 {
switch (tag) { switch (tag) {
.button => return name, .button => return name,
.input => { .input => {
const tpe = (try parser.elementGetAttribute(element, "type")) orelse ""; const tpe = try parser.inputGetType(@ptrCast(element));
// only an image type can be a sumbitter // only an image type can be a sumbitter
if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) { if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
return name; return name;

View File

@@ -21,14 +21,60 @@ const Page = @import("../../browser/page.zig").Page;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
dispatchKeyEvent,
dispatchMouseEvent, dispatchMouseEvent,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
.dispatchMouseEvent => return dispatchMouseEvent(cmd), .dispatchMouseEvent => return dispatchMouseEvent(cmd),
} }
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
fn dispatchKeyEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
type: Type,
key: []const u8 = "",
code: []const u8 = "",
modifiers: u4 = 0,
// Many optional parameters are not implemented yet, see documentation url.
const Type = enum {
keyDown,
keyUp,
rawKeyDown,
char,
};
})) orelse return error.InvalidParams;
try cmd.sendResult(null, .{});
// quickly ignore types we know we don't handle
switch (params.type) {
.keyUp, .rawKeyDown, .char => return,
.keyDown => {},
}
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
const keyboard_event = Page.KeyboardEvent{
.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);
// result already sent
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: anytype) !void { fn dispatchMouseEvent(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {