add TextTrackCue and VTTCue (for reddit)

This commit is contained in:
Karl Seguin
2025-12-03 20:04:07 +08:00
parent 74ffc273ef
commit 60c1f19581
7 changed files with 385 additions and 1 deletions

View File

@@ -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);
},

View File

@@ -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")) {

View File

@@ -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"),

View File

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<script src="..//testing.js"></script>
<script id=basic>
{
// Test that VTTCue constructor exists
testing.expectEqual("function", typeof VTTCue);
testing.expectEqual("function", typeof TextTrackCue);
// Create a basic VTTCue
const cue = new VTTCue(0, 5, "Hello World");
testing.expectEqual(0, cue.startTime);
testing.expectEqual(5, cue.endTime);
testing.expectEqual("Hello World", cue.text);
}
{
// Test property setters
const cue = new VTTCue(1.5, 10.25, "Test text");
cue.id = "test-cue";
testing.expectEqual("test-cue", cue.id);
cue.startTime = 2.0;
testing.expectEqual(2.0, cue.startTime);
cue.text = "Modified text";
testing.expectEqual("Modified text", cue.text);
}
</script>
<script id=defaults>
{
// Test default values
const cue = new VTTCue(0, 1, "test");
testing.expectEqual("", cue.vertical);
testing.expectEqual(true, cue.snapToLines);
testing.expectEqual("auto", cue.line);
testing.expectEqual("auto", cue.position);
testing.expectEqual(100, cue.size);
testing.expectEqual("center", cue.align);
testing.expectEqual(false, cue.pauseOnExit);
}
</script>
<script id=properties>
{
// Test setting various properties
const cue = new VTTCue(0, 5, "test");
cue.vertical = "rl";
testing.expectEqual("rl", cue.vertical);
cue.snapToLines = false;
testing.expectEqual(false, cue.snapToLines);
cue.line = 10;
testing.expectEqual(10, cue.line);
cue.position = 50;
testing.expectEqual(50, cue.position);
cue.size = 75;
testing.expectEqual(75, cue.size);
cue.align = "left";
testing.expectEqual("left", cue.align);
cue.pauseOnExit = true;
testing.expectEqual(true, cue.pauseOnExit);
}
</script>

View File

@@ -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("<abort_signal>"),
.media_query_list => writer.writeAll("<MediaQueryList>"),
.message_port => writer.writeAll("<MessagePort>"),
.text_track_cue => writer.writeAll("<TextTrackCue>"),
};
}

View File

@@ -0,0 +1,118 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 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, .{});
};

View File

@@ -0,0 +1,182 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 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", .{});
}