Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src/browser/dom/Animation.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (C) 2023-2025s Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// 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 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" },
}, .{});
}
1 change: 1 addition & 0 deletions src/browser/dom/dom.zig
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ pub const Interfaces = .{
@import("performance.zig").Interfaces,
PerformanceObserver,
@import("range.zig").Interfaces,
@import("Animation.zig"),
};
9 changes: 9 additions & 0 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/browser/env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
38 changes: 38 additions & 0 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down