mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
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
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:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user