mirror of
https://github.com/servo/servo.git
synced 2025-08-03 04:30:10 +01:00
Auto merge of #26244 - mrobinson:animation-cancel, r=jdm
Add support for canceling CSS transitions This change adds support for canceling CSS transitions when a property is no longer transitionable. Support for canceling and replacing CSS transitions when the end value changes is still pending. This change also takes advantage of updating the constellation message to fix a bug where transition events could be sent for closed pipelines. Fixes #15079. <!-- Please describe your changes on the following line: --> --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors - [x] These changes fix #15079. <!-- Either: --> - [x] There are tests for these changes OR - [ ] These changes do not require tests because ___ <!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.--> <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
This commit is contained in:
commit
610991bdfc
15 changed files with 351 additions and 153 deletions
|
@ -129,6 +129,7 @@ time
|
|||
timeupdate
|
||||
toggle
|
||||
track
|
||||
transitioncancel
|
||||
transitionend
|
||||
unhandledrejection
|
||||
unload
|
||||
|
|
|
@ -13,7 +13,9 @@ use fxhash::{FxHashMap, FxHashSet};
|
|||
use ipc_channel::ipc::IpcSender;
|
||||
use msg::constellation_msg::PipelineId;
|
||||
use script_traits::UntrustedNodeAddress;
|
||||
use script_traits::{AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg};
|
||||
use script_traits::{
|
||||
AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, TransitionEventType,
|
||||
};
|
||||
use style::animation::{update_style_for_animation, Animation};
|
||||
use style::dom::TElement;
|
||||
use style::font_metrics::ServoMetricsProvider;
|
||||
|
@ -28,6 +30,7 @@ pub fn update_animation_state<E>(
|
|||
script_chan: &IpcSender<ConstellationControlMsg>,
|
||||
running_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>,
|
||||
expired_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>,
|
||||
cancelled_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>,
|
||||
mut keys_to_remove: FxHashSet<OpaqueNode>,
|
||||
mut newly_transitioning_nodes: Option<&mut Vec<UntrustedNodeAddress>>,
|
||||
new_animations_receiver: &Receiver<Animation>,
|
||||
|
@ -36,6 +39,8 @@ pub fn update_animation_state<E>(
|
|||
) where
|
||||
E: TElement,
|
||||
{
|
||||
send_events_for_cancelled_animations(script_chan, cancelled_animations, pipeline_id);
|
||||
|
||||
let mut new_running_animations = vec![];
|
||||
while let Ok(animation) = new_animations_receiver.try_recv() {
|
||||
let mut should_push = true;
|
||||
|
@ -102,11 +107,13 @@ pub fn update_animation_state<E>(
|
|||
|
||||
if let Animation::Transition(node, _, ref property_animation) = running_animation {
|
||||
script_chan
|
||||
.send(ConstellationControlMsg::TransitionEnd(
|
||||
node.to_untrusted_node_address(),
|
||||
property_animation.property_name().into(),
|
||||
property_animation.duration,
|
||||
))
|
||||
.send(ConstellationControlMsg::TransitionEvent {
|
||||
pipeline_id,
|
||||
event_type: TransitionEventType::TransitionEnd,
|
||||
node: node.to_untrusted_node_address(),
|
||||
property_name: property_animation.property_name().into(),
|
||||
elapsed_time: property_animation.duration,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
@ -161,6 +168,37 @@ pub fn update_animation_state<E>(
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
/// Send events for cancelled animations. Currently this only handles cancelled
|
||||
/// transitions, but eventually this should handle cancelled CSS animations as
|
||||
/// well.
|
||||
pub fn send_events_for_cancelled_animations(
|
||||
script_channel: &IpcSender<ConstellationControlMsg>,
|
||||
cancelled_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>,
|
||||
pipeline_id: PipelineId,
|
||||
) {
|
||||
for (node, animations) in cancelled_animations.drain() {
|
||||
for animation in animations {
|
||||
match animation {
|
||||
Animation::Transition(transition_node, _, ref property_animation) => {
|
||||
debug_assert!(transition_node == node);
|
||||
script_channel
|
||||
.send(ConstellationControlMsg::TransitionEvent {
|
||||
pipeline_id,
|
||||
event_type: TransitionEventType::TransitionCancel,
|
||||
node: node.to_untrusted_node_address(),
|
||||
property_name: property_animation.property_name().into(),
|
||||
elapsed_time: property_animation.duration,
|
||||
})
|
||||
.unwrap();
|
||||
},
|
||||
Animation::Keyframes(..) => {
|
||||
warn!("Got unexpected animation in expired transitions list.")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recalculates style for a set of animations. This does *not* run with the DOM
|
||||
/// lock held. Returns a set of nodes associated with animations that are no longer
|
||||
/// valid.
|
||||
|
|
|
@ -207,6 +207,9 @@ pub struct LayoutThread {
|
|||
/// The list of animations that have expired since the last style recalculation.
|
||||
expired_animations: ServoArc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>,
|
||||
|
||||
/// The list of animations that have been cancelled during the last style recalculation.
|
||||
cancelled_animations: ServoArc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>,
|
||||
|
||||
/// A counter for epoch messages
|
||||
epoch: Cell<Epoch>,
|
||||
|
||||
|
@ -576,6 +579,7 @@ impl LayoutThread {
|
|||
document_shared_lock: None,
|
||||
running_animations: ServoArc::new(RwLock::new(Default::default())),
|
||||
expired_animations: ServoArc::new(RwLock::new(Default::default())),
|
||||
cancelled_animations: ServoArc::new(RwLock::new(Default::default())),
|
||||
// Epoch starts at 1 because of the initial display list for epoch 0 that we send to WR
|
||||
epoch: Cell::new(Epoch(1)),
|
||||
viewport_size: Size2D::new(Au(0), Au(0)),
|
||||
|
@ -655,6 +659,7 @@ impl LayoutThread {
|
|||
visited_styles_enabled: false,
|
||||
running_animations: self.running_animations.clone(),
|
||||
expired_animations: self.expired_animations.clone(),
|
||||
cancelled_animations: self.cancelled_animations.clone(),
|
||||
registered_speculative_painters: &self.registered_painters,
|
||||
local_context_creation_data: Mutex::new(thread_local_style_context_creation_data),
|
||||
timer: self.timer.clone(),
|
||||
|
@ -1783,6 +1788,7 @@ impl LayoutThread {
|
|||
&self.script_chan,
|
||||
&mut *self.running_animations.write(),
|
||||
&mut *self.expired_animations.write(),
|
||||
&mut *self.cancelled_animations.write(),
|
||||
invalid_nodes,
|
||||
newly_transitioning_nodes,
|
||||
&self.new_animations_receiver,
|
||||
|
|
|
@ -609,6 +609,7 @@ impl LayoutThread {
|
|||
visited_styles_enabled: false,
|
||||
running_animations: Default::default(),
|
||||
expired_animations: Default::default(),
|
||||
cancelled_animations: Default::default(),
|
||||
registered_speculative_painters: &self.registered_painters,
|
||||
local_context_creation_data: Mutex::new(thread_local_style_context_creation_data),
|
||||
timer: self.timer.clone(),
|
||||
|
|
|
@ -496,6 +496,7 @@ macro_rules! global_event_handlers(
|
|||
event_handler!(suspend, GetOnsuspend, SetOnsuspend);
|
||||
event_handler!(timeupdate, GetOntimeupdate, SetOntimeupdate);
|
||||
event_handler!(toggle, GetOntoggle, SetOntoggle);
|
||||
event_handler!(transitioncancel, GetOntransitioncancel, SetOntransitioncancel);
|
||||
event_handler!(transitionend, GetOntransitionend, SetOntransitionend);
|
||||
event_handler!(volumechange, GetOnvolumechange, SetOnvolumechange);
|
||||
event_handler!(waiting, GetOnwaiting, SetOnwaiting);
|
||||
|
|
|
@ -93,6 +93,7 @@ interface mixin GlobalEventHandlers {
|
|||
// https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl
|
||||
partial interface mixin GlobalEventHandlers {
|
||||
attribute EventHandler ontransitionend;
|
||||
attribute EventHandler ontransitioncancel;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/selection-api/#extensions-to-globaleventhandlers-interface
|
||||
|
|
|
@ -133,17 +133,15 @@ use script_traits::CompositorEvent::{
|
|||
CompositionEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent, ResizeEvent, TouchEvent,
|
||||
WheelEvent,
|
||||
};
|
||||
use script_traits::StructuredSerializedData;
|
||||
use script_traits::{CompositorEvent, ConstellationControlMsg};
|
||||
use script_traits::{
|
||||
DiscardBrowsingContext, DocumentActivity, EventResult, HistoryEntryReplacement,
|
||||
CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext, DocumentActivity,
|
||||
EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult, LayoutMsg, LoadData,
|
||||
LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter,
|
||||
ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan,
|
||||
StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId, TransitionEventType,
|
||||
UntrustedNodeAddress, UpdatePipelineIdReason, WebrenderIpcSender, WheelDelta, WindowSizeData,
|
||||
WindowSizeType,
|
||||
};
|
||||
use script_traits::{InitialScriptState, JsEvalResult, LayoutMsg, LoadData, LoadOrigin};
|
||||
use script_traits::{MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo};
|
||||
use script_traits::{Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory};
|
||||
use script_traits::{ScriptToConstellationChan, TimerSchedulerMsg};
|
||||
use script_traits::{TouchEventType, TouchId, UntrustedNodeAddress, WheelDelta};
|
||||
use script_traits::{UpdatePipelineIdReason, WebrenderIpcSender, WindowSizeData, WindowSizeType};
|
||||
use servo_atoms::Atom;
|
||||
use servo_config::opts;
|
||||
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
||||
|
@ -1668,46 +1666,43 @@ impl ScriptThread {
|
|||
fn message_to_pipeline(&self, msg: &MixedMessage) -> Option<PipelineId> {
|
||||
use script_traits::ConstellationControlMsg::*;
|
||||
match *msg {
|
||||
MixedMessage::FromConstellation(ref inner_msg) => {
|
||||
match *inner_msg {
|
||||
StopDelayingLoadEventsMode(id) => Some(id),
|
||||
NavigationResponse(id, _) => Some(id),
|
||||
AttachLayout(ref new_layout_info) => Some(new_layout_info.new_pipeline_id),
|
||||
Resize(id, ..) => Some(id),
|
||||
ResizeInactive(id, ..) => Some(id),
|
||||
UnloadDocument(id) => Some(id),
|
||||
ExitPipeline(id, ..) => Some(id),
|
||||
ExitScriptThread => None,
|
||||
SendEvent(id, ..) => Some(id),
|
||||
Viewport(id, ..) => Some(id),
|
||||
SetScrollState(id, ..) => Some(id),
|
||||
GetTitle(id) => Some(id),
|
||||
SetDocumentActivity(id, ..) => Some(id),
|
||||
ChangeFrameVisibilityStatus(id, ..) => Some(id),
|
||||
NotifyVisibilityChange(id, ..) => Some(id),
|
||||
NavigateIframe(id, ..) => Some(id),
|
||||
PostMessage { target: id, .. } => Some(id),
|
||||
UpdatePipelineId(_, _, _, id, _) => Some(id),
|
||||
UpdateHistoryState(id, ..) => Some(id),
|
||||
RemoveHistoryStates(id, ..) => Some(id),
|
||||
FocusIFrame(id, ..) => Some(id),
|
||||
WebDriverScriptCommand(id, ..) => Some(id),
|
||||
TickAllAnimations(id) => Some(id),
|
||||
// FIXME https://github.com/servo/servo/issues/15079
|
||||
TransitionEnd(..) => None,
|
||||
WebFontLoaded(id) => Some(id),
|
||||
DispatchIFrameLoadEvent {
|
||||
target: _,
|
||||
parent: id,
|
||||
child: _,
|
||||
} => Some(id),
|
||||
DispatchStorageEvent(id, ..) => Some(id),
|
||||
ReportCSSError(id, ..) => Some(id),
|
||||
Reload(id, ..) => Some(id),
|
||||
PaintMetric(..) => None,
|
||||
ExitFullScreen(id, ..) => Some(id),
|
||||
MediaSessionAction(..) => None,
|
||||
}
|
||||
MixedMessage::FromConstellation(ref inner_msg) => match *inner_msg {
|
||||
StopDelayingLoadEventsMode(id) => Some(id),
|
||||
NavigationResponse(id, _) => Some(id),
|
||||
AttachLayout(ref new_layout_info) => Some(new_layout_info.new_pipeline_id),
|
||||
Resize(id, ..) => Some(id),
|
||||
ResizeInactive(id, ..) => Some(id),
|
||||
UnloadDocument(id) => Some(id),
|
||||
ExitPipeline(id, ..) => Some(id),
|
||||
ExitScriptThread => None,
|
||||
SendEvent(id, ..) => Some(id),
|
||||
Viewport(id, ..) => Some(id),
|
||||
SetScrollState(id, ..) => Some(id),
|
||||
GetTitle(id) => Some(id),
|
||||
SetDocumentActivity(id, ..) => Some(id),
|
||||
ChangeFrameVisibilityStatus(id, ..) => Some(id),
|
||||
NotifyVisibilityChange(id, ..) => Some(id),
|
||||
NavigateIframe(id, ..) => Some(id),
|
||||
PostMessage { target: id, .. } => Some(id),
|
||||
UpdatePipelineId(_, _, _, id, _) => Some(id),
|
||||
UpdateHistoryState(id, ..) => Some(id),
|
||||
RemoveHistoryStates(id, ..) => Some(id),
|
||||
FocusIFrame(id, ..) => Some(id),
|
||||
WebDriverScriptCommand(id, ..) => Some(id),
|
||||
TickAllAnimations(id) => Some(id),
|
||||
TransitionEvent { .. } => None,
|
||||
WebFontLoaded(id) => Some(id),
|
||||
DispatchIFrameLoadEvent {
|
||||
target: _,
|
||||
parent: id,
|
||||
child: _,
|
||||
} => Some(id),
|
||||
DispatchStorageEvent(id, ..) => Some(id),
|
||||
ReportCSSError(id, ..) => Some(id),
|
||||
Reload(id, ..) => Some(id),
|
||||
PaintMetric(..) => None,
|
||||
ExitFullScreen(id, ..) => Some(id),
|
||||
MediaSessionAction(..) => None,
|
||||
},
|
||||
MixedMessage::FromDevtools(_) => None,
|
||||
MixedMessage::FromScript(ref inner_msg) => match *inner_msg {
|
||||
|
@ -1896,8 +1891,20 @@ impl ScriptThread {
|
|||
ConstellationControlMsg::TickAllAnimations(pipeline_id) => {
|
||||
self.handle_tick_all_animations(pipeline_id)
|
||||
},
|
||||
ConstellationControlMsg::TransitionEnd(unsafe_node, name, duration) => {
|
||||
self.handle_transition_event(unsafe_node, name, duration)
|
||||
ConstellationControlMsg::TransitionEvent {
|
||||
pipeline_id,
|
||||
event_type,
|
||||
node,
|
||||
property_name,
|
||||
elapsed_time,
|
||||
} => {
|
||||
self.handle_transition_event(
|
||||
pipeline_id,
|
||||
event_type,
|
||||
node,
|
||||
property_name,
|
||||
elapsed_time,
|
||||
);
|
||||
},
|
||||
ConstellationControlMsg::WebFontLoaded(pipeline_id) => {
|
||||
self.handle_web_font_loaded(pipeline_id)
|
||||
|
@ -2899,12 +2906,16 @@ impl ScriptThread {
|
|||
document.run_the_animation_frame_callbacks();
|
||||
}
|
||||
|
||||
/// Handles firing of transition events.
|
||||
/// Handles firing of transition-related events.
|
||||
///
|
||||
/// TODO(mrobinson): Add support for more events.
|
||||
fn handle_transition_event(
|
||||
&self,
|
||||
pipeline_id: PipelineId,
|
||||
event_type: TransitionEventType,
|
||||
unsafe_node: UntrustedNodeAddress,
|
||||
name: String,
|
||||
duration: f64,
|
||||
property_name: String,
|
||||
elapsed_time: f64,
|
||||
) {
|
||||
let js_runtime = self.js_runtime.rt();
|
||||
let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) };
|
||||
|
@ -2926,29 +2937,35 @@ impl ScriptThread {
|
|||
},
|
||||
}
|
||||
|
||||
let window = window_from_node(&*node);
|
||||
|
||||
// Not quite the right thing - see #13865.
|
||||
node.dirty(NodeDamage::NodeStyleDamaged);
|
||||
|
||||
if let Some(el) = node.downcast::<Element>() {
|
||||
if !el.has_css_layout_box() {
|
||||
return;
|
||||
}
|
||||
if self.closed_pipelines.borrow().contains(&pipeline_id) {
|
||||
warn!("Ignoring transition event for closed pipeline.");
|
||||
return;
|
||||
}
|
||||
|
||||
let init = TransitionEventInit {
|
||||
let event_atom = match event_type {
|
||||
TransitionEventType::TransitionEnd => {
|
||||
// Not quite the right thing - see #13865.
|
||||
node.dirty(NodeDamage::NodeStyleDamaged);
|
||||
atom!("transitionend")
|
||||
},
|
||||
TransitionEventType::TransitionCancel => atom!("transitioncancel"),
|
||||
};
|
||||
|
||||
let event_init = TransitionEventInit {
|
||||
parent: EventInit {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
},
|
||||
propertyName: DOMString::from(name),
|
||||
elapsedTime: Finite::new(duration as f32).unwrap(),
|
||||
// FIXME: Handle pseudo-elements properly
|
||||
propertyName: DOMString::from(property_name),
|
||||
elapsedTime: Finite::new(elapsed_time as f32).unwrap(),
|
||||
// TODO: Handle pseudo-elements properly
|
||||
pseudoElement: DOMString::new(),
|
||||
};
|
||||
let transition_event = TransitionEvent::new(&window, atom!("transitionend"), &init);
|
||||
transition_event.upcast::<Event>().fire(node.upcast());
|
||||
|
||||
let window = window_from_node(&*node);
|
||||
TransitionEvent::new(&window, event_atom, &event_init)
|
||||
.upcast::<Event>()
|
||||
.fire(node.upcast());
|
||||
}
|
||||
|
||||
/// Handles a Web font being loaded. Does nothing if the page no longer exists.
|
||||
|
|
|
@ -282,6 +282,16 @@ pub enum UpdatePipelineIdReason {
|
|||
Traversal,
|
||||
}
|
||||
|
||||
/// The type of transition event to trigger.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub enum TransitionEventType {
|
||||
/// The transition has ended by reaching the end of its animation.
|
||||
TransitionEnd,
|
||||
/// The transition ended early for some reason, such as the property
|
||||
/// no longer being transitionable or being replaced by another transition.
|
||||
TransitionCancel,
|
||||
}
|
||||
|
||||
/// Messages sent from the constellation or layout to the script thread.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum ConstellationControlMsg {
|
||||
|
@ -368,8 +378,19 @@ pub enum ConstellationControlMsg {
|
|||
WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
|
||||
/// Notifies script thread that all animations are done
|
||||
TickAllAnimations(PipelineId),
|
||||
/// Notifies the script thread of a transition end
|
||||
TransitionEnd(UntrustedNodeAddress, String, f64),
|
||||
/// Notifies the script thread that a transition related event should be sent.
|
||||
TransitionEvent {
|
||||
/// The pipeline id of the layout task that sent this message.
|
||||
pipeline_id: PipelineId,
|
||||
/// The type of transition event this should trigger.
|
||||
event_type: TransitionEventType,
|
||||
/// The address of the node which owns this transition.
|
||||
node: UntrustedNodeAddress,
|
||||
/// The property name of the property that is transitioning.
|
||||
property_name: String,
|
||||
/// The elapsed time property to send with this transition event.
|
||||
elapsed_time: f64,
|
||||
},
|
||||
/// Notifies the script thread that a new Web font has been loaded, and thus the page should be
|
||||
/// reflowed.
|
||||
WebFontLoaded(PipelineId),
|
||||
|
@ -429,7 +450,7 @@ impl fmt::Debug for ConstellationControlMsg {
|
|||
FocusIFrame(..) => "FocusIFrame",
|
||||
WebDriverScriptCommand(..) => "WebDriverScriptCommand",
|
||||
TickAllAnimations(..) => "TickAllAnimations",
|
||||
TransitionEnd(..) => "TransitionEnd",
|
||||
TransitionEvent { .. } => "TransitionEvent",
|
||||
WebFontLoaded(..) => "WebFontLoaded",
|
||||
DispatchIFrameLoadEvent { .. } => "DispatchIFrameLoadEvent",
|
||||
DispatchStorageEvent(..) => "DispatchStorageEvent",
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::font_metrics::FontMetricsProvider;
|
|||
use crate::properties::animated_properties::{AnimatedProperty, TransitionPropertyIteration};
|
||||
use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
|
||||
use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState;
|
||||
use crate::properties::{self, CascadeMode, ComputedValues, LonghandId};
|
||||
use crate::properties::{self, CascadeMode, ComputedValues, LonghandId, LonghandIdSet};
|
||||
use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
|
||||
use crate::stylesheets::Origin;
|
||||
use crate::timer::Timer;
|
||||
|
@ -245,6 +245,17 @@ impl Animation {
|
|||
Animation::Keyframes(..) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this animation has the same end value as another one.
|
||||
#[inline]
|
||||
pub fn is_transition_with_same_end_value(&self, other_animation: &PropertyAnimation) -> bool {
|
||||
match *self {
|
||||
Animation::Transition(_, _, ref animation) => {
|
||||
animation.has_the_same_end_value_as(other_animation)
|
||||
},
|
||||
Animation::Keyframes(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an animation for a given property.
|
||||
|
@ -261,6 +272,11 @@ pub struct PropertyAnimation {
|
|||
}
|
||||
|
||||
impl PropertyAnimation {
|
||||
/// Returns the given property longhand id.
|
||||
pub fn property_id(&self) -> LonghandId {
|
||||
self.property.id()
|
||||
}
|
||||
|
||||
/// Returns the given property name.
|
||||
pub fn property_name(&self) -> &'static str {
|
||||
self.property.name()
|
||||
|
@ -351,20 +367,86 @@ impl PropertyAnimation {
|
|||
}
|
||||
}
|
||||
|
||||
/// Inserts transitions into the queue of running animations as applicable for
|
||||
/// the given style difference. This is called from the layout worker threads.
|
||||
/// Returns true if any animations were kicked off and false otherwise.
|
||||
pub fn start_transitions_if_applicable(
|
||||
/// Start any new transitions for this node and ensure that any existing transitions
|
||||
/// that are cancelled are marked as cancelled in the SharedStyleContext. This is
|
||||
/// at the end of calculating style for a single node.
|
||||
#[cfg(feature = "servo")]
|
||||
pub fn update_transitions(
|
||||
context: &SharedStyleContext,
|
||||
new_animations_sender: &Sender<Animation>,
|
||||
opaque_node: OpaqueNode,
|
||||
old_style: &ComputedValues,
|
||||
new_style: &mut Arc<ComputedValues>,
|
||||
timer: &Timer,
|
||||
running_and_expired_transitions: &[PropertyAnimation],
|
||||
) -> bool {
|
||||
let mut had_animations = false;
|
||||
expired_transitions: &[PropertyAnimation],
|
||||
) {
|
||||
let mut all_running_animations = context.running_animations.write();
|
||||
let previously_running_animations = all_running_animations
|
||||
.remove(&opaque_node)
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let properties_that_transition = start_transitions_if_applicable(
|
||||
context,
|
||||
new_animations_sender,
|
||||
opaque_node,
|
||||
old_style,
|
||||
new_style,
|
||||
expired_transitions,
|
||||
&previously_running_animations,
|
||||
);
|
||||
|
||||
let mut all_cancelled_animations = context.cancelled_animations.write();
|
||||
let mut cancelled_animations = all_cancelled_animations
|
||||
.remove(&opaque_node)
|
||||
.unwrap_or_else(Vec::new);
|
||||
let mut running_animations = vec![];
|
||||
|
||||
// For every animation that was running before this style change, we cancel it
|
||||
// if the property no longer transitions.
|
||||
for running_animation in previously_running_animations.into_iter() {
|
||||
if let Animation::Transition(_, _, ref property_animation) = running_animation {
|
||||
if !properties_that_transition.contains(property_animation.property_id()) {
|
||||
cancelled_animations.push(running_animation);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
running_animations.push(running_animation);
|
||||
}
|
||||
|
||||
if !cancelled_animations.is_empty() {
|
||||
all_cancelled_animations.insert(opaque_node, cancelled_animations);
|
||||
}
|
||||
if !running_animations.is_empty() {
|
||||
all_running_animations.insert(opaque_node, running_animations);
|
||||
}
|
||||
}
|
||||
|
||||
/// Kick off any new transitions for this node and return all of the properties that are
|
||||
/// transitioning. This is at the end of calculating style for a single node.
|
||||
#[cfg(feature = "servo")]
|
||||
pub fn start_transitions_if_applicable(
|
||||
context: &SharedStyleContext,
|
||||
new_animations_sender: &Sender<Animation>,
|
||||
opaque_node: OpaqueNode,
|
||||
old_style: &ComputedValues,
|
||||
new_style: &mut Arc<ComputedValues>,
|
||||
expired_transitions: &[PropertyAnimation],
|
||||
running_animations: &[Animation],
|
||||
) -> LonghandIdSet {
|
||||
// If the style of this element is display:none, then we don't start any transitions
|
||||
// and we cancel any currently running transitions by returning an empty LonghandIdSet.
|
||||
if new_style.get_box().clone_display().is_none() {
|
||||
return LonghandIdSet::new();
|
||||
}
|
||||
|
||||
let mut properties_that_transition = LonghandIdSet::new();
|
||||
let transitions: Vec<TransitionPropertyIteration> = new_style.transition_properties().collect();
|
||||
for transition in &transitions {
|
||||
if properties_that_transition.contains(transition.longhand_id) {
|
||||
continue;
|
||||
} else {
|
||||
properties_that_transition.insert(transition.longhand_id);
|
||||
}
|
||||
|
||||
let property_animation = match PropertyAnimation::from_longhand(
|
||||
transition.longhand_id,
|
||||
new_style
|
||||
|
@ -387,20 +469,27 @@ pub fn start_transitions_if_applicable(
|
|||
property_animation.update(Arc::get_mut(new_style).unwrap(), 0.0);
|
||||
|
||||
// Per [1], don't trigger a new transition if the end state for that
|
||||
// transition is the same as that of a transition that's already
|
||||
// running on the same node.
|
||||
//
|
||||
// transition is the same as that of a transition that's expired.
|
||||
// [1]: https://drafts.csswg.org/css-transitions/#starting
|
||||
debug!(
|
||||
"checking {:?} for matching end value",
|
||||
running_and_expired_transitions
|
||||
);
|
||||
if running_and_expired_transitions
|
||||
debug!("checking {:?} for matching end value", expired_transitions);
|
||||
if expired_transitions
|
||||
.iter()
|
||||
.any(|animation| animation.has_the_same_end_value_as(&property_animation))
|
||||
{
|
||||
debug!(
|
||||
"Not initiating transition for {}, other transition \
|
||||
"Not initiating transition for {}, expired transition \
|
||||
found with the same end value",
|
||||
property_animation.property_name()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if running_animations
|
||||
.iter()
|
||||
.any(|animation| animation.is_transition_with_same_end_value(&property_animation))
|
||||
{
|
||||
debug!(
|
||||
"Not initiating transition for {}, running transition \
|
||||
found with the same end value",
|
||||
property_animation.property_name()
|
||||
);
|
||||
|
@ -410,7 +499,7 @@ pub fn start_transitions_if_applicable(
|
|||
// Kick off the animation.
|
||||
debug!("Kicking off transition of {:?}", property_animation);
|
||||
let box_style = new_style.get_box();
|
||||
let now = timer.seconds();
|
||||
let now = context.timer.seconds();
|
||||
let start_time = now + (box_style.transition_delay_mod(transition.index).seconds() as f64);
|
||||
new_animations_sender
|
||||
.send(Animation::Transition(
|
||||
|
@ -419,11 +508,9 @@ pub fn start_transitions_if_applicable(
|
|||
property_animation,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
had_animations = true;
|
||||
}
|
||||
|
||||
had_animations
|
||||
properties_that_transition
|
||||
}
|
||||
|
||||
fn compute_style_for_animation_step<E>(
|
||||
|
@ -608,12 +695,6 @@ where
|
|||
if progress >= 0.0 {
|
||||
property_animation.update(Arc::make_mut(style), progress);
|
||||
}
|
||||
|
||||
// FIXME(emilio): Should check before updating the style that the
|
||||
// transition_property still transitions this, or bail out if not.
|
||||
//
|
||||
// Or doing it in process_animations, only if transition_property
|
||||
// changed somehow (even better).
|
||||
AnimationUpdate::Regular
|
||||
},
|
||||
Animation::Keyframes(_, ref animation, ref name, ref state) => {
|
||||
|
|
|
@ -194,6 +194,10 @@ pub struct SharedStyleContext<'a> {
|
|||
#[cfg(feature = "servo")]
|
||||
pub expired_animations: Arc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>,
|
||||
|
||||
/// The list of animations that have expired since the last style recalculation.
|
||||
#[cfg(feature = "servo")]
|
||||
pub cancelled_animations: Arc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>,
|
||||
|
||||
/// Paint worklets
|
||||
#[cfg(feature = "servo")]
|
||||
pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters,
|
||||
|
|
|
@ -440,7 +440,7 @@ trait PrivateMatchMethods: TElement {
|
|||
use crate::animation;
|
||||
|
||||
let this_opaque = self.as_node().opaque();
|
||||
let mut running_and_expired_transitions = vec![];
|
||||
let mut expired_transitions = vec![];
|
||||
let shared_context = context.shared;
|
||||
if let Some(ref mut old_values) = *old_values {
|
||||
// We apply the expired transitions and animations to the old style
|
||||
|
@ -454,14 +454,13 @@ trait PrivateMatchMethods: TElement {
|
|||
shared_context,
|
||||
this_opaque,
|
||||
old_values,
|
||||
&mut running_and_expired_transitions,
|
||||
&mut expired_transitions,
|
||||
);
|
||||
|
||||
Self::update_style_for_animations_and_collect_running_transitions(
|
||||
Self::update_style_for_animations(
|
||||
shared_context,
|
||||
this_opaque,
|
||||
old_values,
|
||||
&mut running_and_expired_transitions,
|
||||
&context.thread_local.font_metrics_provider,
|
||||
);
|
||||
}
|
||||
|
@ -479,13 +478,13 @@ trait PrivateMatchMethods: TElement {
|
|||
// Trigger transitions if necessary. This will set `new_values` to
|
||||
// the starting value of the transition if it did trigger a transition.
|
||||
if let Some(ref values) = old_values {
|
||||
animation::start_transitions_if_applicable(
|
||||
animation::update_transitions(
|
||||
&shared_context,
|
||||
new_animations_sender,
|
||||
this_opaque,
|
||||
&values,
|
||||
new_values,
|
||||
&shared_context.timer,
|
||||
&running_and_expired_transitions,
|
||||
&expired_transitions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -627,33 +626,30 @@ trait PrivateMatchMethods: TElement {
|
|||
}
|
||||
|
||||
#[cfg(feature = "servo")]
|
||||
fn update_style_for_animations_and_collect_running_transitions(
|
||||
fn update_style_for_animations(
|
||||
context: &SharedStyleContext,
|
||||
node: OpaqueNode,
|
||||
style: &mut Arc<ComputedValues>,
|
||||
running_transitions: &mut Vec<crate::animation::PropertyAnimation>,
|
||||
font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
|
||||
) {
|
||||
use crate::animation::{self, Animation, AnimationUpdate};
|
||||
|
||||
let had_running_animations = context.running_animations.read().get(&node).is_some();
|
||||
if !had_running_animations {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut all_running_animations = context.running_animations.write();
|
||||
for mut running_animation in all_running_animations.get_mut(&node).unwrap() {
|
||||
if let Animation::Transition(_, _, ref property_animation) = *running_animation {
|
||||
running_transitions.push(property_animation.clone());
|
||||
continue;
|
||||
}
|
||||
let running_animations = match all_running_animations.get_mut(&node) {
|
||||
Some(running_animations) => running_animations,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let update = animation::update_style_for_animation::<Self>(
|
||||
context,
|
||||
&mut running_animation,
|
||||
style,
|
||||
font_metrics,
|
||||
);
|
||||
for running_animation in running_animations.iter_mut() {
|
||||
let update = match *running_animation {
|
||||
Animation::Transition(..) => continue,
|
||||
Animation::Keyframes(..) => animation::update_style_for_animation::<Self>(
|
||||
context,
|
||||
running_animation,
|
||||
style,
|
||||
font_metrics,
|
||||
),
|
||||
};
|
||||
|
||||
match *running_animation {
|
||||
Animation::Transition(..) => unreachable!(),
|
||||
|
|
|
@ -399093,6 +399093,13 @@
|
|||
}
|
||||
]
|
||||
],
|
||||
"transitioncancel-002.html": [
|
||||
"e62b17b5dc60ef762e0a0780c967b6e014da5bc9",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
]
|
||||
],
|
||||
"transitionevent-interface.html": [
|
||||
"a40ba4537518361c13aab1d9b0648387f7c88aaa",
|
||||
[
|
||||
|
|
|
@ -5,42 +5,24 @@
|
|||
[HTMLElement interface: attribute ontransitionstart]
|
||||
expected: FAIL
|
||||
|
||||
[Document interface: attribute ontransitioncancel]
|
||||
expected: FAIL
|
||||
|
||||
[Document interface: document must inherit property "ontransitionstart" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Document interface: document must inherit property "ontransitioncancel" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[HTMLElement interface: attribute ontransitionrun]
|
||||
expected: FAIL
|
||||
|
||||
[HTMLElement interface: document must inherit property "ontransitioncancel" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Window interface: attribute ontransitionrun]
|
||||
expected: FAIL
|
||||
|
||||
[Window interface: attribute ontransitioncancel]
|
||||
expected: FAIL
|
||||
|
||||
[HTMLElement interface: document must inherit property "ontransitionstart" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[HTMLElement interface: attribute ontransitioncancel]
|
||||
expected: FAIL
|
||||
|
||||
[HTMLElement interface: document must inherit property "ontransitionrun" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Window interface: window must inherit property "ontransitionrun" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Window interface: window must inherit property "ontransitioncancel" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Document interface: attribute ontransitionstart]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[transitioncancel-001.html]
|
||||
[transitioncancel should be fired if the element is made display:none during the transition]
|
||||
expected: FAIL
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CSS Transitions Test: Removing transitioning property from transition-property triggers transitioncancel</title>
|
||||
<link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com">
|
||||
<meta name="assert" content="Removing transitioning property from transition-property
|
||||
causes transitioncancel.">
|
||||
<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#event-dispatch">
|
||||
|
||||
<script src="/resources/testharness.js" type="text/javascript"></script>
|
||||
<script src="/resources/testharnessreport.js" type="text/javascript"></script>
|
||||
<script src="./support/helper.js" type="text/javascript"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
promise_test(async t => {
|
||||
// Create element and prepare to trigger a transition on it.
|
||||
const div = addDiv(t, {
|
||||
style: 'transition: background-color 0.25s; background-color: red;',
|
||||
});
|
||||
|
||||
// Attach event listeners
|
||||
const eventWatcher = new EventWatcher(t, div, ['transitioncancel']);
|
||||
div.addEventListener('transitionend', t.step_func((event) => {
|
||||
assert_unreached('transitionend event should not be fired');
|
||||
}));
|
||||
|
||||
// Trigger transition
|
||||
getComputedStyle(div).backgroundColor;
|
||||
div.style.backgroundColor = 'green';
|
||||
getComputedStyle(div).backgroundColor;
|
||||
|
||||
// Remove the transitioning property from transition-property asynchronously.
|
||||
await waitForFrame();
|
||||
div.style.transitionProperty = 'none';
|
||||
|
||||
await eventWatcher.wait_for('transitioncancel');
|
||||
}, 'Removing a transitioning property from transition-property should trigger transitioncancel');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue