compositor: Always send an animating tick when a pipeline starts animating (#37507)

Instead of taking into account whether the entire WebView starts
animating, always send an animation tick when a pipeline moves from the
"not animating" to "animating" state. It could be that the WebView was
animating, but not painting if the animation was not producing display
lists. In that case, the required tick would never come, because it is
sent after a repaint.

Testing: Added a new WPT test.
Fixes: #37458.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2025-06-17 19:47:42 +02:00 committed by GitHub
parent ded753f01b
commit 56e901d0c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 112 additions and 10 deletions

View file

@ -279,13 +279,14 @@ impl WebViewRenderer {
}
/// Sets or unsets the animations-running flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
/// true if the pipeline has started animating.
pub(crate) fn change_pipeline_running_animations_state(
&mut self,
pipeline_id: PipelineId,
animation_state: AnimationState,
) -> bool {
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
let was_animating = pipeline_details.animating();
match animation_state {
AnimationState::AnimationsPresent => {
pipeline_details.animations_running = true;
@ -300,23 +301,38 @@ impl WebViewRenderer {
pipeline_details.animation_callbacks_running = false;
},
}
self.update_animation_state()
let started_animating = !was_animating && pipeline_details.animating();
self.update_animation_state();
// It's important that an animation tick is triggered even if the
// WebViewRenderer's overall animation state hasn't changed. It's possible that
// the WebView was animating, but not producing new display lists. In that case,
// no repaint will happen and thus no repaint will trigger the next animation tick.
started_animating
}
/// Sets or unsets the throttled flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
/// true if the pipeline has started animating.
pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) -> bool {
self.ensure_pipeline_details(pipeline_id).throttled = throttled;
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
let was_animating = pipeline_details.animating();
pipeline_details.throttled = throttled;
let started_animating = !was_animating && pipeline_details.animating();
// Throttling a pipeline can cause it to be taken into the "not-animating" state.
self.update_animation_state()
self.update_animation_state();
// It's important that an animation tick is triggered even if the
// WebViewRenderer's overall animation state hasn't changed. It's possible that
// the WebView was animating, but not producing new display lists. In that case,
// no repaint will happen and thus no repaint will trigger the next animation tick.
started_animating
}
pub(crate) fn update_animation_state(&mut self) -> bool {
let animating = self.pipelines.values().any(PipelineDetails::animating);
let old_animating = std::mem::replace(&mut self.animating, animating);
self.webview.set_animating(self.animating);
old_animating != self.animating
fn update_animation_state(&mut self) {
self.animating = self.pipelines.values().any(PipelineDetails::animating);
self.webview.set_animating(self.animating());
}
/// On a Window refresh tick (e.g. vsync)

View file

@ -454483,6 +454483,10 @@
"59843ae54b64f6ce4f7e616d4be491c911ea84cf",
[]
],
"transition-in-iframe-001-iframe.html": [
"b7214bd378bd60b2729bc276bd775a389fd4ebcb",
[]
],
"two.gif": [
"01435c80209d533dc2164ac48279574c7ba4615e",
[]
@ -614633,6 +614637,13 @@
{}
]
],
"transition-in-iframe-001.html": [
"2e5e64de2308aa0c1f176c1d047cd178cac5d3ed",
[
null,
{}
]
],
"transition-property-001.html": [
"47a1417070f0c2e7b8171259d9c4f63c44e96bcc",
[

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<body>
<style>
html {
background: red;
transition: all 0.1s;
}
</style>
<script>
window.addEventListener("message", () => {
document.documentElement.style.background = "green";
});
document.documentElement.addEventListener(
"transitionend", () => {
window.parent.postMessage("complete", "*");
}
);
</script>
</body>
</html>

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<title>Transitions: Transition in &lt;iframe&gt; on page with empty rAF finishes</title>
<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/">
<link rel="author" href="mailto:mrobinson@igalia.com">
<link rel="author" href="mailto:obrufau@igalia.com">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/rendering-utils.js"></script>
<style>
#iframe {
width: 100px;
height: 100px;
border: none;
}
</style>
<script>
let rAFId = null;
function triggerNeverEndingUselessRAF() {
rAFId = requestAnimationFrame(triggerNeverEndingUselessRAF);
}
promise_test(async t => {
await waitForAtLeastOneFrame();
await waitForAtLeastOneFrame();
let iframe = document.createElement("iframe");
iframe.id = "iframe";
iframe.src = "support/transition-in-iframe-001-iframe.html"
iframe.sandbox = "allow-scripts";
iframe.addEventListener("load", async () => {
await waitForAtLeastOneFrame();
await waitForAtLeastOneFrame();
iframe.contentWindow.postMessage("loaded", "*");
});
triggerNeverEndingUselessRAF();
document.body.appendChild(iframe);
await new Promise(resolve => {
window.addEventListener("message", () => {
cancelAnimationFrame(rAFId);
resolve();
});
});
});
</script>
</html>