diff --git a/packages/blitz-dom/src/stylo.rs b/packages/blitz-dom/src/stylo.rs index cd5f3c7b6b..e855d6a936 100644 --- a/packages/blitz-dom/src/stylo.rs +++ b/packages/blitz-dom/src/stylo.rs @@ -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 { diff --git a/packages/blitz-dom/tests/stylo_test.rs b/packages/blitz-dom/tests/stylo_test.rs new file mode 100644 index 0000000000..d527192a75 --- /dev/null +++ b/packages/blitz-dom/tests/stylo_test.rs @@ -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); +} diff --git a/packages/blitz-html/tests/html_document_test.rs b/packages/blitz-html/tests/html_document_test.rs new file mode 100644 index 0000000000..74ef9d25ea --- /dev/null +++ b/packages/blitz-html/tests/html_document_test.rs @@ -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#" + +