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", .{}); +}