fix(svg): dispatch native SMIL timing events

This commit is contained in:
ldm0
2026-10-11 18:06:48 +08:00
parent be1e6d61a8
commit ee092ec499
23 changed files with 1647 additions and 53 deletions
@@ -170,6 +170,7 @@ pub(crate) use self::css_runtime::{
pub(crate) use self::events::{
construct_native_touch_event, construct_original_event,
construct_original_page_transition_event, construct_original_storage_event_utf16,
construct_svg_time_event,
};
pub(crate) use self::touch_runtime::construct_native_touch;
pub(crate) use crypto::{
@@ -26,13 +26,14 @@ use super::{
interaction_event_source_getter, keyboard_event_init_callback, message_event_init_callback,
mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback,
submit_event_agent_invoked_getter_function, submit_event_respond_with_callback,
submit_event_submitter_getter_function, track_event_track_getter_function,
ui_event_init_callback, ui_event_pseudo_target_getter_function,
ui_event_which_getter_function,
submit_event_submitter_getter_function, time_event_init_callback,
track_event_track_getter_function, ui_event_init_callback,
ui_event_pseudo_target_getter_function, ui_event_which_getter_function,
},
selection_surface::document_get_selection_callback,
specs::ConstructorSpec,
};
use crate::util::v8str;
use crate::web_api_interfaces;
use crate::{native_bridge::document, window_host};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiFunctionTemplateDeclaration};
@@ -131,6 +132,19 @@ struct EventBaseTemplateMethodsDeclaration {
init_event: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::TimeEvent, enumerable, receiver)]
struct TimeEventTemplateDeclaration {
#[webapi(accessor_property, getter = event_platform_attribute_getter,
data = v8str(scope, "view"))]
view: (),
#[webapi(accessor_property, getter = event_value_attribute_getter,
data = v8str(scope, "detail"))]
detail: (),
#[webapi(method = "initTimeEvent", length = 1, callback = time_event_init_callback)]
init_time_event: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::BeforeUnloadEvent, enumerable, receiver)]
struct BeforeUnloadEventTemplateAccessorsDeclaration {
@@ -642,6 +656,7 @@ fn install_declaration<'s, D: WebApiFunctionTemplateDeclaration>(
fn event_template_declaration(interface: &str) -> Option<EventTemplateDeclaration> {
Some(match interface {
"Event" => EventTemplateDeclaration::new::<EventBaseTemplateMethodsDeclaration>(),
"TimeEvent" => EventTemplateDeclaration::new::<TimeEventTemplateDeclaration>(),
"BeforeUnloadEvent" => {
EventTemplateDeclaration::new::<BeforeUnloadEventTemplateAccessorsDeclaration>()
}
@@ -12,6 +12,7 @@ mod methods;
mod modifiers;
mod subclasses;
mod submit;
mod time;
mod ui;
mod value;
mod wrappers;
@@ -36,6 +37,8 @@ pub(crate) use subclasses::construct_native_touch_event;
pub(in crate::context_bootstrap) use subclasses::input_event_get_target_ranges_callback;
pub(in crate::context_bootstrap) use subclasses::interaction_event_source_getter;
pub(in crate::context_bootstrap) use subclasses::keyboard_event_init_callback;
pub(crate) use time::construct_svg_time_event;
pub(in crate::context_bootstrap) use time::time_event_init_callback;
pub(in crate::context_bootstrap) use ui::{
composition_event_init_callback, initialize_legacy_ui_event, ui_event_init_callback,
ui_event_which_getter_function,
@@ -22,6 +22,7 @@ pub(crate) fn event_backing<'s>(
event: v8::Local<'_, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let event = v8::Local::new(scope, event);
let event = moli_webapi_declare::web_api_object_target(scope, event).unwrap_or(event);
crate::util::get_private_object(scope, event, EVENT_BACKING_SLOT).unwrap_or(event)
}
@@ -0,0 +1,81 @@
//! TimeEvent instances are produced by the native SVG timeline. The interface
//! has no public constructor and is not a Document.createEvent legacy alias.
use super::{
define_event_property, initialize_event_object, mark_event_trusted, new_event_state,
new_event_wrapper, reinitialize_event_object,
};
use crate::{web_api_interfaces, webidl};
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "TimeEvent.initTimeEvent")]
struct InitTimeEventArgs<'s> {
#[webidl(required, converter = "raw")]
event_type: webidl::DomString16,
#[webidl(nullable, interface = web_api_interfaces::Window,
brand_check = super::super::is_window_receiver)]
view: Option<v8::Local<'s, v8::Object>>,
#[webidl(default = 0)]
detail: i32,
}
pub(in crate::context_bootstrap) fn time_event_init_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<InitTimeEventArgs>(scope, &args) else {
return;
};
let event = moli_webapi_declare::web_api_object_target(scope, args.this())
.expect("generated TimeEvent receiver validation");
if !reinitialize_event_object(scope, event, &parsed.event_type.0, false, false) {
return;
}
let view = parsed
.view
.map_or_else(|| v8::null(scope).into(), Into::into);
define_event_property(scope, event, "view", view);
define_event_property(
scope,
event,
"detail",
v8::Integer::new(scope, parsed.detail).into(),
);
}
pub(crate) fn construct_svg_time_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
kind: moli_svg::SvgAnimationEventKind,
) -> Option<v8::Local<'s, v8::Object>> {
let state = new_event_state(scope);
initialize_event_object(scope, state, kind.name(), false, false);
let view = scope.get_current_context().global(scope);
define_event_property(scope, state, "view", view.into());
let detail = match kind {
moli_svg::SvgAnimationEventKind::Repeat(iteration) => {
// TimeEvent.detail is a WebIDL long, including its modulo conversion.
let number = v8::Number::new(scope, iteration);
webidl::convert::<webidl::Long>(
scope,
number.into(),
webidl::Context::member("TimeEvent", "detail"),
)
.ok()?
.0
}
_ => 0,
};
define_event_property(
scope,
state,
"detail",
v8::Integer::new(scope, detail).into(),
);
web_api_interfaces::TimeEvent::DESCRIPTOR
.initialize(scope, state)
.ok()?;
let event = new_event_wrapper(scope, state)?;
mark_event_trusted(scope, event);
Some(event)
}
@@ -4,6 +4,10 @@ use crate::web_api_interfaces;
use std::collections::HashSet;
const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
ConstructorSpec {
interface: web_api_interfaces::TimeEvent::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::TrustedTypePolicy::DESCRIPTOR,
kind: ConstructorKind::TrustedTypePolicy,
@@ -162,7 +162,7 @@ fn add_instance<'s>(
offset: f32,
) {
if let Ok((runtime, handle)) = svg_receiver_runtime_and_handle(scope, args) {
unsafe { &*runtime }.add_svg_animation_instance(handle, begin, offset);
unsafe { &mut *runtime }.add_svg_animation_instance(handle, begin, offset);
}
}
@@ -226,7 +226,7 @@ fn set_current_time<'s>(
return;
};
if let Ok((runtime, handle)) = svg_receiver_runtime_and_handle(scope, &args) {
unsafe { &*runtime }.seek_svg_animations(handle, parsed.seconds);
unsafe { &mut *runtime }.seek_svg_animations(handle, parsed.seconds);
}
}
@@ -236,7 +236,7 @@ fn set_paused<'s>(
pause: bool,
) {
if let Ok((runtime, handle)) = svg_receiver_runtime_and_handle(scope, args) {
unsafe { &*runtime }.pause_svg_animations(handle, pause);
unsafe { &mut *runtime }.pause_svg_animations(handle, pause);
}
}
@@ -1890,6 +1890,7 @@ pub(crate) struct RuntimeMutationApplyResult {
changed_text_controls: Vec<(DomHandle, bool)>,
number_list_attribute_projections:
Vec<context_bootstrap::svg_runtime::NumberListAttributeProjection>,
svg_animation_documents: Vec<DomHandle>,
}
impl RuntimeMutationApplyResult {
@@ -1936,8 +1937,15 @@ pub(super) fn finish_runtime_mutation_effects(
font_face_use_roots,
changed_text_controls,
number_list_attribute_projections,
svg_animation_documents,
} = result;
for document in svg_animation_documents {
if unsafe { &*host_ptr }.document_ready_state_for_handle(document) == "complete" {
unsafe { &mut *host_ptr }.queue_svg_animation_document_update(document);
}
}
context_bootstrap::svg_runtime::apply_number_list_attribute_projections(
scope,
number_list_attribute_projections,
@@ -2354,6 +2362,11 @@ pub(super) fn prepare_runtime_mutation_effects(
font_face_use_roots,
changed_text_controls: Vec::new(),
number_list_attribute_projections: Vec::new(),
svg_animation_documents: if document_followups_deferred_to_parser_owner {
Vec::new()
} else {
JsContextHost::svg_animation_mutation_documents(dom_host, effects)
},
}
}
@@ -34,6 +34,7 @@ struct AnimationFrameProvider {
wake_pending: bool,
focus_fixup_target: Option<WindowDocumentTaskTarget>,
observer_target: Option<WindowDocumentTaskTarget>,
svg_target: Option<WindowDocumentTaskTarget>,
handles: Vec<u32>,
callbacks: HashMap<u32, WindowWebIdlCallbackFunction>,
}
@@ -86,6 +87,7 @@ impl AnimationFrameState {
wake_pending: false,
focus_fixup_target: None,
observer_target: None,
svg_target: None,
handles: Vec::new(),
callbacks: HashMap::new(),
});
@@ -195,6 +197,21 @@ impl JsContextHost {
self.queue_window_animation_frame_wake(scope, binding);
}
pub(super) fn queue_svg_animation_rendering_wake(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
target: WindowDocumentTaskTarget,
) {
let dispatch_scope = target.dispatch_scope();
let Some(owner) = self.current_window_execution_context_owner(dispatch_scope) else {
return;
};
self.animation_frames
.ensure_provider(owner, dispatch_scope)
.svg_target = Some(target);
self.ensure_document_rendering_wake(scope, owner, dispatch_scope);
}
pub(crate) fn request_window_animation_frame<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
@@ -284,9 +301,13 @@ impl JsContextHost {
if provider.observer_target != Some(target) {
provider.observer_target = None;
}
if provider.svg_target != Some(target) {
provider.svg_target = None;
}
if provider.callbacks.is_empty()
&& provider.focus_fixup_target.is_none()
&& provider.observer_target.is_none()
&& provider.svg_target.is_none()
{
provider.wake_pending = false;
provider.handles.clear();
@@ -329,15 +350,26 @@ impl JsContextHost {
invoked |= self.dispatch_authorized_post_parse_autofocus(scope, host_ptr, top);
}
let handles = self
let (handles, sample_svg) = self
.animation_frames
.provider_mut(owner)
.map(|provider| {
provider.wake_pending = false;
provider.observer_target = None;
std::mem::take(&mut provider.handles)
(
std::mem::take(&mut provider.handles),
provider.svg_target.take() == Some(target),
)
})
.unwrap_or_default();
if sample_svg {
invoked |= self.dispatch_authorized_svg_animation_events(
scope,
host_ptr,
target,
resolved.document_handle,
);
}
for handle in handles {
if !self.window_execution_context_owner_is_current(owner, dispatch_scope) {
self.animation_frames.discard(owner);
@@ -61,6 +61,24 @@ struct ReadyChildWindowEventListenerInvocation {
}
impl JsContextHost {
pub(super) fn child_window_has_event_listener(
&self,
target: ChildWindowEventTarget,
event_type: &(impl EventTypeKey + ?Sized),
) -> bool {
self.child_window_event_target_is_current(target)
&& self
.child_window_event_listeners
.get(&target.child_handle())
.and_then(|listeners| listeners.get(event_type))
.is_some_and(|listeners| {
listeners.iter().any(|entry| {
entry.local_window_id == Some(target.owner().local_window_id)
&& entry.callback_state.callback_id().is_some()
})
})
}
fn child_window_event_requires_runtime_dispatch(
&self,
handle: DomHandle,
@@ -10,6 +10,7 @@ use crate::{
pub(super) enum PendingRenderingUpdatePayload {
DocumentScrollEvents,
AnimationStartScan(EventTargetHandle),
SvgAnimationEvents(crate::document_runtime::DomHandle),
/// Flush a top-level Document's autofocus candidates after DOMContentLoaded.
/// The candidate is intentionally resolved at execution time.
PostParseAutofocus,
@@ -275,6 +276,9 @@ impl JsContextHost {
PendingRenderingUpdatePayload::AnimationStartScan(event_target) => {
self.dispatch_authorized_animation_start_scan(scope, host_ptr, target, event_target)
}
PendingRenderingUpdatePayload::SvgAnimationEvents(document) => {
self.dispatch_authorized_svg_animation_events(scope, host_ptr, target, document)
}
PendingRenderingUpdatePayload::PostParseAutofocus => {
self.dispatch_authorized_post_parse_autofocus(scope, host_ptr, target)
}
@@ -315,13 +319,17 @@ impl JsContextHost {
let Some(pending) = self.rendering_updates.remove(task_id) else {
return false;
};
if let PendingRenderingUpdatePayload::AnimationFrameCallbacks(owner) =
pending.into_payload()
{
// document.open() rotates the parser/lifecycle incarnation without
// replacing the Window's animation callback map. Retire the stale
// rendering entry and admit a new exact-Document entry explicitly.
self.publish_animation_frame_rendering_update(owner);
match pending.into_payload() {
PendingRenderingUpdatePayload::AnimationFrameCallbacks(owner) => {
// document.open() rotates the parser/lifecycle incarnation without
// replacing the Window's animation callback map. Retire the stale
// rendering entry and admit a new exact-Document entry explicitly.
self.publish_animation_frame_rendering_update(owner);
}
PendingRenderingUpdatePayload::SvgAnimationEvents(document) => {
self.discard_svg_document_events(document);
}
_ => {}
}
true
}
@@ -11,18 +11,32 @@ use style::values::{
};
use super::JsContextHost;
use crate::{document_runtime::DomHandle, native_bridge::document::SVG_NS};
use crate::{document_runtime::DomHandle, dom::native::DomHost, native_bridge::document::SVG_NS};
mod events;
fn is_animation_element(dom: &DomHost, handle: DomHandle) -> bool {
dom.node(handle).is_some_and(|node| {
node.namespace() == Some(SVG_NS)
&& matches!(
node.local_name(),
Some("animate" | "set" | "animateMotion" | "animateTransform")
)
})
}
pub(super) struct SvgSmilState {
epoch: Instant,
clocks: HashMap<DomHandle, SvgPresentationClock>,
instances: HashMap<DomHandle, SvgAnimationInstanceTimes>,
effects: HashMap<DomHandle, FragmentEffects>,
events: events::SvgEventState,
}
struct FragmentEffects {
version: u64,
targets: HashMap<DomHandle, Vec<DomHandle>>,
animations: Vec<DomHandle>,
}
impl Default for SvgSmilState {
@@ -32,6 +46,7 @@ impl Default for SvgSmilState {
clocks: HashMap::new(),
instances: HashMap::new(),
effects: HashMap::new(),
events: events::SvgEventState::default(),
}
}
}
@@ -43,6 +58,13 @@ impl SvgSmilState {
}
impl JsContextHost {
#[cfg(test)]
pub(crate) fn advance_svg_clocks_for_test(&self, elapsed: std::time::Duration) {
// Advance natural time without invoking the author-facing seek API or
// changing the event source's last sampled position.
self.svg_smil.borrow_mut().epoch -= elapsed;
}
/// All nested SVG elements in one SVG document fragment share its clock.
/// An intervening non-SVG element (for example in foreignObject) starts a
/// new fragment. Merely having an associated synthetic Window is not enough.
@@ -66,22 +88,32 @@ impl JsContextHost {
root
}
pub(crate) fn record_svg_document_begin(&self, document: DomHandle) {
pub(crate) fn record_svg_document_begin(&mut self, document: DomHandle) {
if self.window_endpoint_for_document(document).is_none() {
return;
}
let mut state = self.svg_smil.borrow_mut();
let now = state.now();
let mut started = Vec::new();
let mut nodes = vec![document];
while let Some(handle) = nodes.pop() {
if self.dom_host().node(handle).is_some_and(|node| {
node.namespace() == Some(SVG_NS) && node.local_name() == Some("svg")
}) && self.svg_animation_fragment(handle) == Some(handle)
{
state.clocks.entry(handle).or_default().start(now);
let clock = state.clocks.entry(handle).or_default();
if !clock.has_started() {
clock.start(now);
started.push(handle);
}
}
nodes.extend(self.dom_host().child_nodes(handle).unwrap_or_default());
}
drop(state);
for root in started {
self.initialize_svg_animation_event_positions(root);
}
self.queue_svg_animation_document_update(document);
}
fn with_svg_clock<R>(
@@ -115,8 +147,9 @@ impl JsContextHost {
.is_some_and(|root| self.with_svg_clock(root, |clock, _| clock.is_paused()))
}
pub(crate) fn pause_svg_animations(&self, handle: DomHandle, pause: bool) {
pub(crate) fn pause_svg_animations(&mut self, handle: DomHandle, pause: bool) {
if self.svg_animation_fragment(handle) == Some(handle) {
self.prepare_svg_fragment_events(handle);
self.with_svg_clock(handle, |clock, now| {
if pause {
clock.pause(now);
@@ -124,12 +157,22 @@ impl JsContextHost {
clock.unpause(now);
}
});
if let Some(document) = self.dom_host().owner_document_handle(handle) {
self.queue_svg_animation_document_update(document);
}
}
}
pub(crate) fn seek_svg_animations(&self, handle: DomHandle, seconds: f32) {
pub(crate) fn seek_svg_animations(&mut self, handle: DomHandle, seconds: f32) {
if self.svg_animation_fragment(handle) == Some(handle) {
self.prepare_svg_fragment_events(handle);
let before = self.svg_presentation_time(handle);
self.with_svg_clock(handle, |clock, now| clock.seek(now, f64::from(seconds)));
let after = self.svg_presentation_time(handle);
self.prepare_svg_seek_events(handle, before, after);
if let Some(document) = self.dom_host().owner_document_handle(handle) {
self.queue_svg_animation_document_update(document);
}
}
}
@@ -236,7 +279,7 @@ impl JsContextHost {
}
pub(crate) fn add_svg_animation_instance(
&self,
&mut self,
animation: DomHandle,
begin: bool,
offset: f32,
@@ -244,7 +287,23 @@ impl JsContextHost {
if self.svg_animation_fragment(animation).is_none() {
return;
}
let time = self.svg_presentation_time(animation) + f64::from(offset);
let root = self
.svg_animation_fragment(animation)
.expect("checked SVG fragment");
self.prepare_svg_fragment_events(root);
let current_time = self.svg_presentation_time(animation);
let timing = self.svg_animation_timing(animation);
let before = {
let state = self.svg_smil.borrow();
timing.active_interval(
state
.instances
.get(&animation)
.unwrap_or(&SvgAnimationInstanceTimes::default()),
current_time,
)
};
let time = current_time + f64::from(offset);
let mut state = self.svg_smil.borrow_mut();
let instances = state.instances.entry(animation).or_default();
let list = if begin {
@@ -255,6 +314,47 @@ impl JsContextHost {
if !list.contains(&time) {
list.push(time);
}
let after = timing.active_interval(instances, current_time);
drop(state);
self.prepare_svg_instance_events(root, animation, before, after, current_time);
if let Some(document) = self.dom_host().owner_document_handle(animation) {
self.queue_svg_animation_document_update(document);
}
}
fn ensure_svg_fragment_effects(&self, root: DomHandle) {
let dom = self.dom_host();
let version = dom.dom_version();
let mut state = self.svg_smil.borrow_mut();
if state
.effects
.get(&root)
.is_some_and(|effects| effects.version == version)
{
return;
}
let mut targets: HashMap<DomHandle, Vec<DomHandle>> = HashMap::new();
let mut animations = Vec::new();
let mut nodes = vec![root];
while let Some(handle) = nodes.pop() {
if is_animation_element(dom, handle)
&& self.svg_animation_fragment(handle) == Some(root)
{
animations.push(handle);
if let Some(target) = self.svg_animation_target(handle) {
targets.entry(target).or_default().push(handle);
}
}
nodes.extend(dom.child_handles_reversed(handle));
}
state.effects.insert(
root,
FragmentEffects {
version,
targets,
animations,
},
);
}
/// Native path sampling is applied to an observation's typed style, shared
@@ -278,35 +378,18 @@ impl JsContextHost {
return None;
}
let animations = {
let mut state = self.svg_smil.borrow_mut();
let version = dom.dom_version();
if state
.effects
.get(&root)
.is_none_or(|effects| effects.version != version)
{
let mut targets: HashMap<DomHandle, Vec<DomHandle>> = HashMap::new();
let mut nodes = vec![root];
while let Some(handle) = nodes.pop() {
if dom.node(handle).is_some_and(|node| {
node.namespace() == Some(SVG_NS)
&& matches!(node.local_name(), Some("animate" | "set"))
}) && self.svg_animation_fragment(handle) == Some(root)
&& let Some(target) = self.svg_animation_target(handle)
{
targets.entry(target).or_default().push(handle);
}
let children = dom.child_nodes(handle).unwrap_or_default();
nodes.extend(children.into_iter().rev());
}
state
.effects
.insert(root, FragmentEffects { version, targets });
}
self.ensure_svg_fragment_effects(root);
let state = self.svg_smil.borrow();
state.effects.get(&root)?.targets.get(&target)?.clone()
};
let mut result = None;
for animation in animations {
if !dom
.node(animation)
.is_some_and(|node| matches!(node.local_name(), Some("animate" | "set")))
{
continue;
}
if dom.get_attribute(animation, "attributeName").as_deref() != Some("d") {
continue;
}
@@ -0,0 +1,646 @@
//! Native SVG timing events use the Document rendering source. Timer wakes do
//! not dispatch script, and every event revalidates the exact Document owner.
use std::collections::{HashMap, VecDeque};
use moli_svg::{
SvgAnimationEvent, SvgAnimationEventKind, SvgAnimationEvents, SvgAnimationInterval,
};
use super::super::{
JsContextHost, OwnerDispatchScope, WindowDocumentTaskTarget,
rendering_updates::PendingRenderingUpdatePayload,
};
use crate::{
document_runtime::{DomHandle, EventTargetHandle},
dom::native::{DomHost, DomMutationEffects},
native_bridge::document::SVG_NS,
page_task_queue::RendererPageRenderingUpdateTaskKind,
};
#[derive(Default)]
pub(super) struct SvgEventState {
positions: HashMap<DomHandle, EventPosition>,
roots: HashMap<DomHandle, (u64, Vec<DomHandle>)>,
pending: HashMap<DomHandle, VecDeque<Vec<EventStream>>>,
}
#[derive(Clone, Copy)]
struct EventPosition {
document: DomHandle,
root: DomHandle,
time: f64,
}
struct EventStream {
animation: DomHandle,
iterator: EventIterator,
next: Option<SvgAnimationEvent>,
}
enum EventIterator {
Natural(SvgAnimationEvents),
Transition(std::vec::IntoIter<SvgAnimationEvent>),
}
impl EventStream {
fn peek(&mut self) -> Option<SvgAnimationEvent> {
if self.next.is_none() {
self.next = match &mut self.iterator {
EventIterator::Natural(events) => events.next(),
EventIterator::Transition(events) => events.next(),
};
}
self.next
}
fn next_non_repeat_time(&self) -> Option<f64> {
if let Some(event) = self.next
&& !matches!(event.kind, SvgAnimationEventKind::Repeat(_))
{
return Some(event.time);
}
match &self.iterator {
EventIterator::Natural(events) => events.next_non_repeat_time(),
EventIterator::Transition(events) => events.as_slice().first().map(|event| event.time),
}
}
fn skip_unobserved_repeats_before(&mut self, time: f64) {
self.next = None;
if let EventIterator::Natural(events) = &mut self.iterator {
events.skip_repeats_before(time);
}
}
}
impl JsContextHost {
pub(crate) fn svg_animation_mutation_documents(
dom: &DomHost,
effects: &DomMutationEffects,
) -> Vec<DomHandle> {
fn contains_animation(dom: &DomHost, root: DomHandle) -> bool {
let mut nodes = vec![root];
while let Some(handle) = nodes.pop() {
if super::is_animation_element(dom, handle) {
return true;
}
nodes.extend(dom.child_handles(handle));
}
false
}
let mut documents = Vec::new();
let attributes = effects
.style()
.attribute_mutations()
.iter()
.filter_map(|mutation| {
(dom.is_connected_to_document(mutation.target())
&& super::is_animation_element(dom, mutation.target())
&& mutation.namespace().is_none()
&& matches!(
mutation.local_name(),
"begin" | "end" | "dur" | "repeatCount" | "repeatDur" | "restart"
))
.then_some(mutation.target())
});
let child_lists = effects
.style()
.child_list_mutations()
.iter()
.filter_map(|mutation| {
(dom.is_connected_to_document(mutation.target())
&& mutation
.added_nodes()
.iter()
.chain(mutation.removed_nodes())
.any(|&node| contains_animation(dom, node)))
.then_some(mutation.target())
});
for target in attributes.chain(child_lists) {
if let Some(document) = dom.owner_document_handle(target)
&& !documents.contains(&document)
{
documents.push(document);
}
}
documents
}
fn svg_document_roots(&self, document: DomHandle) -> Vec<DomHandle> {
let version = self.dom_host().dom_version();
if let Some((cached_version, roots)) = self.svg_smil.borrow().events.roots.get(&document)
&& *cached_version == version
{
return roots.clone();
}
let mut roots = Vec::new();
let mut nodes = vec![document];
while let Some(handle) = nodes.pop() {
if self.dom_host().node(handle).is_some_and(|node| {
node.namespace() == Some(SVG_NS) && node.local_name() == Some("svg")
}) && self.svg_animation_fragment(handle) == Some(handle)
{
roots.push(handle);
}
nodes.extend(self.dom_host().child_handles_reversed(handle));
}
self.svg_smil
.borrow_mut()
.events
.roots
.insert(document, (version, roots.clone()));
roots
}
fn svg_fragment_animations(&self, root: DomHandle) -> Vec<DomHandle> {
self.ensure_svg_fragment_effects(root);
self.svg_smil
.borrow()
.effects
.get(&root)
.map(|effects| effects.animations.clone())
.unwrap_or_default()
}
pub(super) fn initialize_svg_animation_event_positions(&self, root: DomHandle) {
let Some(document) = self.dom_host().owner_document_handle(root) else {
return;
};
let animations = self.svg_fragment_animations(root);
let initial_time = self.with_svg_clock(root, |clock, _| clock.initial_time());
let mut streams = Vec::new();
let mut state = self.svg_smil.borrow_mut();
for animation in animations {
state.events.positions.insert(
animation,
EventPosition {
document,
root,
time: if initial_time == 0.0 {
-1.0
} else {
initial_time
},
},
);
if initial_time > 0.0 {
let timing = self.svg_animation_timing(animation);
let instances = state.instances.get(&animation).cloned().unwrap_or_default();
let active = timing.active_interval(&instances, initial_time);
let events = SvgAnimationEvent::active_transition(None, active, initial_time)
.collect::<Vec<_>>();
if !events.is_empty() {
streams.push(EventStream {
animation,
iterator: EventIterator::Transition(events.into_iter()),
next: None,
});
}
}
}
drop(state);
self.push_svg_event_batch(root, streams);
}
pub(crate) fn queue_svg_animation_document_update(&mut self, document: DomHandle) {
self.retire_disconnected_svg_animations(document);
let Some(endpoint) = self.window_endpoint_for_document(document) else {
return;
};
let Some(target) =
self.current_window_document_task_target_for_dispatch_scope(endpoint.dispatch_scope())
else {
return;
};
let mut has_animation = false;
for root in self.svg_document_roots(document) {
let was_started = self
.svg_smil
.borrow()
.clocks
.get(&root)
.is_some_and(|clock| clock.has_started());
let started = self
.with_svg_clock(root, |clock, now| {
let started = clock.has_started();
(started, clock.current_time(now))
})
.0;
if started {
if !was_started {
self.initialize_svg_animation_event_positions(root);
}
let animations = self.svg_fragment_animations(root);
has_animation |= !animations.is_empty();
}
}
if !has_animation
&& !self
.svg_smil
.borrow()
.events
.pending
.contains_key(&document)
{
return;
}
self.queue_rendering_update(
target,
RendererPageRenderingUpdateTaskKind::SvgAnimationEvents,
PendingRenderingUpdatePayload::SvgAnimationEvents(document),
);
}
fn push_svg_event_batch(&self, root: DomHandle, streams: Vec<EventStream>) {
if streams.is_empty() {
return;
}
if let Some(document) = self.dom_host().owner_document_handle(root) {
self.svg_smil
.borrow_mut()
.events
.pending
.entry(document)
.or_default()
.push_back(streams);
}
}
pub(super) fn prepare_svg_fragment_events(&self, root: DomHandle) {
let Some(document) = self.dom_host().owner_document_handle(root) else {
return;
};
let time = self.svg_presentation_time(root);
if !self.with_svg_clock(root, |clock, _| clock.has_started()) {
return;
}
let animations = self.svg_fragment_animations(root);
let mut streams = Vec::new();
for animation in animations {
let timing = self.svg_animation_timing(animation);
let mut state = self.svg_smil.borrow_mut();
let previous = state
.events
.positions
.get(&animation)
.copied()
.filter(|position| position.root == root && position.document == document);
let instances = state.instances.get(&animation).cloned().unwrap_or_default();
let iterator = match previous {
Some(previous) => {
EventIterator::Natural(timing.events_between(&instances, previous.time, time))
}
None => {
let after = timing.active_interval(&instances, time);
EventIterator::Transition(
SvgAnimationEvent::active_transition(None, after, time)
.collect::<Vec<_>>()
.into_iter(),
)
}
};
state.events.positions.insert(
animation,
EventPosition {
document,
root,
time,
},
);
drop(state);
let mut stream = EventStream {
animation,
iterator,
next: None,
};
if stream.peek().is_some() {
streams.push(stream);
}
}
self.push_svg_event_batch(root, streams);
}
pub(super) fn prepare_svg_seek_events(&self, root: DomHandle, before: f64, after: f64) {
let Some(document) = self.dom_host().owner_document_handle(root) else {
return;
};
if !self.with_svg_clock(root, |clock, _| clock.has_started()) {
return;
}
let mut streams = Vec::new();
for animation in self.svg_fragment_animations(root) {
let timing = self.svg_animation_timing(animation);
let mut state = self.svg_smil.borrow_mut();
let instances = state.instances.get(&animation).cloned().unwrap_or_default();
let events = SvgAnimationEvent::active_transition(
timing.active_interval(&instances, before),
timing.active_interval(&instances, after),
after,
)
.collect::<Vec<_>>();
state.events.positions.insert(
animation,
EventPosition {
document,
root,
time: after,
},
);
if !events.is_empty() {
streams.push(EventStream {
animation,
iterator: EventIterator::Transition(events.into_iter()),
next: None,
});
}
}
self.push_svg_event_batch(root, streams);
}
pub(super) fn prepare_svg_instance_events(
&self,
root: DomHandle,
animation: DomHandle,
before: Option<SvgAnimationInterval>,
after: Option<SvgAnimationInterval>,
time: f64,
) {
if !self.with_svg_clock(root, |clock, _| clock.has_started()) {
return;
}
let events = SvgAnimationEvent::active_transition(before, after, time).collect::<Vec<_>>();
self.push_svg_event_batch(
root,
if events.is_empty() {
Vec::new()
} else {
vec![EventStream {
animation,
iterator: EventIterator::Transition(events.into_iter()),
next: None,
}]
},
);
}
fn has_svg_repeat_listener(&self, animation: DomHandle) -> bool {
let path = self.build_propagation_path(EventTargetHandle::Node(animation), false);
let window = match self.owner_dispatch_scope_for_node(animation) {
Some(OwnerDispatchScope::Top) => EventTargetHandle::Window,
Some(OwnerDispatchScope::Child(handle)) => {
let Some(target) = self.current_child_window_event_target(handle) else {
return false;
};
EventTargetHandle::ChildWindow(target)
}
Some(OwnerDispatchScope::LightweightPopup(id)) => {
let Some(target) = self.current_popup_window_event_target(id) else {
return false;
};
EventTargetHandle::PopupWindow(target)
}
None => return false,
};
path.into_iter()
.map(|target| {
if target == EventTargetHandle::Window {
window
} else {
target
}
})
.any(|target| match target {
EventTargetHandle::ChildWindow(target) => {
self.child_window_has_event_listener(target, "repeatEvent")
}
_ => self.has_event_listener(target, "repeatEvent"),
})
}
fn take_svg_event(&self, document: DomHandle) -> Option<(DomHandle, SvgAnimationEvent)> {
let mut state = self.svg_smil.borrow_mut();
let batches = state.events.pending.get_mut(&document)?;
loop {
let Some(streams) = batches.front_mut() else {
state.events.pending.remove(&document);
return None;
};
let index = streams
.iter_mut()
.enumerate()
.filter_map(|(index, stream)| stream.peek().map(|event| (index, event)))
.min_by(|(a, first), (b, second)| {
first
.time
.total_cmp(&second.time)
.then(first.kind.order().cmp(&second.kind.order()))
.then(a.cmp(b))
})
.map(|(index, _)| index);
let Some(index) = index else {
batches.pop_front();
continue;
};
let event = streams[index].next.expect("selected event");
if matches!(event.kind, SvgAnimationEventKind::Repeat(_))
&& !self.has_svg_repeat_listener(streams[index].animation)
{
// Begin/end handlers can add a repeat listener. Do not skip
// beyond another stream's next opportunity to run author code.
let until = streams
.iter()
.filter_map(EventStream::next_non_repeat_time)
.min_by(f64::total_cmp)
.unwrap_or(f64::INFINITY);
streams[index].skip_unobserved_repeats_before(until);
continue;
}
let stream = &mut streams[index];
stream.next = None;
return Some((stream.animation, event));
}
}
fn svg_document_has_future_events(&self, document: DomHandle) -> bool {
if self
.svg_smil
.borrow()
.events
.pending
.contains_key(&document)
{
return true;
}
for root in self.svg_document_roots(document) {
let current_time = self.svg_presentation_time(root);
let through = if self.svg_animations_paused(root) {
current_time
} else {
f64::INFINITY
};
for animation in self.svg_fragment_animations(root) {
let timing = self.svg_animation_timing(animation);
let state = self.svg_smil.borrow();
// Callbacks and microtasks can cross a boundary after the
// rendering source sampled it. Such a boundary still needs a
// wake, even when it is now in the presentation clock's past.
let time = state
.events
.positions
.get(&animation)
.filter(|position| position.root == root && position.document == document)
.map_or(current_time, |position| position.time);
let instances = state.instances.get(&animation).cloned().unwrap_or_default();
if timing
.events_between(&instances, time, through)
.next()
.is_some()
{
return true;
}
}
}
false
}
pub(in crate::native_bridge::context_host) fn dispatch_authorized_svg_animation_events(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
target: WindowDocumentTaskTarget,
expected_document: DomHandle,
) -> bool {
let Some(resolved) = self.resolve_authorized_window_document_task_context(scope, target)
else {
self.discard_svg_document_events(expected_document);
return false;
};
if resolved.document_handle != expected_document {
self.discard_svg_document_events(expected_document);
return false;
}
let scope = &mut v8::ContextScope::new(scope, resolved.context);
let previous = target.dispatch_scope().enter(scope);
let watchdog = crate::v8_execution_watchdog::V8ExecutionWatchdog::arm(
crate::v8_execution_watchdog::V8ExecutionWatchdogKind::SvgAnimationEvents,
scope.thread_safe_handle(),
crate::v8_execution_watchdog::SCRIPT_TURN_WATCHDOG_TIMEOUT,
);
for root in self.svg_document_roots(expected_document) {
self.prepare_svg_fragment_events(root);
}
let mut invoked = false;
// Yield through the existing rendering source instead of monopolizing
// a script turn when a long clock jump has many observed repetitions.
for _ in 0..256 {
if scope.is_execution_terminating() {
break;
}
if !self.window_document_owner_is_current_for_dispatch_scope(
target.owner(),
target.dispatch_scope(),
) {
self.discard_svg_document_events(expected_document);
break;
}
let Some((animation, event)) = self.take_svg_event(expected_document) else {
break;
};
// Already queued boundaries keep their target after removal. The
// removal step terminates its interval and stops future sampling;
// its begin/end tasks still belong to this exact Document.
if self.dom_host().owner_document_handle(animation) != Some(expected_document) {
continue;
}
let Some(event) = crate::context_bootstrap::construct_svg_time_event(scope, event.kind)
else {
continue;
};
let _ = self.dispatch_public_event_best_effort(
scope,
host_ptr,
EventTargetHandle::Node(animation),
event,
"SVG timing event",
);
invoked = true;
}
if watchdog.disarm() == crate::v8_execution_watchdog::V8ExecutionWatchdogOutcome::TimedOut {
tracing::warn!("SVG timing events exceeded their execution deadline");
}
if self.window_document_owner_is_current_for_dispatch_scope(
target.owner(),
target.dispatch_scope(),
) && self.svg_document_has_future_events(expected_document)
{
self.queue_svg_animation_rendering_wake(scope, target);
}
target.dispatch_scope().restore(scope, previous);
invoked
}
pub(in crate::native_bridge::context_host) fn discard_svg_document_events(
&self,
document: DomHandle,
) {
let mut state = self.svg_smil.borrow_mut();
state.events.pending.remove(&document);
state.events.roots.remove(&document);
state
.events
.positions
.retain(|_, position| position.document != document);
}
fn retire_disconnected_svg_animations(&self, document: DomHandle) {
let current = |animation, root| {
self.dom_host().is_connected_to_document(animation)
&& self.dom_host().owner_document_handle(animation) == Some(document)
&& self.svg_animation_fragment(animation) == Some(root)
};
let retired: Vec<_> = self
.svg_smil
.borrow()
.events
.positions
.iter()
.filter(|&(&animation, position)| {
position.document == document && !current(animation, position.root)
})
.map(|(&animation, position)| (animation, *position))
.collect();
for (animation, position) in retired {
let time = self.svg_presentation_time(position.root);
let timing = self.svg_animation_timing(animation);
let mut state = self.svg_smil.borrow_mut();
let instances = state.instances.get(&animation).cloned().unwrap_or_default();
state.events.positions.remove(&animation);
let mut natural = EventStream {
animation,
iterator: EventIterator::Natural(timing.events_between(
&instances,
position.time,
time,
)),
next: None,
};
let active = timing.active_interval(&instances, time);
let end = SvgAnimationEvent::active_transition(active, None, time).collect::<Vec<_>>();
let batches = state.events.pending.entry(document).or_default();
if natural.peek().is_some() {
batches.push_back(vec![natural]);
}
if !end.is_empty() {
batches.push_back(vec![EventStream {
animation,
iterator: EventIterator::Transition(end.into_iter()),
next: None,
}]);
}
if batches.is_empty() {
state.events.pending.remove(&document);
}
}
}
}
@@ -35,6 +35,7 @@ impl RendererPageRenderingUpdateTaskId {
pub(crate) enum RendererPageRenderingUpdateTaskKind {
DocumentScrollEvents,
AnimationStartScan,
SvgAnimationEvents,
PostParseAutofocus,
AnimationFrameCallbacks,
IntersectionObserverDelivery,
@@ -48,7 +48,7 @@ impl ScriptVm {
self.document_runtime.set_document_ready_state(state);
if state == crate::dom::native::DocumentReadyState::Complete {
self._context_host
.borrow()
.borrow_mut()
.record_svg_document_begin(self.document_runtime.document_handle());
}
Ok(())
@@ -2347,6 +2347,7 @@ mod svg_path_data;
mod svg_redraw_methods;
mod svg_root_factory_receivers;
mod svg_smil_path_timing;
mod svg_smil_time_events;
mod svg_switch_mpath_interfaces;
mod svg_tree_queries;
mod svg_uri_reference;
@@ -0,0 +1,54 @@
(() => {
const check=(value,name)=>{if(!value)throw new Error(name)};
const event=nativeTimeEvent;
const realm=document.querySelector('iframe').contentWindow;
const init=realm.TimeEvent.prototype.initTimeEvent;
check(event instanceof TimeEvent && event instanceof Event,'native interfaces');
check(event.isTrusted && event.type==='repeatEvent' && event.detail===-1 && event.view===window,'native payload and long conversion');
check(Object.getPrototypeOf(event)===TimeEvent.prototype,'native prototype');
check(!Object.hasOwn(event,'view') && !Object.hasOwn(event,'detail'),'prototype attribute placement');
check(Object.getPrototypeOf(TimeEvent.prototype)===Event.prototype,'Event inheritance');
for(const R of [window,realm]){
try { new R.TimeEvent(); throw 42; } catch(error){check(Object.getPrototypeOf(error)===R.TypeError.prototype,'illegal constructor realm')}
}
let conversions=0,traps=0;
const text={toString(){conversions++;return 'changed'}};
const numeric={valueOf(){conversions++;return 7}};
const proxy=new Proxy(event,{get(){traps++;throw 42},getPrototypeOf(){traps++;throw 42}});
const revoked=Proxy.revocable(event,{});revoked.revoke();
for(const receiver of [{},Object.create(TimeEvent.prototype),Object.create(event),proxy,revoked.proxy]){
try { init.call(receiver,text,window,numeric); throw 42; }
catch(error){check(Object.getPrototypeOf(error)===realm.TypeError.prototype,'receiver error realm')}
}
check(conversions===0 && traps===0,'receiver validation before argument conversion');
const original={marker:1};
try { init.call(event,{toString(){throw original}},window,numeric);throw 42 }
catch(error){check(error===original,'original conversion exception')}
check(conversions===0 && event.type==='repeatEvent','conversion failure leaves state intact');
const stamp=event.timeStamp;
init.call(event,'x\ud800',realm,4294967295);
check(event.type==='x\ud800' && event.detail===-1 && event.view===realm,'UTF-16 and cross realm Window');
check(!event.isTrusted && event.timeStamp===stamp,'legacy initialization preserves creation time');
init.call(event,'again');
check(event.view===null && event.detail===0 && !event.bubbles && !event.cancelable,'optional argument defaults');
init.call(nativeTimeEventProxy,'native-proxy',realm,9);
check(event.type==='native-proxy' && event.view===realm && event.detail===9,'registered native Proxy identity');
const detail=Object.getOwnPropertyDescriptor(realm.TimeEvent.prototype,'detail').get;
check(detail.call(nativeTimeEventProxy)===9,'registered native Proxy accessor');
init.call(event,'again');
event.added=1;
const target=new EventTarget();
target.addEventListener('again',()=>{
init.call(event,text,window,numeric);
check(event.type==='again' && event.view===null && event.detail===0,'dispatching initialization is ignored');
});
target.dispatchEvent(event);
check(conversions===2,'dispatching events still convert arguments');
const Original=TimeEvent;
globalThis.TimeEvent=function(){throw 42};
try { check(event instanceof Original,'author replacement retains native identity') }
finally { globalThis.TimeEvent=Original }
try { document.createEvent('TimeEvent');throw 42 }
catch(error){check(error.name==='NotSupportedError','TimeEvent is not a legacy createEvent alias')}
return 'time-event:ok';
})()
@@ -0,0 +1,290 @@
use super::*;
use crate::network::ResourceRequestClient;
#[test]
fn native_time_events_preserve_brands_realms_and_legacy_argument_order() {
let mut vm = new_storage_page_task_executor_test_vm("https://svg-time-event.test/");
vm.eval("document.body.innerHTML='<iframe></iframe>'")
.unwrap();
let context = &vm.page_default_context as *const _;
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context, |scope, _| {
let event = crate::context_bootstrap::construct_svg_time_event(
scope,
moli_svg::SvgAnimationEventKind::Repeat(4294967295.0),
)
.unwrap();
let global = scope.get_current_context().global(scope);
assert_eq!(
global.create_data_property(
scope,
crate::util::v8str(scope, "nativeTimeEvent").into(),
event.into()
),
Some(true)
);
let handler = crate::util::new_null_prototype_object(scope);
let proxy = v8::Proxy::new(scope, event, handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
assert!(crate::web_api_interfaces::TimeEvent::is_instance(
scope,
proxy.into()
));
assert_eq!(
global.create_data_property(
scope,
crate::util::v8str(scope, "nativeTimeEventProxy").into(),
proxy.into()
),
Some(true)
);
Ok(())
})
.unwrap();
assert_eq!(
vm.eval(include_str!("svg_smil_time_event_brand.js"))
.unwrap(),
"time-event:ok"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_pending_seek_at_document_begin_does_not_replay_finished_intervals() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-pending-seek-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="first" begin="5s" dur="1s" repeatCount="2"/><set id="second" begin="9s" dur="11s"/></svg>'; globalThis.svg=document.querySelector('svg'); globalThis.events=[];
for(const animation of document.querySelectorAll('set')) for(const type of ['beginEvent','repeatEvent','endEvent']) animation.addEventListener(type,event=>events.push(animation.id+':'+event.type));
svg.pauseAnimations(); svg.setCurrentTime(10);"#).unwrap();
assert_eq!(vm.eval("svg.getCurrentTime()").unwrap(), "0");
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "second:beginEvent");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_seeking_events_are_deferred_and_do_not_replay_skipped_intervals() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-seeking-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="first" begin="5s" dur="1s" repeatCount="2"/><set id="second" begin="9s" dur="11s"/></svg>'; globalThis.svg=document.querySelector('svg'); globalThis.events=[];
for(const animation of document.querySelectorAll('set')) for(const type of ['beginEvent','repeatEvent','endEvent']) animation.addEventListener(type,event=>events.push(animation.id+':'+event.type+':'+event.detail));"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.eval("svg.pauseAnimations(); svg.setCurrentTime(10)")
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "");
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "second:beginEvent:0");
vm.eval("events.length=0; svg.setCurrentTime(5.5)").unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(
vm.eval("events.join('|')").unwrap(),
"second:endEvent:0|first:beginEvent:0"
);
vm.eval("events.length=0; svg.setCurrentTime(6.5)").unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_time_events_progress_without_author_timers_or_frame_callbacks() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-autonomous-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="animation" begin="0s" dur="0.02s" repeatCount="2"/></svg>'; globalThis.events=[];
for(const type of ['beginEvent','repeatEvent','endEvent']) animation.addEventListener(type,event=>events.push([event.type,event.detail,event.isTrusted,event instanceof TimeEvent,event.view===window].join(':')));"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(
vm.eval("events.join('|')").unwrap(),
"beginEvent:0:true:true:true|repeatEvent:1:true:true:true|endEvent:0:true:true:true"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_time_events_stop_when_a_listener_replaces_its_document() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-retired-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="first" begin="0s" dur="0.01s" repeatCount="2"/><set id="second" begin="0s" dur="0.01s"/></svg>'; globalThis.events=[];
for(const animation of document.querySelectorAll('set')) for(const type of ['beginEvent','repeatEvent','endEvent']) animation.addEventListener(type,event=>events.push(animation.id+':'+event.type));
first.onbegin=()=>{document.open();document.write('<!doctype html><body id="replacement"></body>');document.close();};"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "first:beginEvent");
assert_eq!(vm.eval("document.body.id").unwrap(), "replacement");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_timing_events_skip_unobserved_repetitions_without_losing_the_end() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm =
new_storage_page_task_executor_test_vm("https://svg-unobserved-repeat-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="animation" begin="0s" dur="0.000000000001s" repeatCount="1000000000000000000" end="0.01s"/></svg>'; globalThis.events=[];
animation.onbegin=()=>events.push('begin'); animation.onend=()=>events.push('end');"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "begin|end");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_begin_handler_microtasks_can_install_repeat_handlers() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm =
new_storage_page_task_executor_test_vm("https://svg-repeat-handler-checkpoint.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="animation" begin="0s" dur="0.01s" repeatCount="3"/></svg>'; globalThis.events=[];
animation.onbegin=()=>queueMicrotask(()=>{animation.onrepeat=event=>events.push(event.detail);}); animation.onend=()=>events.push('end');"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "1|2|end");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_boundaries_crossed_during_a_handler_still_schedule_a_rendering_wake() {
fn advance_clock(
_scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments<'_>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let pointer = v8::Local::<v8::External>::try_from(args.data())
.unwrap()
.value()
.cast::<crate::native_bridge::JsContextHost>();
unsafe { &*pointer }.advance_svg_clocks_for_test(std::time::Duration::from_secs(4_000));
}
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-crossed-boundaries.test/");
let context = &vm.page_default_context as *const _;
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context, |scope, host_ptr| {
let data = v8::External::new(scope, host_ptr.cast());
let callback = v8::Function::builder(advance_clock)
.data(data.into())
.build(scope)
.unwrap();
let global = scope.get_current_context().global(scope);
assert_eq!(
global.create_data_property(
scope,
crate::util::v8str(scope, "advanceSvgClock").into(),
callback.into()
),
Some(true)
);
Ok(())
})
.unwrap();
vm.eval(r#"document.body.innerHTML='<svg><set id="animation" begin="0s" dur="1000s" repeatCount="3"/></svg>'; globalThis.events=[];
animation.onbegin=()=>{events.push('begin'); advanceSvgClock();};
animation.onrepeat=event=>events.push(event.detail); animation.onend=()=>events.push('end');"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "begin");
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "begin|1|2|end");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_animation_removal_ends_the_interval_without_losing_queued_events() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-reinsert-events.test/");
vm.eval(r#"document.body.innerHTML='<svg><set id="animation" begin="5s" dur="10s"/></svg>'; globalThis.svg=document.querySelector('svg'); globalThis.animation=document.querySelector('set'); globalThis.events=[];
for(const type of ['beginEvent','repeatEvent','endEvent']) animation.addEventListener(type,event=>events.push(event.type));"#).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.eval("svg.pauseAnimations();svg.setCurrentTime(6);animation.remove()")
.unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "beginEvent|endEvent");
vm.eval("svg.append(animation)").unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(
vm.eval("events.join('|')").unwrap(),
"beginEvent|endEvent|beginEvent"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn child_svg_repeat_events_reach_the_owning_windows_capture_listener() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm = new_storage_page_task_executor_test_vm("https://svg-child-repeat-events.test/");
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
vm.eval("document.body.innerHTML='<iframe></iframe>'")
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
vm.eval(r#"globalThis.events=[]; globalThis.other=document.querySelector('iframe').contentWindow;
other.addEventListener('repeatEvent',other.Function('event', 'parent.events.push([event.detail,event.view===window,event instanceof TimeEvent].join(":"))'),true);
other.document.body.innerHTML='<svg><set id="animation" begin="0s" dur="0.02s" repeatCount="2"/></svg>';"#).unwrap();
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
vm.advance_timers_until_deadline_for_test(&loader)
.await
.unwrap();
assert_eq!(vm.eval("events.join('|')").unwrap(), "1:true:true");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 32)]
async fn svg_timing_event_watchdog_bounds_listeners_and_their_microtasks() {
use crate::v8_execution_watchdog::{V8ExecutionWatchdog, V8ExecutionWatchdogKind};
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
for runaway in ["while(true) {}", "queueMicrotask(()=>{while(true) {}})"] {
let mut vm = new_storage_page_task_executor_test_vm("https://svg-event-watchdog.test/");
vm.eval(&format!(r#"document.body.innerHTML='<svg><set id="animation" begin="0s" dur="0.01s"/></svg>';globalThis.calls=0;animation.onbegin=()=>{{calls++;{runaway}}};"#)).unwrap();
vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)
.unwrap();
let _budget = V8ExecutionWatchdog::override_timeout_for_test(
V8ExecutionWatchdogKind::SvgAnimationEvents,
std::time::Duration::from_millis(100),
);
vm.run_one_rendering_update_executor_turn(&loader)
.await
.unwrap();
assert_eq!(
vm.eval("[calls,6*7].join('|')").unwrap(),
"1|42",
"{runaway}"
);
}
}
@@ -30,6 +30,7 @@ pub(crate) enum V8ExecutionWatchdogKind {
LifecycleEvent,
FocusFixup,
RenderingObservers,
SvgAnimationEvents,
}
#[cfg(test)]
@@ -42,6 +43,7 @@ impl V8ExecutionWatchdogKind {
Self::AnimationFrameCallback => 3,
Self::FocusFixup => 4,
Self::RenderingObservers => 5,
Self::SvgAnimationEvents => 6,
}
}
}
@@ -49,8 +51,8 @@ impl V8ExecutionWatchdogKind {
#[cfg(test)]
std::thread_local! {
static V8_EXECUTION_WATCHDOG_TIMEOUT_OVERRIDES:
std::cell::Cell<[Option<Duration>; 6]> =
const { std::cell::Cell::new([None; 6]) };
std::cell::Cell<[Option<Duration>; 7]> =
const { std::cell::Cell::new([None; 7]) };
}
#[cfg(test)]
@@ -296,6 +296,7 @@ interfaces! {
FontFaceSet: EventTarget;
FontFaceSetIterator = "FontFaceSet Iterator";
FontFaceSetLoadEvent: Event;
TimeEvent: Event;
FormData;
FormDataIterator = "FormData Iterator";
FormDataEvent: Event;
+3 -2
View File
@@ -18,8 +18,9 @@ pub use length::{
pub use matrix::{SvgMatrixComponents, serialize_number};
pub use path::{SvgPathSegment, parse_path_segments};
pub use timing::{
SvgAnimationInstanceTimes, SvgAnimationInterval, SvgAnimationRestart, SvgAnimationTiming,
SvgPresentationClock, parse_clock_value,
SvgAnimationEvent, SvgAnimationEventKind, SvgAnimationEvents, SvgAnimationInstanceTimes,
SvgAnimationInterval, SvgAnimationRestart, SvgAnimationTiming, SvgPresentationClock,
parse_clock_value,
};
pub use transform::{
SvgTransform, SvgTransformKind, consolidate_transform_matrices, parse_transform_attribute,
+16 -1
View File
@@ -2,11 +2,15 @@
//! Wall time is supplied by the owner, so seeking never rewrites DOM attributes
//! and paused clocks can be sampled deterministically.
mod events;
pub use events::{SvgAnimationEvent, SvgAnimationEventKind, SvgAnimationEvents};
#[derive(Debug, Default)]
pub struct SvgPresentationClock {
started: bool,
paused: bool,
position: f64,
initial_position: f64,
anchor: f64,
}
@@ -14,6 +18,7 @@ impl SvgPresentationClock {
pub fn start(&mut self, now: f64) {
if !self.started {
self.started = true;
self.initial_position = self.position;
self.anchor = now;
}
}
@@ -57,6 +62,11 @@ impl SvgPresentationClock {
pub fn has_started(&self) -> bool {
self.started
}
/// The position chosen at Document begin, including a pending seek.
pub fn initial_time(&self) -> f64 {
self.initial_position
}
}
/// SMIL clock values. Offsets may have a sign; simple durations may not.
@@ -167,7 +177,11 @@ impl SvgAnimationTiming {
let mut ends = self.ends.clone();
ends.extend_from_slice(&instances.ends);
ends.sort_by(f64::total_cmp);
let duration = (self.simple_duration * self.repeat_count).min(self.repeat_duration);
let duration = if self.simple_duration == 0.0 {
0.0
} else {
(self.simple_duration * self.repeat_count).min(self.repeat_duration)
};
let mut intervals: Vec<SvgAnimationInterval> = Vec::new();
for begin in begins {
if let Some(previous) = intervals.last_mut() {
@@ -235,6 +249,7 @@ mod tests {
clock.pause(3.0);
assert_eq!(clock.current_time(4.0), 0.0);
clock.start(10.0);
assert_eq!(clock.initial_time(), 7.0);
assert_eq!(clock.current_time(20.0), 7.0);
clock.unpause(20.0);
assert_eq!(clock.current_time(21.5), 8.5);
+324
View File
@@ -0,0 +1,324 @@
//! Animation timing events are separate from value sampling. Normal clock
//! progress visits interval boundaries; a seek only changes active membership.
use super::{SvgAnimationInstanceTimes, SvgAnimationInterval, SvgAnimationTiming};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SvgAnimationEventKind {
Begin,
Repeat(f64),
End,
}
impl SvgAnimationEventKind {
pub fn name(self) -> &'static str {
match self {
Self::Begin => "beginEvent",
Self::Repeat(_) => "repeatEvent",
Self::End => "endEvent",
}
}
pub fn order(self) -> u8 {
match self {
Self::End => 0,
Self::Begin => 1,
Self::Repeat(_) => 2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SvgAnimationEvent {
pub time: f64,
pub kind: SvgAnimationEventKind,
}
/// An owned, lazy interval traversal. A renderer can yield between events
/// without allocating a vector proportional to the repeat count.
#[derive(Debug)]
pub struct SvgAnimationEvents {
intervals: std::vec::IntoIter<SvgAnimationInterval>,
current: Option<SvgAnimationInterval>,
duration: f64,
after: f64,
through: f64,
phase: u8,
repetition: f64,
}
impl SvgAnimationTiming {
pub fn active_interval(
&self,
instances: &SvgAnimationInstanceTimes,
time: f64,
) -> Option<SvgAnimationInterval> {
self.intervals(instances)
.into_iter()
.find(|interval| interval.begin <= time && time < interval.end)
}
pub fn events_between(
&self,
instances: &SvgAnimationInstanceTimes,
after: f64,
through: f64,
) -> SvgAnimationEvents {
SvgAnimationEvents {
intervals: self.intervals(instances).into_iter(),
current: None,
duration: self.simple_duration,
after,
through,
phase: 0,
repetition: 1.0,
}
}
}
impl SvgAnimationEvent {
pub fn active_transition(
before: Option<SvgAnimationInterval>,
after: Option<SvgAnimationInterval>,
time: f64,
) -> impl Iterator<Item = Self> {
let changed = before.map(|interval| interval.begin) != after.map(|interval| interval.begin);
[
(changed && before.is_some()).then_some(Self {
time,
kind: SvgAnimationEventKind::End,
}),
(changed && after.is_some()).then_some(Self {
time,
kind: SvgAnimationEventKind::Begin,
}),
]
.into_iter()
.flatten()
}
}
impl SvgAnimationEvents {
/// The next boundary that can run begin/end handlers. This permits a
/// renderer to skip unobserved repeat events only up to the next point
/// where author code could install a repeat listener.
pub fn next_non_repeat_time(&self) -> Option<f64> {
let current = self.current.into_iter().filter_map(|interval| {
if self.phase == 0 && self.contains(interval.begin.max(0.0)) {
Some(interval.begin.max(0.0))
} else {
self.contains(interval.end).then_some(interval.end)
}
});
current
.chain(self.intervals.as_slice().iter().filter_map(|interval| {
if self.contains(interval.begin.max(0.0)) {
Some(interval.begin.max(0.0))
} else {
self.contains(interval.end).then_some(interval.end)
}
}))
.min_by(f64::total_cmp)
}
pub fn skip_repeats_before(&mut self, time: f64) {
if self.phase == 1
&& self.duration.is_finite()
&& self.duration > 0.0
&& let Some(interval) = self.current
{
self.repetition = self
.repetition
.max(((time - interval.begin) / self.duration).ceil());
}
}
fn contains(&self, time: f64) -> bool {
time.is_finite() && time >= 0.0 && self.after < time && time <= self.through
}
}
impl Iterator for SvgAnimationEvents {
type Item = SvgAnimationEvent;
fn next(&mut self) -> Option<Self::Item> {
loop {
let interval = if let Some(interval) = self.current {
interval
} else {
let interval = self.intervals.next()?;
self.current = Some(interval);
self.phase = 0;
self.repetition = ((self.after.max(0.0) - interval.begin) / self.duration)
.floor()
.max(0.0)
+ 1.0;
interval
};
let (time, kind) = match self.phase {
0 => {
self.phase = 1;
(interval.begin.max(0.0), SvgAnimationEventKind::Begin)
}
1 if self.duration.is_finite() && self.duration > 0.0 => {
let time = interval.begin + self.repetition * self.duration;
if !time.is_finite() || time >= interval.end || time > self.through {
self.phase = 2;
continue;
}
let repetition = self.repetition;
self.repetition = (repetition + 1.0).max(repetition.next_up());
(time, SvgAnimationEventKind::Repeat(repetition))
}
1 => {
self.phase = 2;
continue;
}
_ => {
self.current = None;
(interval.end, SvgAnimationEventKind::End)
}
};
if self.contains(time) && (interval.end > 0.0 || interval.begin >= 0.0) {
return Some(SvgAnimationEvent { time, kind });
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SvgAnimationRestart;
fn timing(duration: f64, count: f64) -> SvgAnimationTiming {
SvgAnimationTiming {
begins: vec![0.0],
ends: vec![],
simple_duration: duration,
repeat_count: count,
repeat_duration: f64::INFINITY,
freeze: false,
restart: SvgAnimationRestart::Always,
}
}
#[test]
fn normal_progress_includes_begin_repeats_and_terminal_end_once() {
let timing = timing(1.0, 3.0);
let instances = SvgAnimationInstanceTimes::default();
let rows: Vec<_> = timing.events_between(&instances, -1.0, 3.0).collect();
assert_eq!(
rows.iter().map(|event| event.time).collect::<Vec<_>>(),
[0.0, 1.0, 2.0, 3.0]
);
assert_eq!(
rows.iter().map(|event| event.kind).collect::<Vec<_>>(),
[
SvgAnimationEventKind::Begin,
SvgAnimationEventKind::Repeat(1.0),
SvgAnimationEventKind::Repeat(2.0),
SvgAnimationEventKind::End
]
);
assert!(timing.events_between(&instances, 3.0, 4.0).next().is_none());
}
#[test]
fn seeking_changes_active_membership_without_skipped_repeat_events() {
let timing = timing(1.0, 3.0);
let instances = SvgAnimationInstanceTimes::default();
let first = timing.active_interval(&instances, 0.5);
assert!(
SvgAnimationEvent::active_transition(
first,
timing.active_interval(&instances, 2.5),
2.5
)
.next()
.is_none()
);
let rows: Vec<_> = SvgAnimationEvent::active_transition(first, None, 5.0).collect();
assert_eq!(
rows,
[SvgAnimationEvent {
time: 5.0,
kind: SvgAnimationEventKind::End
}]
);
assert_eq!(
SvgAnimationEvent::active_transition(None, first, 0.5)
.next()
.unwrap()
.kind,
SvgAnimationEventKind::Begin
);
}
#[test]
fn restarted_intervals_end_before_the_replacement_begins() {
let mut timing = timing(3.0, 1.0);
timing.begins.push(1.0);
let rows: Vec<_> = timing
.events_between(&SvgAnimationInstanceTimes::default(), -1.0, 4.0)
.collect();
assert_eq!(
rows.iter()
.map(|event| (event.time, event.kind))
.collect::<Vec<_>>(),
[
(0.0, SvgAnimationEventKind::Begin),
(1.0, SvgAnimationEventKind::End),
(1.0, SvgAnimationEventKind::Begin),
(4.0, SvgAnimationEventKind::End)
]
);
}
#[test]
fn repeat_duration_and_fractional_counts_preserve_the_final_partial_iteration() {
let mut timing = timing(2.0, 2.5);
timing.repeat_duration = 4.5;
let rows: Vec<_> = timing
.events_between(&SvgAnimationInstanceTimes::default(), -1.0, 5.0)
.collect();
assert_eq!(
rows.iter().map(|event| event.time).collect::<Vec<_>>(),
[0.0, 2.0, 4.0, 4.5]
);
}
#[test]
fn enormous_repeat_counts_are_lazy_and_unobserved_repeats_can_be_skipped() {
let timing = timing(0.001, 1e30);
let mut events = timing.events_between(&SvgAnimationInstanceTimes::default(), -1.0, 1e20);
assert_eq!(events.next().unwrap().kind, SvgAnimationEventKind::Begin);
assert_eq!(
events.next().unwrap().kind,
SvgAnimationEventKind::Repeat(1.0)
);
events.skip_repeats_before(1e20);
let next = events.next();
assert!(next.is_none() || next.unwrap().time >= 1e20);
}
#[test]
fn zero_duration_and_intervals_before_document_begin_do_not_loop() {
let instances = SvgAnimationInstanceTimes::default();
for count in [3.0, f64::INFINITY] {
let zero_duration = timing(0.0, count);
let rows: Vec<_> = zero_duration
.events_between(&instances, -1.0, 1.0)
.collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].kind, SvgAnimationEventKind::Begin);
assert_eq!(rows[1].kind, SvgAnimationEventKind::End);
assert_eq!(rows[1].time, 0.0);
}
let mut ended = timing(1.0, 1.0);
for begin in [-5.0, -1.0] {
ended.begins = vec![begin];
assert!(ended.events_between(&instances, -1.0, 1.0).next().is_none());
}
}
}