Skip to content

Commit bc341e9

Browse files
authored
Merge pull request #1041 from lightpanda-io/nikneym/keyboard-event
KeyboardEvent support
2 parents 66e403c + 80851f4 commit bc341e9

File tree

5 files changed

+306
-19
lines changed

5 files changed

+306
-19
lines changed

src/browser/events/event.zig

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,20 @@ const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
3333
const CustomEvent = @import("custom_event.zig").CustomEvent;
3434
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
3535
const MouseEvent = @import("mouse_event.zig").MouseEvent;
36+
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
3637
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
3738
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
3839

3940
// Event interfaces
40-
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
41+
pub const Interfaces = .{
42+
Event,
43+
CustomEvent,
44+
ProgressEvent,
45+
MouseEvent,
46+
KeyboardEvent,
47+
ErrorEvent,
48+
MessageEvent,
49+
};
4150

4251
pub const Union = generate.Union(Interfaces);
4352

@@ -63,6 +72,7 @@ pub const Event = struct {
6372
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
6473
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
6574
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
75+
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
6676
};
6777
}
6878

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const log = @import("../../log.zig");
21+
const builtin = @import("builtin");
22+
23+
const netsurf = @import("../netsurf.zig");
24+
const Event = @import("event.zig").Event;
25+
const JsObject = @import("../env.zig").JsObject;
26+
27+
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
28+
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
29+
const UIEvent = Event;
30+
31+
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
32+
pub const KeyboardEvent = struct {
33+
pub const Self = netsurf.KeyboardEvent;
34+
pub const prototype = *UIEvent;
35+
36+
pub const ConstructorOptions = struct {
37+
key: []const u8 = "",
38+
code: []const u8 = "",
39+
location: netsurf.KeyboardEventOpts.LocationCode = .standard,
40+
repeat: bool = false,
41+
isComposing: bool = false,
42+
// Currently not supported but we take as argument.
43+
charCode: u32 = 0,
44+
// Currently not supported but we take as argument.
45+
keyCode: u32 = 0,
46+
// Currently not supported but we take as argument.
47+
which: u32 = 0,
48+
ctrlKey: bool = false,
49+
shiftKey: bool = false,
50+
altKey: bool = false,
51+
metaKey: bool = false,
52+
};
53+
54+
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*netsurf.KeyboardEvent {
55+
const options: ConstructorOptions = maybe_options orelse .{};
56+
57+
var event = try netsurf.keyboardEventCreate();
58+
try netsurf.eventSetInternalType(@ptrCast(&event), .keyboard_event);
59+
60+
try netsurf.keyboardEventInit(
61+
event,
62+
event_type,
63+
.{
64+
.key = options.key,
65+
.code = options.code,
66+
.location = options.location,
67+
.repeat = options.repeat,
68+
.is_composing = options.isComposing,
69+
.ctrl_key = options.ctrlKey,
70+
.shift_key = options.shiftKey,
71+
.alt_key = options.altKey,
72+
.meta_key = options.metaKey,
73+
},
74+
);
75+
76+
return event;
77+
}
78+
79+
// Returns the modifier state for given modifier key.
80+
pub fn _getModifierState(self: *Self, key: []const u8) bool {
81+
// Chrome and Firefox do case-sensitive match, here we prefer the same.
82+
if (std.mem.eql(u8, key, "Alt")) {
83+
return get_altKey(self);
84+
}
85+
86+
if (std.mem.eql(u8, key, "AltGraph")) {
87+
return (get_altKey(self) and get_ctrlKey(self));
88+
}
89+
90+
if (std.mem.eql(u8, key, "Control")) {
91+
return get_ctrlKey(self);
92+
}
93+
94+
if (std.mem.eql(u8, key, "Shift")) {
95+
return get_shiftKey(self);
96+
}
97+
98+
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
99+
return get_metaKey(self);
100+
}
101+
102+
// Special case for IE.
103+
if (comptime builtin.os.tag == .windows) {
104+
if (std.mem.eql(u8, key, "Win")) {
105+
return get_metaKey(self);
106+
}
107+
}
108+
109+
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
110+
// event.getModifierState("Accel") returns true when at least one of
111+
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
112+
//
113+
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
114+
if (std.mem.eql(u8, key, "Accel")) {
115+
return (get_ctrlKey(self) or get_metaKey(self));
116+
}
117+
118+
// TODO: Add support for "CapsLock", "ScrollLock".
119+
return false;
120+
}
121+
122+
// Getters.
123+
124+
pub fn get_altKey(self: *Self) bool {
125+
return netsurf.keyboardEventKeyIsSet(self, .alt);
126+
}
127+
128+
pub fn get_ctrlKey(self: *Self) bool {
129+
return netsurf.keyboardEventKeyIsSet(self, .ctrl);
130+
}
131+
132+
pub fn get_metaKey(self: *Self) bool {
133+
return netsurf.keyboardEventKeyIsSet(self, .meta);
134+
}
135+
136+
pub fn get_shiftKey(self: *Self) bool {
137+
return netsurf.keyboardEventKeyIsSet(self, .shift);
138+
}
139+
140+
pub fn get_isComposing(self: *Self) bool {
141+
return self.is_composing;
142+
}
143+
144+
pub fn get_location(self: *Self) u32 {
145+
return self.location;
146+
}
147+
148+
pub fn get_key(self: *Self) ![]const u8 {
149+
return netsurf.keyboardEventGetKey(self);
150+
}
151+
152+
pub fn get_repeat(self: *Self) bool {
153+
return self.repeat;
154+
}
155+
};
156+
157+
const testing = @import("../../testing.zig");
158+
test "Browser: Events.Keyboard" {
159+
try testing.htmlRunner("events/keyboard.html");
160+
}

src/browser/netsurf.zig

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ pub const EventType = enum(u8) {
548548
abort_signal = 5,
549549
xhr_event = 6,
550550
message_event = 7,
551+
keyboard_event = 8,
551552
};
552553

553554
pub const MutationEvent = c.dom_mutation_event;
@@ -951,15 +952,44 @@ pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
951952
c._dom_keyboard_event_destroy(evt);
952953
}
953954

954-
const KeyboardEventOpts = struct {
955-
key: []const u8,
956-
code: []const u8,
955+
pub fn keyboardEventKeyIsSet(
956+
evt: *KeyboardEvent,
957+
comptime key: enum { ctrl, alt, shift, meta },
958+
) bool {
959+
var is_set: bool = false;
960+
const err = switch (key) {
961+
.ctrl => c._dom_keyboard_event_get_ctrl_key(evt, &is_set),
962+
.alt => c._dom_keyboard_event_get_alt_key(evt, &is_set),
963+
.shift => c._dom_keyboard_event_get_shift_key(evt, &is_set),
964+
.meta => c._dom_keyboard_event_get_meta_key(evt, &is_set),
965+
};
966+
// None of the earlier can fail.
967+
std.debug.assert(err == c.DOM_NO_ERR);
968+
969+
return is_set;
970+
}
971+
972+
pub const KeyboardEventOpts = struct {
973+
key: []const u8 = "",
974+
code: []const u8 = "",
975+
location: LocationCode = .standard,
976+
repeat: bool = false,
957977
bubbles: bool = false,
958978
cancelable: bool = false,
959-
ctrl: bool = false,
960-
alt: bool = false,
961-
shift: bool = false,
962-
meta: bool = false,
979+
is_composing: bool = false,
980+
ctrl_key: bool = false,
981+
alt_key: bool = false,
982+
shift_key: bool = false,
983+
meta_key: bool = false,
984+
985+
pub const LocationCode = enum(u32) {
986+
standard = c.DOM_KEY_LOCATION_STANDARD,
987+
left = c.DOM_KEY_LOCATION_LEFT,
988+
right = c.DOM_KEY_LOCATION_RIGHT,
989+
numpad = c.DOM_KEY_LOCATION_NUMPAD,
990+
mobile = 0x04, // Non-standard, deprecated.
991+
joystick = 0x05, // Non-standard, deprecated.
992+
};
963993
};
964994

965995
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
@@ -972,13 +1002,13 @@ pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEve
9721002
null, // dom_abstract_view* ?
9731003
try strFromData(opts.key),
9741004
try strFromData(opts.code),
975-
0, // location 0 == standard
976-
opts.ctrl,
977-
opts.shift,
978-
opts.alt,
979-
opts.meta,
980-
false, // repease
981-
false, // is_composiom
1005+
@intFromEnum(opts.location),
1006+
opts.ctrl_key,
1007+
opts.shift_key,
1008+
opts.alt_key,
1009+
opts.meta_key,
1010+
opts.repeat, // repease
1011+
opts.is_composing, // is_composiom
9821012
);
9831013
try DOMErr(err);
9841014
}

src/browser/page.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -942,10 +942,10 @@ pub const Page = struct {
942942
.cancelable = true,
943943
.key = kbe.key,
944944
.code = kbe.code,
945-
.alt = kbe.alt,
946-
.ctrl = kbe.ctrl,
947-
.meta = kbe.meta,
948-
.shift = kbe.shift,
945+
.alt_key = kbe.alt,
946+
.ctrl_key = kbe.ctrl,
947+
.meta_key = kbe.meta,
948+
.shift_key = kbe.shift,
949949
});
950950
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
951951
}

src/tests/events/keyboard.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script src="../testing.js"></script>
2+
3+
<script id=default>
4+
let event = new KeyboardEvent("test", { key: "a" });
5+
testing.expectEqual(true, event instanceof KeyboardEvent);
6+
testing.expectEqual(true, event instanceof Event);
7+
8+
testing.expectEqual("test", event.type);
9+
testing.expectEqual("a", event.key);
10+
11+
testing.expectEqual(0, event.location);
12+
testing.expectEqual(false, event.repeat);
13+
testing.expectEqual(false, event.isComposing);
14+
15+
testing.expectEqual(false, event.ctrlKey);
16+
testing.expectEqual(false, event.shiftKey);
17+
testing.expectEqual(false, event.metaKey);
18+
testing.expectEqual(false, event.altKey);
19+
</script>
20+
21+
<script id=getModifierState>
22+
event = new KeyboardEvent("test", {
23+
altKey: true,
24+
shiftKey: true,
25+
metaKey: true,
26+
ctrlKey: true,
27+
});
28+
29+
testing.expectEqual(true, event.getModifierState("Alt"));
30+
testing.expectEqual(true, event.getModifierState("AltGraph"));
31+
testing.expectEqual(true, event.getModifierState("Control"));
32+
testing.expectEqual(true, event.getModifierState("Shift"));
33+
testing.expectEqual(true, event.getModifierState("Meta"));
34+
testing.expectEqual(true, event.getModifierState("OS"));
35+
testing.expectEqual(true, event.getModifierState("Accel"));
36+
</script>
37+
38+
<script id=keyDownListener>
39+
event = new KeyboardEvent("keydown", { key: "z" });
40+
let isKeyDown = false;
41+
42+
document.addEventListener("keydown", (e) => {
43+
isKeyDown = true;
44+
45+
testing.expectEqual(true, e instanceof KeyboardEvent);
46+
testing.expectEqual(true, e instanceof Event);
47+
testing.expectEqual("z", event.key);
48+
});
49+
50+
document.dispatchEvent(event);
51+
52+
testing.expectEqual(true, isKeyDown);
53+
</script>
54+
55+
<script id=keyUpListener>
56+
event = new KeyboardEvent("keyup", { key: "x" });
57+
let isKeyUp = false;
58+
59+
document.addEventListener("keyup", (e) => {
60+
isKeyUp = true;
61+
62+
testing.expectEqual(true, e instanceof KeyboardEvent);
63+
testing.expectEqual(true, e instanceof Event);
64+
testing.expectEqual("x", event.key);
65+
});
66+
67+
document.dispatchEvent(event);
68+
69+
testing.expectEqual(true, isKeyUp);
70+
</script>
71+
72+
<script id=keyPressListener>
73+
event = new KeyboardEvent("keypress", { key: "w" });
74+
let isKeyPress = false;
75+
76+
document.addEventListener("keypress", (e) => {
77+
isKeyPress = true;
78+
79+
testing.expectEqual(true, e instanceof KeyboardEvent);
80+
testing.expectEqual(true, e instanceof Event);
81+
testing.expectEqual("w", event.key);
82+
});
83+
84+
document.dispatchEvent(event);
85+
86+
testing.expectEqual(true, isKeyPress);
87+
</script>

0 commit comments

Comments
 (0)