Skip to content

Commit 4b59e49

Browse files
committed
Add basic support for key events
Support CDP's Input.dispatchKeyEvent and DOM key events. Currently only keydown is supported and expects every key to be a displayable character. It turns out that manipulating the DOM via key events isn't great because the behavior really depends on the cursor. So, to do this more accurately, we'd have to introduce some concept of a cursor. Personally, I don't think we'll run into many pages that are purposefully using keyboard events. But driver (puppeteer/playwright) scripts might be another issue.
1 parent f12e9b6 commit 4b59e49

File tree

6 files changed

+199
-12
lines changed

6 files changed

+199
-12
lines changed

src/browser/dom/document.zig

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,17 +243,23 @@ pub const Document = struct {
243243
return try TreeWalker.init(root, what_to_show, filter);
244244
}
245245

246-
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
247-
const state = try page.getOrCreateNodeState(@ptrCast(self));
248-
if (state.active_element) |ae| {
249-
return try Element.toInterface(ae);
246+
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
247+
if (page.getNodeState(@ptrCast(self))) |state| {
248+
if (state.active_element) |ae| {
249+
return ae;
250+
}
250251
}
251252

252253
if (try parser.documentHTMLBody(page.window.document)) |body| {
253-
return try Element.toInterface(@ptrCast(body));
254+
return @ptrCast(body);
254255
}
255256

256-
return get_documentElement(self);
257+
return try parser.documentGetDocumentElement(self);
258+
}
259+
260+
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
261+
const ae = (try getActiveElement(self, page)) orelse return null;
262+
return try Element.toInterface(ae);
257263
}
258264

259265
// TODO: some elements can't be focused, like if they're disabled

src/browser/netsurf.zig

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const c = @cImport({
2525
@cInclude("events/event_target.h");
2626
@cInclude("events/event.h");
2727
@cInclude("events/mouse_event.h");
28+
@cInclude("events/keyboard_event.h");
2829
@cInclude("utils/validate.h");
2930
});
3031

@@ -862,6 +863,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
862863
return eventDefaultPrevented(@ptrCast(evt));
863864
}
864865

866+
// KeyboardEvent
867+
868+
pub const KeyboardEvent = c.dom_keyboard_event;
869+
870+
pub fn keyboardEventCreate() !*KeyboardEvent {
871+
var evt: ?*KeyboardEvent = undefined;
872+
const err = c._dom_keyboard_event_create(&evt);
873+
try DOMErr(err);
874+
return evt.?;
875+
}
876+
877+
pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
878+
c._dom_keyboard_event_destroy(evt);
879+
}
880+
881+
const KeyboardEventOpts = struct {
882+
key: []const u8,
883+
code: []const u8,
884+
bubbles: bool = false,
885+
cancelable: bool = false,
886+
ctrl: bool = false,
887+
alt: bool = false,
888+
shift: bool = false,
889+
meta: bool = false,
890+
};
891+
892+
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
893+
const s = try strFromData(typ);
894+
const err = c._dom_keyboard_event_init(
895+
evt,
896+
s,
897+
opts.bubbles,
898+
opts.cancelable,
899+
null, // dom_abstract_view* ?
900+
try strFromData(opts.key),
901+
try strFromData(opts.code),
902+
0, // location 0 == standard
903+
opts.ctrl,
904+
opts.shift,
905+
opts.alt,
906+
opts.meta,
907+
false, // repease
908+
false, // is_composiom
909+
);
910+
try DOMErr(err);
911+
}
912+
913+
pub fn keyboardEventGetKey(evt: *KeyboardEvent) ![]const u8 {
914+
var s: ?*String = undefined;
915+
_ = c._dom_keyboard_event_get_key(evt, &s);
916+
return strToData(s.?);
917+
}
918+
865919
// NodeType
866920

867921
pub const NodeType = enum(u4) {
@@ -2391,6 +2445,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
23912445
return strToData(s);
23922446
}
23932447

2448+
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
2449+
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
2450+
try DOMErr(err);
2451+
}
2452+
23942453
// Select
23952454
pub fn selectGetOptions(select: *Select) !*OptionCollection {
23962455
var collection: ?*OptionCollection = null;

src/browser/page.zig

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub const Page = struct {
8080

8181
microtask_node: Loop.CallbackNode,
8282

83+
keydown_event_node: parser.EventNode,
8384
window_clicked_event_node: parser.EventNode,
8485

8586
// Our JavaScript context for this specific page. This is what we use to
@@ -112,6 +113,7 @@ pub const Page = struct {
112113
.state_pool = &browser.state_pool,
113114
.cookie_jar = &session.cookie_jar,
114115
.microtask_node = .{ .func = microtaskCallback },
116+
.keydown_event_node = .{ .func = keydownCallback },
115117
.window_clicked_event_node = .{ .func = windowClicked },
116118
.request_factory = browser.http_client.requestFactory(.{
117119
.notification = browser.notification,
@@ -305,6 +307,12 @@ pub const Page = struct {
305307
&self.window_clicked_event_node,
306308
false,
307309
);
310+
_ = try parser.eventTargetAddEventListener(
311+
parser.toEventTarget(parser.Element, document_element),
312+
"keydown",
313+
&self.keydown_event_node,
314+
false,
315+
);
308316

309317
// https://html.spec.whatwg.org/#read-html
310318

@@ -593,6 +601,76 @@ pub const Page = struct {
593601
}
594602
}
595603

604+
pub const KeyboardEvent = struct {
605+
type: Type,
606+
key: []const u8,
607+
code: []const u8,
608+
alt: bool,
609+
ctrl: bool,
610+
meta: bool,
611+
shift: bool,
612+
613+
const Type = enum {
614+
keydown,
615+
};
616+
};
617+
618+
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
619+
if (kbe.type != .keydown) {
620+
return;
621+
}
622+
623+
const Document = @import("dom/document.zig").Document;
624+
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
625+
626+
const event = try parser.keyboardEventCreate();
627+
defer parser.keyboardEventDestroy(event);
628+
try parser.keyboardEventInit(event, "keydown", .{
629+
.bubbles = true,
630+
.cancelable = true,
631+
.key = kbe.key,
632+
.code = kbe.code,
633+
.alt = kbe.alt,
634+
.ctrl = kbe.ctrl,
635+
.meta = kbe.meta,
636+
.shift = kbe.shift,
637+
});
638+
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
639+
}
640+
641+
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
642+
const self: *Page = @fieldParentPtr("keydown_event_node", node);
643+
self._keydownCallback(event) catch |err| {
644+
log.err(.browser, "keydown handler error", .{ .err = err });
645+
};
646+
}
647+
648+
fn _keydownCallback(page: *Page, event: *parser.Event) !void {
649+
const kbe: *parser.KeyboardEvent = @ptrCast(event);
650+
const target = (try parser.eventTarget(event)) orelse return;
651+
const node = parser.eventTargetToNode(target);
652+
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
653+
switch (tag) {
654+
.input => {
655+
const element: *parser.Element = @ptrCast(node);
656+
const input_type = (try parser.elementGetAttribute(element, "type")) orelse "text";
657+
if (std.mem.eql(u8, input_type, "text")) {
658+
const value = try parser.inputGetValue(@ptrCast(element));
659+
const new_key = try parser.keyboardEventGetKey(kbe);
660+
const new_value = try std.mem.concat(page.arena, u8, &.{ value, new_key });
661+
try parser.inputSetValue(@ptrCast(element), new_value);
662+
}
663+
},
664+
.textarea => {
665+
const value = try parser.textareaGetValue(@ptrCast(node));
666+
const new_key = try parser.keyboardEventGetKey(kbe);
667+
const new_value = try std.mem.concat(page.arena, u8, &.{ value, new_key });
668+
try parser.textareaSetValue(@ptrCast(node), new_value);
669+
},
670+
else => {},
671+
}
672+
}
673+
596674
// As such we schedule the function to be called as soon as possible.
597675
// The page.arena is safe to use here, but the transfer_arena exists
598676
// specifically for this type of lifetime.

src/browser/xhr/form_data.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
162162
}
163163
submitter_included = true;
164164
}
165-
const value = (try parser.elementGetAttribute(element, "value")) orelse "";
165+
const value = try parser.inputGetValue(@ptrCast(element));
166166
try entries.appendOwned(arena, name, value);
167167
},
168168
.select => {
@@ -189,11 +189,11 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
189189
}
190190

191191
if (submitter_included == false) {
192-
if (submitter_) |submitter| {
192+
if (submitter_name_) |submitter_name| {
193193
// this can happen if the submitter is outside the form, but associated
194194
// with the form via a form=ID attribute
195-
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse "";
196-
try entries.appendOwned(arena, submitter_name_.?, value);
195+
const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
196+
try entries.appendOwned(arena, submitter_name, value);
197197
}
198198
}
199199

src/cdp/domains/input.zig

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,60 @@ const Page = @import("../../browser/page.zig").Page;
2121

2222
pub fn processMessage(cmd: anytype) !void {
2323
const action = std.meta.stringToEnum(enum {
24+
dispatchKeyEvent,
2425
dispatchMouseEvent,
2526
}, cmd.input.action) orelse return error.UnknownMethod;
2627

2728
switch (action) {
29+
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
2830
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
2931
}
3032
}
3133

34+
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
35+
fn dispatchKeyEvent(cmd: anytype) !void {
36+
const params = (try cmd.params(struct {
37+
type: Type,
38+
key: []const u8,
39+
code: []const u8,
40+
modifiers: u4,
41+
// Many optional parameters are not implemented yet, see documentation url.
42+
43+
const Type = enum {
44+
keyDown,
45+
keyUp,
46+
rawKeyDown,
47+
char,
48+
};
49+
})) orelse return error.InvalidParams;
50+
51+
try cmd.sendResult(null, .{});
52+
53+
// quickly ignore types we know we don't handle
54+
switch (params.type) {
55+
.keyUp, .rawKeyDown, .char => return,
56+
.keyDown => {},
57+
}
58+
59+
const bc = cmd.browser_context orelse return;
60+
const page = bc.session.currentPage() orelse return;
61+
62+
const keyboard_event = Page.KeyboardEvent{
63+
.key = params.key,
64+
.code = params.code,
65+
.type = switch (params.type) {
66+
.keyDown => .keydown,
67+
else => unreachable,
68+
},
69+
.alt = params.modifiers & 1 == 1,
70+
.ctrl = params.modifiers & 2 == 2,
71+
.meta = params.modifiers & 4 == 4,
72+
.shift = params.modifiers & 8 == 8,
73+
};
74+
try page.keyboardEvent(keyboard_event);
75+
// result already sent
76+
}
77+
3278
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
3379
fn dispatchMouseEvent(cmd: anytype) !void {
3480
const params = (try cmd.params(struct {

src/runtime/loop.zig

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ pub const Loop = struct {
127127
}
128128
}
129129

130-
131130
// JS callbacks APIs
132131
// -----------------
133132

@@ -255,7 +254,6 @@ pub const Loop = struct {
255254
}
256255
}.onConnect;
257256

258-
259257
const callback = try self.event_callback_pool.create();
260258
errdefer self.event_callback_pool.destroy(callback);
261259
callback.* = .{ .loop = self, .ctx = ctx };

0 commit comments

Comments
 (0)