Skip to content

Commit 0de33b3

Browse files
authored
Merge pull request #773 from lightpanda-io/keydown_handling
Add basic support for key events
2 parents d9ce89a + cf39bdc commit 0de33b3

File tree

5 files changed

+223
-15
lines changed

5 files changed

+223
-15
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(@alignCast(@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(@alignCast(@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(@alignCast(@ptrCast(body)));
254+
return @alignCast(@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: 67 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
@cInclude("html/html_element.h");
3031
@cInclude("html/html_document.h");
@@ -864,6 +865,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
864865
return eventDefaultPrevented(@ptrCast(evt));
865866
}
866867

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

869923
pub const NodeType = enum(u4) {
@@ -2393,6 +2447,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
23932447
return strToData(s);
23942448
}
23952449

2450+
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
2451+
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
2452+
try DOMErr(err);
2453+
}
2454+
23962455
// Select
23972456
pub fn selectGetOptions(select: *Select) !*OptionCollection {
23982457
var collection: ?*OptionCollection = null;
@@ -2775,3 +2834,11 @@ pub fn inputSetValue(input: *Input, value: []const u8) !void {
27752834
const err = c.dom_html_input_element_set_value(input, try strFromData(value));
27762835
try DOMErr(err);
27772836
}
2837+
2838+
pub fn buttonGetType(button: *Button) ![]const u8 {
2839+
var s_: ?*String = null;
2840+
const err = c.dom_html_button_element_get_type(button, &s_);
2841+
try DOMErr(err);
2842+
const s = s_ orelse return "button";
2843+
return strToData(s);
2844+
}

src/browser/page.zig

Lines changed: 92 additions & 3 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,
@@ -307,6 +309,12 @@ pub const Page = struct {
307309
&self.window_clicked_event_node,
308310
false,
309311
);
312+
_ = try parser.eventTargetAddEventListener(
313+
parser.toEventTarget(parser.Element, document_element),
314+
"keydown",
315+
&self.keydown_event_node,
316+
false,
317+
);
310318

311319
// https://html.spec.whatwg.org/#read-html
312320

@@ -574,14 +582,14 @@ pub const Page = struct {
574582
},
575583
.input => {
576584
const element: *parser.Element = @ptrCast(node);
577-
const input_type = (try parser.elementGetAttribute(element, "type")) orelse return;
585+
const input_type = try parser.inputGetType(@ptrCast(element));
578586
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
579587
return self.elementSubmitForm(element);
580588
}
581589
},
582590
.button => {
583591
const element: *parser.Element = @ptrCast(node);
584-
const button_type = (try parser.elementGetAttribute(element, "type")) orelse return;
592+
const button_type = try parser.buttonGetType(@ptrCast(element));
585593
if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
586594
return self.elementSubmitForm(element);
587595
}
@@ -595,6 +603,88 @@ pub const Page = struct {
595603
}
596604
}
597605

606+
pub const KeyboardEvent = struct {
607+
type: Type,
608+
key: []const u8,
609+
code: []const u8,
610+
alt: bool,
611+
ctrl: bool,
612+
meta: bool,
613+
shift: bool,
614+
615+
const Type = enum {
616+
keydown,
617+
};
618+
};
619+
620+
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
621+
if (kbe.type != .keydown) {
622+
return;
623+
}
624+
625+
const Document = @import("dom/document.zig").Document;
626+
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
627+
628+
const event = try parser.keyboardEventCreate();
629+
defer parser.keyboardEventDestroy(event);
630+
try parser.keyboardEventInit(event, "keydown", .{
631+
.bubbles = true,
632+
.cancelable = true,
633+
.key = kbe.key,
634+
.code = kbe.code,
635+
.alt = kbe.alt,
636+
.ctrl = kbe.ctrl,
637+
.meta = kbe.meta,
638+
.shift = kbe.shift,
639+
});
640+
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
641+
}
642+
643+
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
644+
const self: *Page = @fieldParentPtr("keydown_event_node", node);
645+
self._keydownCallback(event) catch |err| {
646+
log.err(.browser, "keydown handler error", .{ .err = err });
647+
};
648+
}
649+
650+
fn _keydownCallback(self: *Page, event: *parser.Event) !void {
651+
const target = (try parser.eventTarget(event)) orelse return;
652+
const node = parser.eventTargetToNode(target);
653+
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
654+
655+
const kbe: *parser.KeyboardEvent = @ptrCast(event);
656+
var new_key = try parser.keyboardEventGetKey(kbe);
657+
if (std.mem.eql(u8, new_key, "Dead")) {
658+
return;
659+
}
660+
661+
switch (tag) {
662+
.input => {
663+
const element: *parser.Element = @ptrCast(node);
664+
const input_type = try parser.inputGetType(@ptrCast(element));
665+
if (std.mem.eql(u8, input_type, "text")) {
666+
if (std.mem.eql(u8, new_key, "Enter")) {
667+
const form = (try self.formForElement(element)) orelse return;
668+
return self.submitForm(@ptrCast(form), null);
669+
}
670+
671+
const value = try parser.inputGetValue(@ptrCast(element));
672+
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
673+
try parser.inputSetValue(@ptrCast(element), new_value);
674+
}
675+
},
676+
.textarea => {
677+
const value = try parser.textareaGetValue(@ptrCast(node));
678+
if (std.mem.eql(u8, new_key, "Enter")) {
679+
new_key = "\n";
680+
}
681+
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
682+
try parser.textareaSetValue(@ptrCast(node), new_value);
683+
},
684+
else => {},
685+
}
686+
}
687+
598688
// As such we schedule the function to be called as soon as possible.
599689
// The page.arena is safe to use here, but the transfer_arena exists
600690
// specifically for this type of lifetime.
@@ -656,7 +746,6 @@ pub const Page = struct {
656746
} else {
657747
action = try URL.concatQueryString(transfer_arena, action, buf.items);
658748
}
659-
660749
try self.navigateFromWebAPI(action, opts);
661750
}
662751

src/browser/xhr/form_data.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
137137
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)));
138138
switch (tag) {
139139
.input => {
140-
const tpe = try parser.elementGetAttribute(element, "type") orelse "";
140+
const tpe = try parser.inputGetType(@ptrCast(element));
141141
if (std.ascii.eqlIgnoreCase(tpe, "image")) {
142142
if (submitter_name_) |submitter_name| {
143143
if (std.mem.eql(u8, submitter_name, name)) {
@@ -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

@@ -249,7 +249,7 @@ fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 {
249249
switch (tag) {
250250
.button => return name,
251251
.input => {
252-
const tpe = (try parser.elementGetAttribute(element, "type")) orelse "";
252+
const tpe = try parser.inputGetType(@ptrCast(element));
253253
// only an image type can be a sumbitter
254254
if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
255255
return name;

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 = 0,
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 {

0 commit comments

Comments
 (0)