diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 0fd81e81..2c2d7710 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -1243,6 +1243,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
attribute_iterator,
.{ ._proto = undefined },
),
+ asUint("canvas") => return self.createHtmlElementT(
+ Element.Html.Canvas,
+ namespace,
+ attribute_iterator,
+ .{ ._proto = undefined },
+ ),
asUint("dialog") => return self.createHtmlElementT(
Element.Html.Dialog,
namespace,
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 63a6d751..dc297b21 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -1122,8 +1122,18 @@ fn _debugValue(self: *const Context, js_val: v8.Value, seen: *std.AutoHashMapUnm
if (depth > 20) {
return writer.writeAll("...deeply nested object...");
}
+ const own_len = js_obj.getOwnPropertyNames(v8_context).length();
+ if (own_len == 0) {
+ const js_val_str = try self.valueToString(js_val, .{});
+ if (js_val_str.len > 2000) {
+ try writer.writeAll(js_val_str[0..2000]);
+ return writer.writeAll(" ... (truncated)");
+ }
+ return writer.writeAll(js_val_str);
+ }
- try writer.print("({d}/{d})", .{ js_obj.getOwnPropertyNames(v8_context).length(), js_obj.getPropertyNames(v8_context).length() });
+ const all_len = js_obj.getPropertyNames(v8_context).length();
+ try writer.print("({d}/{d})", .{ own_len, all_len });
for (0..len) |i| {
if (i == 0) {
try writer.writeByte('\n');
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 4059d6a8..d379ebf2 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -530,6 +530,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"),
+ @import("../webapi/element/html/Canvas.zig"),
@import("../webapi/element/html/Custom.zig"),
@import("../webapi/element/html/Data.zig"),
@import("../webapi/element/html/Dialog.zig"),
diff --git a/src/browser/tests/legacy/html/canvas.html b/src/browser/tests/legacy/html/canvas.html
index ab076487..ed1980eb 100644
--- a/src/browser/tests/legacy/html/canvas.html
+++ b/src/browser/tests/legacy/html/canvas.html
@@ -11,19 +11,3 @@
}
-
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 97a2ecb4..c47896eb 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -172,6 +172,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
.body => "body",
.br => "br",
.button => "button",
+ .canvas => "canvas",
.custom => |e| e._tag_name.str(),
.data => "data",
.dialog => "dialog",
@@ -224,6 +225,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
.body => "BODY",
.br => "BR",
.button => "BUTTON",
+ .canvas => "CANVAS",
.custom => |e| upperTagName(&e._tag_name, buf),
.data => "DATA",
.dialog => "DIALOG",
@@ -1082,6 +1084,7 @@ pub fn getTag(self: *const Element) Tag {
.img => .img,
.br => .br,
.button => .button,
+ .canvas => .canvas,
.heading => |h| h._tag,
.li => .li,
.ul => .ul,
@@ -1123,6 +1126,7 @@ pub const Tag = enum {
body,
br,
button,
+ canvas,
circle,
custom,
data,
diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig
index fe88e96c..8201a68e 100644
--- a/src/browser/webapi/element/Html.zig
+++ b/src/browser/webapi/element/Html.zig
@@ -27,6 +27,7 @@ pub const Anchor = @import("html/Anchor.zig");
pub const Body = @import("html/Body.zig");
pub const BR = @import("html/BR.zig");
pub const Button = @import("html/Button.zig");
+pub const Canvas = @import("html/Canvas.zig");
pub const Custom = @import("html/Custom.zig");
pub const Data = @import("html/Data.zig");
pub const Dialog = @import("html/Dialog.zig");
@@ -74,6 +75,7 @@ pub const Type = union(enum) {
body: *Body,
br: *BR,
button: *Button,
+ canvas: *Canvas,
custom: *Custom,
data: *Data,
dialog: *Dialog,
@@ -126,6 +128,7 @@ pub fn className(self: *const HtmlElement) []const u8 {
.body => "[object HTMLBodyElement]",
.br => "[object HTMLBRElement]",
.button => "[object HTMLButtonElement]",
+ .canvas => "[object HTMLCanvasElement]",
.custom => "[object CUSTOM-TODO]",
.data => "[object HTMLDataElement]",
.dialog => "[object HTMLDialogElement]",
diff --git a/src/browser/webapi/element/html/Canvas.zig b/src/browser/webapi/element/html/Canvas.zig
new file mode 100644
index 00000000..eb60befd
--- /dev/null
+++ b/src/browser/webapi/element/html/Canvas.zig
@@ -0,0 +1,225 @@
+// Copyright (C) 2023-2025 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 js = @import("../../../js/js.zig");
+const Page = @import("../../../Page.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const HtmlElement = @import("../Html.zig");
+
+pub fn registerTypes() []const type {
+ return &.{
+ Canvas,
+ RenderingContext2D,
+ };
+}
+
+const Canvas = @This();
+_proto: *HtmlElement,
+
+pub const RenderingContext2D = struct {
+ pub fn save(_: *RenderingContext2D) void {}
+ pub fn restore(_: *RenderingContext2D) void {}
+
+ pub fn scale(_: *RenderingContext2D, _: f64, _: f64) void {}
+ pub fn rotate(_: *RenderingContext2D, _: f64) void {}
+ pub fn translate(_: *RenderingContext2D, _: f64, _: f64) void {}
+ pub fn transform(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn setTransform(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn resetTransform(_: *RenderingContext2D) void {}
+
+ pub fn getGlobalAlpha(_: *const RenderingContext2D) f64 {
+ return 1.0;
+ }
+ pub fn setGlobalAlpha(_: *RenderingContext2D, _: f64) void {}
+ pub fn getGlobalCompositeOperation(_: *const RenderingContext2D) []const u8 {
+ return "source-over";
+ }
+ pub fn setGlobalCompositeOperation(_: *RenderingContext2D, _: []const u8) void {}
+
+ pub fn getFillStyle(_: *const RenderingContext2D) []const u8 {
+ return "#000000";
+ }
+ pub fn setFillStyle(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn getStrokeStyle(_: *const RenderingContext2D) []const u8 {
+ return "#000000";
+ }
+ pub fn setStrokeStyle(_: *RenderingContext2D, _: []const u8) void {}
+
+ pub fn getLineWidth(_: *const RenderingContext2D) f64 {
+ return 1.0;
+ }
+ pub fn setLineWidth(_: *RenderingContext2D, _: f64) void {}
+ pub fn getLineCap(_: *const RenderingContext2D) []const u8 {
+ return "butt";
+ }
+ pub fn setLineCap(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn getLineJoin(_: *const RenderingContext2D) []const u8 {
+ return "miter";
+ }
+ pub fn setLineJoin(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn getMiterLimit(_: *const RenderingContext2D) f64 {
+ return 10.0;
+ }
+ pub fn setMiterLimit(_: *RenderingContext2D, _: f64) void {}
+
+ pub fn clearRect(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn fillRect(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn strokeRect(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
+
+ pub fn beginPath(_: *RenderingContext2D) void {}
+ pub fn closePath(_: *RenderingContext2D) void {}
+ pub fn moveTo(_: *RenderingContext2D, _: f64, _: f64) void {}
+ pub fn lineTo(_: *RenderingContext2D, _: f64, _: f64) void {}
+ pub fn quadraticCurveTo(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn bezierCurveTo(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn arc(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}
+ pub fn arcTo(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
+ pub fn rect(_: *RenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
+
+ pub fn fill(_: *RenderingContext2D) void {}
+ pub fn stroke(_: *RenderingContext2D) void {}
+ pub fn clip(_: *RenderingContext2D) void {}
+
+ pub fn getFont(_: *const RenderingContext2D) []const u8 {
+ return "10px sans-serif";
+ }
+ pub fn setFont(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn getTextAlign(_: *const RenderingContext2D) []const u8 {
+ return "start";
+ }
+ pub fn setTextAlign(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn getTextBaseline(_: *const RenderingContext2D) []const u8 {
+ return "alphabetic";
+ }
+ pub fn setTextBaseline(_: *RenderingContext2D, _: []const u8) void {}
+ pub fn fillText(_: *RenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
+ pub fn strokeText(_: *RenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
+
+ pub const JsApi = struct {
+ pub const bridge = js.Bridge(RenderingContext2D);
+
+ pub const Meta = struct {
+ pub const name = "CanvasRenderingContext2D";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const save = bridge.function(RenderingContext2D.save, .{});
+ pub const restore = bridge.function(RenderingContext2D.restore, .{});
+
+ pub const scale = bridge.function(RenderingContext2D.scale, .{});
+ pub const rotate = bridge.function(RenderingContext2D.rotate, .{});
+ pub const translate = bridge.function(RenderingContext2D.translate, .{});
+ pub const transform = bridge.function(RenderingContext2D.transform, .{});
+ pub const setTransform = bridge.function(RenderingContext2D.setTransform, .{});
+ pub const resetTransform = bridge.function(RenderingContext2D.resetTransform, .{});
+
+ pub const globalAlpha = bridge.accessor(RenderingContext2D.getGlobalAlpha, RenderingContext2D.setGlobalAlpha, .{});
+ pub const globalCompositeOperation = bridge.accessor(RenderingContext2D.getGlobalCompositeOperation, RenderingContext2D.setGlobalCompositeOperation, .{});
+
+ pub const fillStyle = bridge.accessor(RenderingContext2D.getFillStyle, RenderingContext2D.setFillStyle, .{});
+ pub const strokeStyle = bridge.accessor(RenderingContext2D.getStrokeStyle, RenderingContext2D.setStrokeStyle, .{});
+
+ pub const lineWidth = bridge.accessor(RenderingContext2D.getLineWidth, RenderingContext2D.setLineWidth, .{});
+ pub const lineCap = bridge.accessor(RenderingContext2D.getLineCap, RenderingContext2D.setLineCap, .{});
+ pub const lineJoin = bridge.accessor(RenderingContext2D.getLineJoin, RenderingContext2D.setLineJoin, .{});
+ pub const miterLimit = bridge.accessor(RenderingContext2D.getMiterLimit, RenderingContext2D.setMiterLimit, .{});
+
+ pub const clearRect = bridge.function(RenderingContext2D.clearRect, .{});
+ pub const fillRect = bridge.function(RenderingContext2D.fillRect, .{});
+ pub const strokeRect = bridge.function(RenderingContext2D.strokeRect, .{});
+
+ pub const beginPath = bridge.function(RenderingContext2D.beginPath, .{});
+ pub const closePath = bridge.function(RenderingContext2D.closePath, .{});
+ pub const moveTo = bridge.function(RenderingContext2D.moveTo, .{});
+ pub const lineTo = bridge.function(RenderingContext2D.lineTo, .{});
+ pub const quadraticCurveTo = bridge.function(RenderingContext2D.quadraticCurveTo, .{});
+ pub const bezierCurveTo = bridge.function(RenderingContext2D.bezierCurveTo, .{});
+ pub const arc = bridge.function(RenderingContext2D.arc, .{});
+ pub const arcTo = bridge.function(RenderingContext2D.arcTo, .{});
+ pub const rect = bridge.function(RenderingContext2D.rect, .{});
+
+ pub const fill = bridge.function(RenderingContext2D.fill, .{});
+ pub const stroke = bridge.function(RenderingContext2D.stroke, .{});
+ pub const clip = bridge.function(RenderingContext2D.clip, .{});
+
+ pub const font = bridge.accessor(RenderingContext2D.getFont, RenderingContext2D.setFont, .{});
+ pub const textAlign = bridge.accessor(RenderingContext2D.getTextAlign, RenderingContext2D.setTextAlign, .{});
+ pub const textBaseline = bridge.accessor(RenderingContext2D.getTextBaseline, RenderingContext2D.setTextBaseline, .{});
+ pub const fillText = bridge.function(RenderingContext2D.fillText, .{});
+ pub const strokeText = bridge.function(RenderingContext2D.strokeText, .{});
+ };
+};
+
+pub fn asElement(self: *Canvas) *Element {
+ return self._proto._proto;
+}
+pub fn asConstElement(self: *const Canvas) *const Element {
+ return self._proto._proto;
+}
+pub fn asNode(self: *Canvas) *Node {
+ return self.asElement().asNode();
+}
+
+pub fn getWidth(self: *const Canvas) u32 {
+ const attr = self.asConstElement().getAttributeSafe("width") orelse return 300;
+ return std.fmt.parseUnsigned(u32, attr, 10) catch 300;
+}
+
+pub fn setWidth(self: *Canvas, value: u32, page: *Page) !void {
+ const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
+ try self.asElement().setAttributeSafe("width", str, page);
+}
+
+pub fn getHeight(self: *const Canvas) u32 {
+ const attr = self.asConstElement().getAttributeSafe("height") orelse return 150;
+ return std.fmt.parseUnsigned(u32, attr, 10) catch 150;
+}
+
+pub fn setHeight(self: *Canvas, value: u32, page: *Page) !void {
+ const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
+ try self.asElement().setAttributeSafe("height", str, page);
+}
+
+pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?*RenderingContext2D {
+ _ = self;
+
+ if (!std.mem.eql(u8, context_type, "2d")) {
+ return null;
+ }
+
+ const ctx = try page.arena.create(RenderingContext2D);
+ ctx.* = .{};
+ return ctx;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Canvas);
+
+ pub const Meta = struct {
+ pub const name = "HTMLCanvasElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const width = bridge.accessor(Canvas.getWidth, Canvas.setWidth, .{});
+ pub const height = bridge.accessor(Canvas.getHeight, Canvas.setHeight, .{});
+ pub const getContext = bridge.function(Canvas.getContext, .{});
+};