diff --git a/src/browser/dom/Animation.zig b/src/browser/dom/Animation.zig new file mode 100644 index 00000000..d3d2503e --- /dev/null +++ b/src/browser/dom/Animation.zig @@ -0,0 +1,126 @@ +// Copyright (C) 2023-2025s 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 Page = @import("../page.zig").Page; +const JsObject = @import("../env.zig").JsObject; +const Promise = @import("../env.zig").Promise; +const PromiseResolver = @import("../env.zig").PromiseResolver; + +const Animation = @This(); + +effect: ?JsObject, +timeline: ?JsObject, +ready_resolver: ?PromiseResolver, +finished_resolver: ?PromiseResolver, + +pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation { + return .{ + .effect = if (effect) |eo| try eo.persist() else null, + .timeline = if (timeline) |to| try to.persist() else null, + .ready_resolver = null, + .finished_resolver = null, + }; +} + +pub fn get_playState(self: *const Animation) []const u8 { + _ = self; + return "finished"; +} + +pub fn get_pending(self: *const Animation) bool { + _ = self; + return false; +} + +pub fn get_finished(self: *Animation, page: *Page) !Promise { + if (self.finished_resolver == null) { + const resolver = page.main_context.createPromiseResolver(); + try resolver.resolve(self); + self.finished_resolver = resolver; + } + return self.finished_resolver.?.promise(); +} + +pub fn get_ready(self: *Animation, page: *Page) !Promise { + // never resolved, because we're always "finished" + if (self.ready_resolver == null) { + const resolver = page.main_context.createPromiseResolver(); + self.ready_resolver = resolver; + } + return self.ready_resolver.?.promise(); +} + +pub fn get_effect(self: *const Animation) ?JsObject { + return self.effect; +} + +pub fn set_effect(self: *Animation, effect: JsObject) !void { + self.effect = try effect.persist(); +} + +pub fn get_timeline(self: *const Animation) ?JsObject { + return self.timeline; +} + +pub fn set_timeline(self: *Animation, timeline: JsObject) !void { + self.timeline = try timeline.persist(); +} + +pub fn _play(self: *const Animation) void { + _ = self; +} + +pub fn _pause(self: *const Animation) void { + _ = self; +} + +pub fn _cancel(self: *const Animation) void { + _ = self; +} + +pub fn _finish(self: *const Animation) void { + _ = self; +} + +pub fn _reverse(self: *const Animation) void { + _ = self; +} + +const testing = @import("../../testing.zig"); +test "Browser.Animation" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" }); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let a1 = document.createElement('div').animate(null, null)", null }, + .{ "a1.playState", "finished" }, + .{ "let cb = [];", null }, + .{ "a1.ready.then(() => { cb.push('ready') })", null }, + .{ + \\ a1.finished.then((x) => { + \\ cb.push('finished'); + \\ cb.push(x == a1); + \\ }) + , + null, + }, + .{ "cb", "finished,true" }, + }, .{}); +} diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index 8cc6d55e..2ce677a8 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -52,4 +52,5 @@ pub const Interfaces = .{ @import("performance.zig").Interfaces, PerformanceObserver, @import("range.zig").Interfaces, + @import("Animation.zig"), }; diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 8eab360a..ddfd4553 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -32,6 +32,9 @@ const NodeList = @import("nodelist.zig").NodeList; const HTMLElem = @import("../html/elements.zig"); const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; +const Animation = @import("Animation.zig"); +const JsObject = @import("../env.zig").JsObject; + pub const Union = @import("../html/elements.zig").Union; // WEB IDL https://dom.spec.whatwg.org/#element @@ -499,6 +502,12 @@ pub const Element = struct { } return sr; } + + pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation { + _ = self; + _ = opts; + return Animation.constructor(effect, null); + } }; // Tests diff --git a/src/browser/env.zig b/src/browser/env.zig index bb05e2e3..344e687e 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -41,5 +41,8 @@ const WebApis = struct { pub const JsThis = Env.JsThis; pub const JsObject = Env.JsObject; pub const Function = Env.Function; +pub const Promise = Env.Promise; +pub const PromiseResolver = Env.PromiseResolver; + pub const Env = js.Env(*Page, WebApis); pub const Global = @import("html/window.zig").Window; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 2dde71b2..fcd58b17 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1164,6 +1164,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } + pub fn createPromiseResolver(self: *JsContext) PromiseResolver { + return .{ + .js_context = self, + .resolver = v8.PromiseResolver.init(self.v8_context), + }; + } + // Probing is part of trying to map a JS value to a Zig union. There's // a lot of ambiguity in this process, in part because some JS values // can almost always be coerced. For example, anything can be coerced @@ -1871,6 +1878,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } + pub const PromiseResolver = struct { + js_context: *JsContext, + resolver: v8.PromiseResolver, + + pub fn promise(self: PromiseResolver) Promise { + return .{ + .promise = self.resolver.getPromise(), + }; + } + + pub fn resolve(self: PromiseResolver, value: anytype) !void { + const js_context = self.js_context; + const js_value = try js_context.zigValueToJs(value); + + // resolver.resolve will return null if the promise isn't pending + const ok = self.resolver.resolve(js_context.v8_context, js_value) orelse return; + if (!ok) { + return error.FailedToResolvePromise; + } + } + }; + + pub const Promise = struct { + promise: v8.Promise, + }; + pub const Inspector = struct { isolate: v8.Isolate, inner: *v8.Inspector, @@ -2475,6 +2508,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return value.js_obj.toValue(); } + if (T == Promise) { + // we're returning a v8.Promise + return value.promise.toObject().toValue(); + } + if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) { return isolate.throwException(value.inner); }