Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,20 @@ const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
const CustomEvent = @import("custom_event.zig").CustomEvent;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const MouseEvent = @import("mouse_event.zig").MouseEvent;
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;

// Event interfaces
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
pub const Interfaces = .{
Event,
CustomEvent,
ProgressEvent,
MouseEvent,
KeyboardEvent,
ErrorEvent,
MessageEvent,
};

pub const Union = generate.Union(Interfaces);

Expand All @@ -63,6 +72,7 @@ pub const Event = struct {
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
};
}

Expand Down
160 changes: 160 additions & 0 deletions src/browser/events/keyboard_event.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const log = @import("../../log.zig");
const builtin = @import("builtin");

const netsurf = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;

// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
const UIEvent = Event;

// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
pub const KeyboardEvent = struct {
pub const Self = netsurf.KeyboardEvent;
pub const prototype = *UIEvent;

pub const ConstructorOptions = struct {
key: []const u8 = "",
code: []const u8 = "",
location: netsurf.KeyboardEventOpts.LocationCode = .standard,
repeat: bool = false,
isComposing: bool = false,
// Currently not supported but we take as argument.
charCode: u32 = 0,
// Currently not supported but we take as argument.
keyCode: u32 = 0,
// Currently not supported but we take as argument.
which: u32 = 0,
ctrlKey: bool = false,
shiftKey: bool = false,
altKey: bool = false,
metaKey: bool = false,
};

pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*netsurf.KeyboardEvent {
const options: ConstructorOptions = maybe_options orelse .{};

var event = try netsurf.keyboardEventCreate();
try netsurf.eventSetInternalType(@ptrCast(&event), .keyboard_event);

try netsurf.keyboardEventInit(
event,
event_type,
.{
.key = options.key,
.code = options.code,
.location = options.location,
.repeat = options.repeat,
.is_composing = options.isComposing,
.ctrl_key = options.ctrlKey,
.shift_key = options.shiftKey,
.alt_key = options.altKey,
.meta_key = options.metaKey,
},
);

return event;
}

// Returns the modifier state for given modifier key.
pub fn _getModifierState(self: *Self, key: []const u8) bool {
// Chrome and Firefox do case-sensitive match, here we prefer the same.
if (std.mem.eql(u8, key, "Alt")) {
return get_altKey(self);
}

if (std.mem.eql(u8, key, "AltGraph")) {
return (get_altKey(self) and get_ctrlKey(self));
}

if (std.mem.eql(u8, key, "Control")) {
return get_ctrlKey(self);
}

if (std.mem.eql(u8, key, "Shift")) {
return get_shiftKey(self);
}

if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
return get_metaKey(self);
}

// Special case for IE.
if (comptime builtin.os.tag == .windows) {
if (std.mem.eql(u8, key, "Win")) {
return get_metaKey(self);
}
}

// getModifierState() also accepts a deprecated virtual modifier named "Accel".
// event.getModifierState("Accel") returns true when at least one of
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
//
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
if (std.mem.eql(u8, key, "Accel")) {
return (get_ctrlKey(self) or get_metaKey(self));
}

// TODO: Add support for "CapsLock", "ScrollLock".
return false;
}

// Getters.

pub fn get_altKey(self: *Self) bool {
return netsurf.keyboardEventKeyIsSet(self, .alt);
}

pub fn get_ctrlKey(self: *Self) bool {
return netsurf.keyboardEventKeyIsSet(self, .ctrl);
}

pub fn get_metaKey(self: *Self) bool {
return netsurf.keyboardEventKeyIsSet(self, .meta);
}

pub fn get_shiftKey(self: *Self) bool {
return netsurf.keyboardEventKeyIsSet(self, .shift);
}

pub fn get_isComposing(self: *Self) bool {
return self.is_composing;
}

pub fn get_location(self: *Self) u32 {
return self.location;
}

pub fn get_key(self: *Self) ![]const u8 {
return netsurf.keyboardEventGetKey(self);
}

pub fn get_repeat(self: *Self) bool {
return self.repeat;
}
};

const testing = @import("../../testing.zig");
test "Browser: Events.Keyboard" {
try testing.htmlRunner("events/keyboard.html");
}
58 changes: 44 additions & 14 deletions src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ pub const EventType = enum(u8) {
abort_signal = 5,
xhr_event = 6,
message_event = 7,
keyboard_event = 8,
};

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

const KeyboardEventOpts = struct {
key: []const u8,
code: []const u8,
pub fn keyboardEventKeyIsSet(
evt: *KeyboardEvent,
comptime key: enum { ctrl, alt, shift, meta },
) bool {
var is_set: bool = false;
const err = switch (key) {
.ctrl => c._dom_keyboard_event_get_ctrl_key(evt, &is_set),
.alt => c._dom_keyboard_event_get_alt_key(evt, &is_set),
.shift => c._dom_keyboard_event_get_shift_key(evt, &is_set),
.meta => c._dom_keyboard_event_get_meta_key(evt, &is_set),
};
// None of the earlier can fail.
std.debug.assert(err == c.DOM_NO_ERR);

return is_set;
}

pub const KeyboardEventOpts = struct {
key: []const u8 = "",
code: []const u8 = "",
location: LocationCode = .standard,
repeat: bool = false,
bubbles: bool = false,
cancelable: bool = false,
ctrl: bool = false,
alt: bool = false,
shift: bool = false,
meta: bool = false,
is_composing: bool = false,
ctrl_key: bool = false,
alt_key: bool = false,
shift_key: bool = false,
meta_key: bool = false,

pub const LocationCode = enum(u32) {
standard = c.DOM_KEY_LOCATION_STANDARD,
left = c.DOM_KEY_LOCATION_LEFT,
right = c.DOM_KEY_LOCATION_RIGHT,
numpad = c.DOM_KEY_LOCATION_NUMPAD,
mobile = 0x04, // Non-standard, deprecated.
joystick = 0x05, // Non-standard, deprecated.
};
};

pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
Expand All @@ -972,13 +1002,13 @@ pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEve
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
@intFromEnum(opts.location),
opts.ctrl_key,
opts.shift_key,
opts.alt_key,
opts.meta_key,
opts.repeat, // repease
opts.is_composing, // is_composiom
);
try DOMErr(err);
}
Expand Down
8 changes: 4 additions & 4 deletions src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -942,10 +942,10 @@ pub const Page = struct {
.cancelable = true,
.key = kbe.key,
.code = kbe.code,
.alt = kbe.alt,
.ctrl = kbe.ctrl,
.meta = kbe.meta,
.shift = kbe.shift,
.alt_key = kbe.alt,
.ctrl_key = kbe.ctrl,
.meta_key = kbe.meta,
.shift_key = kbe.shift,
});
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
}
Expand Down
87 changes: 87 additions & 0 deletions src/tests/events/keyboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script src="../testing.js"></script>

<script id=default>
let event = new KeyboardEvent("test", { key: "a" });
testing.expectEqual(true, event instanceof KeyboardEvent);
testing.expectEqual(true, event instanceof Event);

testing.expectEqual("test", event.type);
testing.expectEqual("a", event.key);

testing.expectEqual(0, event.location);
testing.expectEqual(false, event.repeat);
testing.expectEqual(false, event.isComposing);

testing.expectEqual(false, event.ctrlKey);
testing.expectEqual(false, event.shiftKey);
testing.expectEqual(false, event.metaKey);
testing.expectEqual(false, event.altKey);
</script>

<script id=getModifierState>
event = new KeyboardEvent("test", {
altKey: true,
shiftKey: true,
metaKey: true,
ctrlKey: true,
});

testing.expectEqual(true, event.getModifierState("Alt"));
testing.expectEqual(true, event.getModifierState("AltGraph"));
testing.expectEqual(true, event.getModifierState("Control"));
testing.expectEqual(true, event.getModifierState("Shift"));
testing.expectEqual(true, event.getModifierState("Meta"));
testing.expectEqual(true, event.getModifierState("OS"));
testing.expectEqual(true, event.getModifierState("Accel"));
</script>

<script id=keyDownListener>
event = new KeyboardEvent("keydown", { key: "z" });
let isKeyDown = false;

document.addEventListener("keydown", (e) => {
isKeyDown = true;

testing.expectEqual(true, e instanceof KeyboardEvent);
testing.expectEqual(true, e instanceof Event);
testing.expectEqual("z", event.key);
});

document.dispatchEvent(event);

testing.expectEqual(true, isKeyDown);
</script>

<script id=keyUpListener>
event = new KeyboardEvent("keyup", { key: "x" });
let isKeyUp = false;

document.addEventListener("keyup", (e) => {
isKeyUp = true;

testing.expectEqual(true, e instanceof KeyboardEvent);
testing.expectEqual(true, e instanceof Event);
testing.expectEqual("x", event.key);
});

document.dispatchEvent(event);

testing.expectEqual(true, isKeyUp);
</script>

<script id=keyPressListener>
event = new KeyboardEvent("keypress", { key: "w" });
let isKeyPress = false;

document.addEventListener("keypress", (e) => {
isKeyPress = true;

testing.expectEqual(true, e instanceof KeyboardEvent);
testing.expectEqual(true, e instanceof Event);
testing.expectEqual("w", event.key);
});

document.dispatchEvent(event);

testing.expectEqual(true, isKeyPress);
</script>