diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 2f9a073c..73c2a821 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -941,6 +941,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/ModelContext.zig"),
@import("../webapi/Navigator.zig"),
@import("../webapi/NavigatorUAData.zig"),
+ @import("../webapi/Notification.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),
diff --git a/src/browser/tests/canvas/canvas_rendering_context_2d.html b/src/browser/tests/canvas/canvas_rendering_context_2d.html
index 66a673ad..060ca6c8 100644
--- a/src/browser/tests/canvas/canvas_rendering_context_2d.html
+++ b/src/browser/tests/canvas/canvas_rendering_context_2d.html
@@ -158,3 +158,16 @@
testing.expectEqual(null, element.getContext('webgl'));
}
+
+
diff --git a/src/browser/tests/notification.html b/src/browser/tests/notification.html
new file mode 100644
index 00000000..667ef3d0
--- /dev/null
+++ b/src/browser/tests/notification.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig
index 845537cd..d5d07310 100644
--- a/src/browser/webapi/EventTarget.zig
+++ b/src/browser/webapi/EventTarget.zig
@@ -50,6 +50,7 @@ pub const Type = union(enum) {
font_face_set: *@import("css/FontFaceSet.zig"),
websocket: *@import("net/WebSocket.zig"),
cookie_store: *@import("storage/CookieStore.zig"),
+ notification: *@import("Notification.zig"),
};
pub fn init(page: *Page) !*EventTarget {
@@ -161,6 +162,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
.font_face_set => writer.writeAll(""),
.websocket => writer.writeAll(""),
.cookie_store => writer.writeAll(""),
+ .notification => writer.writeAll(""),
};
}
@@ -184,6 +186,7 @@ pub fn toString(self: *EventTarget) []const u8 {
.font_face_set => return "[object FontFaceSet]",
.websocket => return "[object WebSocket]",
.cookie_store => return "[object CookieStore]",
+ .notification => return "[object Notification]",
};
}
diff --git a/src/browser/webapi/Notification.zig b/src/browser/webapi/Notification.zig
new file mode 100644
index 00000000..c9ce3226
--- /dev/null
+++ b/src/browser/webapi/Notification.zig
@@ -0,0 +1,175 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const lp = @import("lightpanda");
+
+const js = @import("../js/js.zig");
+const Page = @import("../Page.zig");
+
+const EventTarget = @import("EventTarget.zig");
+
+const Execution = js.Execution;
+const Allocator = std.mem.Allocator;
+
+const Notification = @This();
+
+_rc: lp.RC(u8) = .{},
+_arena: Allocator,
+_proto: *EventTarget,
+_title: []const u8,
+_body: []const u8 = "",
+_icon: []const u8 = "",
+_image: []const u8 = "",
+_badge: []const u8 = "",
+_tag: []const u8 = "",
+_lang: []const u8 = "",
+_dir: []const u8 = "auto",
+_silent: bool = false,
+_require_interaction: bool = false,
+_renotify: bool = false,
+
+const Options = struct {
+ body: ?[]const u8 = null,
+ icon: ?[]const u8 = null,
+ image: ?[]const u8 = null,
+ badge: ?[]const u8 = null,
+ tag: ?[]const u8 = null,
+ lang: ?[]const u8 = null,
+ dir: ?[]const u8 = null,
+ silent: ?bool = null,
+ requireInteraction: ?bool = null,
+ renotify: ?bool = null,
+};
+
+pub fn init(title: []const u8, options_: ?Options, exec: *const Execution) !*Notification {
+ const arena = try exec.getArena(.small, "Notification");
+ errdefer exec.releaseArena(arena);
+
+ const options = options_ orelse Options{};
+ return exec._factory.eventTargetWithAllocator(arena, Notification{
+ ._arena = arena,
+ ._proto = undefined,
+ ._title = try arena.dupe(u8, title),
+ ._body = if (options.body) |v| try arena.dupe(u8, v) else "",
+ ._icon = if (options.icon) |v| try arena.dupe(u8, v) else "",
+ ._image = if (options.image) |v| try arena.dupe(u8, v) else "",
+ ._badge = if (options.badge) |v| try arena.dupe(u8, v) else "",
+ ._tag = if (options.tag) |v| try arena.dupe(u8, v) else "",
+ ._lang = if (options.lang) |v| try arena.dupe(u8, v) else "",
+ ._dir = if (options.dir) |d| try arena.dupe(u8, d) else "auto",
+ ._silent = options.silent orelse false,
+ ._require_interaction = options.requireInteraction orelse false,
+ ._renotify = options.renotify orelse false,
+ });
+}
+
+pub fn deinit(self: *Notification, page: *Page) void {
+ page.releaseArena(self._arena);
+}
+
+pub fn releaseRef(self: *Notification, page: *Page) void {
+ self._rc.release(self, page);
+}
+
+pub fn acquireRef(self: *Notification) void {
+ self._rc.acquire();
+}
+
+pub fn close(_: *Notification) void {}
+
+fn getPermission() []const u8 {
+ return "default";
+}
+
+fn getMaxActions() u32 {
+ return 2;
+}
+
+fn requestPermission(_: ?js.Function, exec: *const Execution) !js.Promise {
+ return exec.js.local.?.resolvePromise("default");
+}
+
+fn getTitle(self: *const Notification) []const u8 {
+ return self._title;
+}
+fn getBody(self: *const Notification) []const u8 {
+ return self._body;
+}
+fn getIcon(self: *const Notification) []const u8 {
+ return self._icon;
+}
+fn getImage(self: *const Notification) []const u8 {
+ return self._image;
+}
+fn getBadge(self: *const Notification) []const u8 {
+ return self._badge;
+}
+fn getTag(self: *const Notification) []const u8 {
+ return self._tag;
+}
+fn getLang(self: *const Notification) []const u8 {
+ return self._lang;
+}
+fn getDir(self: *const Notification) []const u8 {
+ return self._dir;
+}
+fn getSilent(self: *const Notification) bool {
+ return self._silent;
+}
+fn getRequireInteraction(self: *const Notification) bool {
+ return self._require_interaction;
+}
+fn getRenotify(self: *const Notification) bool {
+ return self._renotify;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Notification);
+
+ pub const Meta = struct {
+ pub const name = "Notification";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(Notification.init, .{});
+
+ pub const permission = bridge.accessor(getPermission, null, .{ .static = true });
+ pub const maxActions = bridge.accessor(getMaxActions, null, .{ .static = true });
+ pub const requestPermission = bridge.function(Notification.requestPermission, .{ .static = true });
+
+ pub const close = bridge.function(Notification.close, .{ .noop = true });
+
+ pub const title = bridge.accessor(getTitle, null, .{});
+ pub const body = bridge.accessor(getBody, null, .{});
+ pub const icon = bridge.accessor(getIcon, null, .{});
+ pub const image = bridge.accessor(getImage, null, .{});
+ pub const badge = bridge.accessor(getBadge, null, .{});
+ pub const tag = bridge.accessor(getTag, null, .{});
+ pub const lang = bridge.accessor(getLang, null, .{});
+ pub const dir = bridge.accessor(getDir, null, .{});
+ pub const silent = bridge.accessor(getSilent, null, .{});
+ pub const requireInteraction = bridge.accessor(getRequireInteraction, null, .{});
+ pub const renotify = bridge.accessor(getRenotify, null, .{});
+};
+
+const testing = @import("../../testing.zig");
+test "WebApi: Notification" {
+ try testing.htmlRunner("notification.html", .{});
+}
diff --git a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig
index e1018c19..68d5ec6c 100644
--- a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig
+++ b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig
@@ -83,6 +83,10 @@ pub fn createImageData(
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
+// CanvasImageSource (HTMLImageElement, HTMLCanvasElement, ImageBitmap, ...) is
+// just taken as a js.Value for now since we don't use it, and that's much easier.
+pub fn drawImage(_: *const CanvasRenderingContext2D, _: js.Value, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
+
pub fn getImageData(
_: *const CanvasRenderingContext2D,
_: i32, // sx
@@ -150,6 +154,7 @@ pub const JsApi = struct {
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
+ pub const drawImage = bridge.function(CanvasRenderingContext2D.drawImage, .{ .noop = true });
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });