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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/blitz-dom/src/stylo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,17 @@ impl crate::document::BaseDocument {
.flush(&guards)
.process_style(root, Some(&self.snapshots));

// Mark actively animating nodes as dirty
// Mark actively animating nodes as dirty.
// Use get_mut to skip stale entries: stylo can hold animation keys for
// nodes already removed from the slab, and direct indexing panics on
// invalid keys. See https://github.com/DioxusLabs/blitz/issues/407
let mut sets = self.animations.sets.write();
for (key, set) in sets.iter_mut() {
let node_id = key.node.id();
self.nodes[node_id].set_restyle_hint(RestyleHint::RESTYLE_SELF);
let Some(node) = self.nodes.get_mut(node_id) else {
continue; // stale animation entry — node removed from slab
};
node.set_restyle_hint(RestyleHint::RESTYLE_SELF);

for animation in set.animations.iter_mut() {
if animation.state == AnimationState::Pending && animation.started_at <= now {
Expand Down
9 changes: 9 additions & 0 deletions packages/blitz-dom/tests/stylo_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use blitz_dom::{BaseDocument, DocumentConfig};

/// Smoke-test: resolve on an empty document must not panic.
#[test]
fn resolve_empty_document_does_not_panic() {
let mut doc = BaseDocument::new(DocumentConfig::default());
doc.resolve(0.0);
doc.resolve(0.1);
}
28 changes: 28 additions & 0 deletions packages/blitz-html/tests/html_document_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use blitz_dom::DocumentConfig;
use blitz_html::HtmlDocument;

/// Regression test for https://github.com/DioxusLabs/blitz/issues/407
///
/// `resolve_stylist` used to panic with "invalid key" when a CSS-animated node
/// was removed between two `resolve` calls. Verify the second resolve is safe.
#[test]
fn resolve_does_not_panic_after_removing_animated_node() {
let html = r#"
<style>
@keyframes pulse { 0% { opacity: 1; } 100% { opacity: 0.5; } }
.pulse { animation: pulse 2s infinite; }
</style>
<div id="pulse-node" class="pulse">animated</div>
"#;

let mut doc = HtmlDocument::from_html(html, DocumentConfig::default());
doc.resolve(0.0);

let pulse_id = doc.get_element_by_id("pulse-node");
if let Some(id) = pulse_id {
doc.mutate().remove_and_drop_node(id);
}

// Must not panic: stale animation entry should be skipped safely.
doc.resolve(0.1);
}