diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig
index a138c44f..408d6744 100644
--- a/src/browser/EventManager.zig
+++ b/src/browser/EventManager.zig
@@ -117,7 +117,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
switch (target._type) {
.node => |node| try self.dispatchNode(node, event, &was_handled),
- .xhr, .window, .abort_signal, .media_query_list, .message_port => {
+ .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue => {
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event, &was_handled);
},
diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index 2a4a0627..91e011ab 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -277,6 +277,15 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
).create(allocator, child);
}
+pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
+ const allocator = self._slab.allocator();
+ const TextTrackCue = @import("webapi/media/TextTrackCue.zig");
+
+ return try AutoPrototypeChain(
+ &.{ EventTarget, TextTrackCue, @TypeOf(child) },
+ ).create(allocator, child);
+}
+
fn hasChainRoot(comptime T: type) bool {
// Check if this is a root
if (@hasDecl(T, "_prototype_root")) {
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 1522c9d3..c3cb095d 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -565,6 +565,8 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
+ @import("../webapi/media/TextTrackCue.zig"),
+ @import("../webapi/media/VTTCue.zig"),
@import("../webapi/EventTarget.zig"),
@import("../webapi/Location.zig"),
@import("../webapi/Navigator.zig"),
diff --git a/src/browser/tests/media/vttcue.html b/src/browser/tests/media/vttcue.html
new file mode 100644
index 00000000..ad1d2cd4
--- /dev/null
+++ b/src/browser/tests/media/vttcue.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig
index 70aacb83..9792f2b3 100644
--- a/src/browser/webapi/EventTarget.zig
+++ b/src/browser/webapi/EventTarget.zig
@@ -36,6 +36,7 @@ pub const Type = union(enum) {
abort_signal: *@import("AbortSignal.zig"),
media_query_list: *@import("css/MediaQueryList.zig"),
message_port: *@import("MessagePort.zig"),
+ text_track_cue: *@import("media/TextTrackCue.zig"),
};
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
@@ -104,6 +105,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
.abort_signal => writer.writeAll(""),
.media_query_list => writer.writeAll(""),
.message_port => writer.writeAll(""),
+ .text_track_cue => writer.writeAll(""),
};
}
diff --git a/src/browser/webapi/media/TextTrackCue.zig b/src/browser/webapi/media/TextTrackCue.zig
new file mode 100644
index 00000000..e590fa7f
--- /dev/null
+++ b/src/browser/webapi/media/TextTrackCue.zig
@@ -0,0 +1,118 @@
+// 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 EventTarget = @import("../EventTarget.zig");
+
+const TextTrackCue = @This();
+
+_type: Type,
+_proto: *EventTarget,
+_id: []const u8 = "",
+_start_time: f64 = 0,
+_end_time: f64 = 0,
+_pause_on_exit: bool = false,
+_on_enter: ?js.Function = null,
+_on_exit: ?js.Function = null,
+
+pub const Type = union(enum) {
+ vtt: *@import("VTTCue.zig"),
+};
+
+pub fn asEventTarget(self: *TextTrackCue) *EventTarget {
+ return self._proto;
+}
+
+pub fn getId(self: *const TextTrackCue) []const u8 {
+ return self._id;
+}
+
+pub fn setId(self: *TextTrackCue, value: []const u8, page: *Page) !void {
+ self._id = try page.dupeString(value);
+}
+
+pub fn getStartTime(self: *const TextTrackCue) f64 {
+ return self._start_time;
+}
+
+pub fn setStartTime(self: *TextTrackCue, value: f64) void {
+ self._start_time = value;
+}
+
+pub fn getEndTime(self: *const TextTrackCue) f64 {
+ return self._end_time;
+}
+
+pub fn setEndTime(self: *TextTrackCue, value: f64) void {
+ self._end_time = value;
+}
+
+pub fn getPauseOnExit(self: *const TextTrackCue) bool {
+ return self._pause_on_exit;
+}
+
+pub fn setPauseOnExit(self: *TextTrackCue, value: bool) void {
+ self._pause_on_exit = value;
+}
+
+pub fn getOnEnter(self: *const TextTrackCue) ?js.Function {
+ return self._on_enter;
+}
+
+pub fn setOnEnter(self: *TextTrackCue, cb_: ?js.Function) !void {
+ if (cb_) |cb| {
+ self._on_enter = try cb.withThis(self);
+ } else {
+ self._on_enter = null;
+ }
+}
+
+pub fn getOnExit(self: *const TextTrackCue) ?js.Function {
+ return self._on_exit;
+}
+
+pub fn setOnExit(self: *TextTrackCue, cb_: ?js.Function) !void {
+ if (cb_) |cb| {
+ self._on_exit = try cb.withThis(self);
+ } else {
+ self._on_exit = null;
+ }
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(TextTrackCue);
+
+ pub const Meta = struct {
+ pub const name = "TextTrackCue";
+
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const Prototype = EventTarget;
+
+ pub const id = bridge.accessor(TextTrackCue.getId, TextTrackCue.setId, .{});
+ pub const startTime = bridge.accessor(TextTrackCue.getStartTime, TextTrackCue.setStartTime, .{});
+ pub const endTime = bridge.accessor(TextTrackCue.getEndTime, TextTrackCue.setEndTime, .{});
+ pub const pauseOnExit = bridge.accessor(TextTrackCue.getPauseOnExit, TextTrackCue.setPauseOnExit, .{});
+ pub const onenter = bridge.accessor(TextTrackCue.getOnEnter, TextTrackCue.setOnEnter, .{});
+ pub const onexit = bridge.accessor(TextTrackCue.getOnExit, TextTrackCue.setOnExit, .{});
+};
diff --git a/src/browser/webapi/media/VTTCue.zig b/src/browser/webapi/media/VTTCue.zig
new file mode 100644
index 00000000..de796a27
--- /dev/null
+++ b/src/browser/webapi/media/VTTCue.zig
@@ -0,0 +1,182 @@
+// 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 TextTrackCue = @import("TextTrackCue.zig");
+
+const VTTCue = @This();
+
+_proto: *TextTrackCue,
+_text: []const u8 = "",
+_region: ?js.Object = null,
+_vertical: []const u8 = "",
+_snap_to_lines: bool = true,
+_line: ?f64 = null, // null represents "auto"
+_position: ?f64 = null, // null represents "auto"
+_size: f64 = 100,
+_align: []const u8 = "center",
+
+pub fn constructor(start_time: f64, end_time: f64, text: []const u8, page: *Page) !*VTTCue {
+ const cue = try page._factory.textTrackCue(VTTCue{
+ ._proto = undefined,
+ ._text = try page.dupeString(text),
+ ._region = null,
+ ._vertical = "",
+ ._snap_to_lines = true,
+ ._line = null, // "auto"
+ ._position = null, // "auto"
+ ._size = 100,
+ ._align = "center",
+ });
+
+ cue._proto._start_time = start_time;
+ cue._proto._end_time = end_time;
+
+ return cue;
+}
+
+pub fn asTextTrackCue(self: *VTTCue) *TextTrackCue {
+ return self._proto;
+}
+
+pub fn getText(self: *const VTTCue) []const u8 {
+ return self._text;
+}
+
+pub fn setText(self: *VTTCue, value: []const u8, page: *Page) !void {
+ self._text = try page.dupeString(value);
+}
+
+pub fn getRegion(self: *const VTTCue) ?js.Object {
+ return self._region;
+}
+
+pub fn setRegion(self: *VTTCue, value: ?js.Object) !void {
+ if (value) |v| {
+ self._region = try v.persist();
+ } else {
+ self._region = null;
+ }
+}
+
+pub fn getVertical(self: *const VTTCue) []const u8 {
+ return self._vertical;
+}
+
+pub fn setVertical(self: *VTTCue, value: []const u8, page: *Page) !void {
+ // Valid values: "", "rl", "lr"
+ self._vertical = try page.dupeString(value);
+}
+
+pub fn getSnapToLines(self: *const VTTCue) bool {
+ return self._snap_to_lines;
+}
+
+pub fn setSnapToLines(self: *VTTCue, value: bool) void {
+ self._snap_to_lines = value;
+}
+
+pub const LineAndPositionSetting = union(enum) {
+ number: f64,
+ auto: []const u8,
+};
+
+pub fn getLine(self: *const VTTCue) LineAndPositionSetting {
+ if (self._line) |num| {
+ return .{ .number = num };
+ }
+ return .{ .auto = "auto" };
+}
+
+pub fn setLine(self: *VTTCue, value: LineAndPositionSetting) void {
+ switch (value) {
+ .number => |num| self._line = num,
+ .auto => self._line = null,
+ }
+}
+
+pub fn getPosition(self: *const VTTCue) LineAndPositionSetting {
+ if (self._position) |num| {
+ return .{ .number = num };
+ }
+ return .{ .auto = "auto" };
+}
+
+pub fn setPosition(self: *VTTCue, value: LineAndPositionSetting) void {
+ switch (value) {
+ .number => |num| self._position = num,
+ .auto => self._position = null,
+ }
+}
+
+pub fn getSize(self: *const VTTCue) f64 {
+ return self._size;
+}
+
+pub fn setSize(self: *VTTCue, value: f64) void {
+ self._size = value;
+}
+
+pub fn getAlign(self: *const VTTCue) []const u8 {
+ return self._align;
+}
+
+pub fn setAlign(self: *VTTCue, value: []const u8, page: *Page) !void {
+ // Valid values: "start", "center", "end", "left", "right"
+ self._align = try page.dupeString(value);
+}
+
+pub fn getCueAsHTML(self: *const VTTCue, page: *Page) !js.Object {
+ // Minimal implementation: return a document fragment
+ // In a full implementation, this would parse the VTT text into HTML nodes
+ _ = self;
+ _ = page;
+ return error.NotImplemented;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(VTTCue);
+
+ pub const Meta = struct {
+ pub const name = "VTTCue";
+
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const Prototype = TextTrackCue;
+
+ pub const constructor = bridge.constructor(VTTCue.constructor, .{});
+ pub const text = bridge.accessor(VTTCue.getText, VTTCue.setText, .{});
+ pub const region = bridge.accessor(VTTCue.getRegion, VTTCue.setRegion, .{});
+ pub const vertical = bridge.accessor(VTTCue.getVertical, VTTCue.setVertical, .{});
+ pub const snapToLines = bridge.accessor(VTTCue.getSnapToLines, VTTCue.setSnapToLines, .{});
+ pub const line = bridge.accessor(VTTCue.getLine, VTTCue.setLine, .{});
+ pub const position = bridge.accessor(VTTCue.getPosition, VTTCue.setPosition, .{});
+ pub const size = bridge.accessor(VTTCue.getSize, VTTCue.setSize, .{});
+ pub const @"align" = bridge.accessor(VTTCue.getAlign, VTTCue.setAlign, .{});
+ pub const getCueAsHTML = bridge.function(VTTCue.getCueAsHTML, .{});
+};
+
+const testing = @import("../../../testing.zig");
+test "WebApi: VTTCue" {
+ try testing.htmlRunner("media/vttcue.html", .{});
+}