mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(svg): dispatch native SMIL timing events
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user