use bevy::image::Image;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, ScrollPosition};
use super::stats::{FlushMeta, OpApplyStats};
use super::{create, update};
use crate::bridge::{JsBridge, ReactNode, SpanKind};
use crate::plugin::Fonts;
use crate::protocol::{NodeId, ROOT_ID, op::Op};
use crate::transition::ScrollTransitionState;
#[allow(clippy::too_many_arguments)]
pub fn apply_js_ops(
mut commands: Commands,
mut bridge: ResMut<JsBridge>,
assets: Res<AssetServer>,
fonts: Res<Fonts>,
mut images: ResMut<Assets<Image>>,
children: Query<&Children>,
rnodes: Query<&ReactNode>,
mut scroll_query: Query<(
&mut ScrollPosition,
&ComputedNode,
Option<&mut ScrollTransitionState>,
)>,
mut stats: ResMut<OpApplyStats>,
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))] meta: FlushMeta,
) {
let mut ops: Vec<Op> = Vec::new();
#[cfg_attr(target_arch = "wasm32", allow(unused_mut, unused_variables))]
let mut batches = 0usize;
while let Ok(batch) = bridge.ops_rx.try_recv() {
ops.extend(batch);
batches += 1;
}
if ops.is_empty() {
return;
}
let op_count = ops.len();
#[cfg(not(target_arch = "wasm32"))]
let started = std::time::Instant::now();
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
let (first_stamp, any_app) = match &meta.flushes {
None => (None, true),
Some(flushes) => {
let (mut first, mut any_app, mut received) = (None, false, 0);
for info in flushes.0.try_iter().take(batches) {
first = first.or(info.sent);
any_app |= !info.devtools;
received += 1;
}
(first, any_app || received < batches)
}
};
tracing::debug!("applying {op_count} reconciler op(s)");
let mut dirty: HashMap<NodeId, ParentDirt> = HashMap::new();
let mut removals: Vec<(NodeId, Entity)> = Vec::new();
let mut removed_under: HashMap<NodeId, usize> = HashMap::new();
let shared_pairs = crate::shared_tags::plan_pairs(&bridge, &ops);
for pair in &shared_pairs {
if let Some(&outgoing) = bridge.nodes.get(&pair.outgoing) {
let incoming = pair.incoming;
commands.queue(move |world: &mut World| {
crate::transition::shared::snapshot_into_pending(world, outgoing, incoming)
});
}
}
for op in ops {
match op {
Op::Reset => {
stats.reset_count += 1;
if let Some(&root) = bridge.nodes.get(&ROOT_ID)
&& let Ok(kids) = children.get(root)
{
for child in kids.iter() {
if rnodes.contains(child) {
commands.entity(child).despawn();
}
}
}
for id in bridge.detached.iter() {
if let Some(&e) = bridge.nodes.get(id) {
commands.entity(e).try_despawn();
}
}
bridge.nodes.retain(|&id, _| id == ROOT_ID);
bridge.names.clear();
bridge.shared_tags.clear();
commands.queue(crate::transition::shared::clear_pending);
bridge.props_cache.clear();
bridge.text_styles.clear();
bridge.spans.clear();
bridge.detached.clear();
bridge.animated.clear();
bridge.scroll_positions.clear();
bridge.siblings.clear();
bridge.child_list.clear();
bridge.parent_of.clear();
bridge.detached_parent.clear();
bridge.child_detached.clear();
dirty.clear();
removed_under.clear();
}
Op::Create {
id,
kind,
props,
text,
} => {
create::apply_create(
&mut commands,
&mut bridge,
&assets,
&fonts,
&mut images,
id,
kind,
props,
text,
);
}
Op::CreateText { id, text } => {
let entity = commands
.spawn((
Text::new(text),
TextColor(Color::WHITE),
ReactNode(id),
crate::ext::ElementFlags::NODE,
))
.id();
bridge.nodes.insert(id, entity);
}
Op::CreateTextSpan { id, text } => {
let entity = commands
.spawn((
TextSpan(text),
ReactNode(id),
crate::ext::ElementFlags::NODE_LESS,
))
.id();
bridge.nodes.insert(id, entity);
bridge.spans.insert(id, SpanKind::RawInherited);
}
Op::Append { parent, child } => {
if bridge.is_detached(child) {
bridge.attach_detached(child, parent);
continue;
}
if let (Some(p), Some(c)) = (resolve(&bridge, parent), resolve(&bridge, child)) {
let old_parent = bridge.parent_of.get(&child).copied();
let same_parent = old_parent == Some(parent);
bridge.layer_dirty.insert(parent);
crate::layer::mark_content_dirty(&mut commands.entity(p));
bridge.append_child(parent, child);
if same_parent {
ParentDirt::rebuild(&mut dirty, parent);
} else {
if let Some(old) = old_parent {
ParentDirt::escalate(&mut dirty, old);
}
commands.entity(p).add_child(c);
}
inherit_text_style(&mut commands, &bridge, parent, child, c);
}
}
Op::Insert {
parent,
child,
before,
} => {
if bridge.is_detached(child) {
bridge.attach_detached(child, parent);
continue;
}
if let (Some(p), Some(c)) = (resolve(&bridge, parent), resolve(&bridge, child)) {
let old_parent = bridge.parent_of.get(&child).copied();
let before_attached = bridge.parent_of.get(&before) == Some(&parent);
bridge.layer_dirty.insert(parent);
crate::layer::mark_content_dirty(&mut commands.entity(p));
bridge.insert_before(parent, child, before);
match old_parent {
Some(old) if old == parent => ParentDirt::rebuild(&mut dirty, parent),
Some(old) => {
ParentDirt::escalate(&mut dirty, old);
commands.entity(p).add_child(c);
ParentDirt::rebuild(&mut dirty, parent);
}
None => {
commands.entity(p).add_child(c);
if before_attached {
ParentDirt::insert(&mut dirty, parent, child, before);
}
}
}
inherit_text_style(&mut commands, &bridge, parent, child, c);
}
}
Op::Remove { parent, child } => {
bridge.layer_dirty.insert(parent);
let count = removed_under.entry(parent).or_insert(0);
*count += 1;
if *count == 1
&& let Some(p) = resolve(&bridge, parent)
{
crate::layer::mark_content_dirty(&mut commands.entity(p));
}
ParentDirt::escalate(&mut dirty, parent);
let mut detached = bridge.detached_under(child);
if bridge.is_detached(child) {
bridge.detach_detached(child);
detached.push(child);
}
for s in detached {
if let Some(se) = resolve(&bridge, s) {
commands.entity(se).despawn();
}
bridge.detach(s);
bridge.forget_subtree(s);
}
if let Some(c) = resolve(&bridge, child) {
removals.push((parent, c));
bridge.detach(child);
bridge.forget_subtree(child);
}
}
Op::Update {
id,
props,
unset,
style_unset,
} => {
update::apply_update(
&mut commands,
&mut bridge,
&assets,
&fonts,
&children,
&rnodes,
&mut scroll_query,
id,
props,
unset,
style_unset,
);
}
Op::UpdateText { id, text } => {
if let Some(e) = resolve(&bridge, id) {
if bridge.spans.contains_key(&id) {
commands.entity(e).insert(TextSpan(text));
} else {
commands.entity(e).insert(Text::new(text));
}
crate::layer::mark_content_dirty(&mut commands.entity(e));
}
}
}
}
for pair in shared_pairs {
let id = pair.incoming;
match bridge.nodes.get(&id) {
Some(&incoming) => commands.queue(move |world: &mut World| {
crate::transition::shared::stamp_pending(world, id, incoming)
}),
None => commands.queue(move |world: &mut World| {
crate::transition::shared::discard_pending(world, id)
}),
}
}
for (&parent, dirt) in &dirty {
let Some(p) = resolve(&bridge, parent) else {
continue;
};
if let ParentDirt::InsertsOnly(inserts) = dirt
&& let Some(planned) = inserts
.iter()
.map(|&(child, before)| Some((resolve(&bridge, child)?, resolve(&bridge, before)?)))
.collect::<Option<Vec<_>>>()
{
for (c, b) in planned {
commands.entity(p).queue(move |mut parent: EntityWorldMut| {
let Some(index) = parent
.get::<Children>()
.and_then(|kids| kids.iter().position(|e| e == b))
else {
return;
};
parent.insert_children(index, &[c]);
});
}
continue;
}
let list: Vec<Entity> = bridge
.children_of(parent)
.filter_map(|id| resolve(&bridge, id))
.collect();
commands.entity(p).replace_children(&list);
}
let mut mass: HashMap<NodeId, (Entity, Vec<Entity>)> = HashMap::new();
for (parent, e) in removals.into_iter().rev() {
if removed_under.get(&parent).copied().unwrap_or(0) >= ParentDirt::MASS_REMOVAL_MIN
&& !dirty.contains_key(&parent)
&& let Some(p) = resolve(&bridge, parent)
{
mass.entry(parent)
.or_insert_with(|| (p, Vec::new()))
.1
.push(e);
} else {
commands.entity(e).try_despawn();
}
}
for (p, removed) in mass.into_values() {
commands.queue(move |world: &mut World| despawn_detached(world, p, removed));
}
stats.applied_count = stats.applied_count.wrapping_add(1);
if any_app {
stats.app_applied_count = stats.app_applied_count.wrapping_add(1);
}
stats.last_ops = op_count;
#[cfg(not(target_arch = "wasm32"))]
{
let end = std::time::Instant::now();
let (wait, pre) = first_stamp
.map(|stamp| {
super::stats::split_pre_apply(stamp, meta.frame.as_ref().and_then(|f| f.0), started)
})
.unwrap_or_default();
stats.last_frame_wait = wait;
stats.last_pre_apply = pre;
stats.last_translate = end.duration_since(started);
stats.last_apply_end = Some(end);
}
}
enum ParentDirt {
InsertsOnly(Vec<(NodeId, NodeId)>),
Rebuild,
}
impl ParentDirt {
const POSITIONAL_CAP: usize = 16;
const MASS_REMOVAL_MIN: usize = 32;
fn rebuild(dirty: &mut HashMap<NodeId, ParentDirt>, parent: NodeId) {
dirty.insert(parent, ParentDirt::Rebuild);
}
fn insert(
dirty: &mut HashMap<NodeId, ParentDirt>,
parent: NodeId,
child: NodeId,
before: NodeId,
) {
match dirty
.entry(parent)
.or_insert_with(|| ParentDirt::InsertsOnly(Vec::new()))
{
ParentDirt::InsertsOnly(inserts) if inserts.len() < Self::POSITIONAL_CAP => {
inserts.push((child, before));
}
slot @ ParentDirt::InsertsOnly(_) => *slot = ParentDirt::Rebuild,
ParentDirt::Rebuild => {}
}
}
fn escalate(dirty: &mut HashMap<NodeId, ParentDirt>, parent: NodeId) {
if let Some(slot) = dirty.get_mut(&parent) {
*slot = ParentDirt::Rebuild;
}
}
}
fn inherit_text_style(
commands: &mut Commands,
bridge: &JsBridge,
parent: NodeId,
child: NodeId,
child_entity: Entity,
) {
if bridge.spans.get(&child) != Some(&SpanKind::RawInherited) {
return;
}
if let Some(style) = bridge.text_styles.get(&parent).cloned() {
commands.entity(child_entity).insert(style);
}
}
pub(super) fn resolve(bridge: &JsBridge, id: NodeId) -> Option<Entity> {
bridge.nodes.get(&id).copied()
}
fn despawn_detached(world: &mut World, parent: Entity, removed: Vec<Entity>) {
use bevy::ecs::entity::EntityHashSet;
use bevy::ecs::relationship::RelationshipTarget;
let taken = world.get_entity_mut(parent).ok().and_then(|mut p| {
p.get_mut::<Children>()
.map(|mut c| std::mem::take(c.collection_mut_risky()))
});
for &e in &removed {
if let Ok(e) = world.get_entity_mut(e) {
e.despawn();
}
}
let Some(mut survivors) = taken else {
return;
};
let removed: EntityHashSet = removed.into_iter().collect();
survivors.retain(|e| !removed.contains(e));
if let Ok(mut p) = world.get_entity_mut(parent) {
if survivors.is_empty() {
p.remove::<Children>();
} else if let Some(mut c) = p.get_mut::<Children>() {
*c.collection_mut_risky() = survivors;
} else {
p.insert(Children::from_collection_risky(survivors));
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_util::{children_of, create_node, ent, ordering_app};
use super::*;
#[test]
fn append_builds_child_order() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1), create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 2), ent(&app, 3), ent(&app, 4)],
);
}
#[test]
fn insert_reorders_existing_child() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
tx.send(vec![Op::Insert {
parent: 1,
child: 4,
before: 2,
}])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 4), ent(&app, 2), ent(&app, 3)],
"C should move to the front: [C, A, B]"
);
}
#[test]
fn insert_new_child_in_the_middle() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
tx.send(vec![
create_node(5),
Op::Insert {
parent: 1,
child: 5,
before: 3,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 2), ent(&app, 5), ent(&app, 3), ent(&app, 4)],
"D should land before B: [A, D, B, C]"
);
}
#[test]
fn insert_orders_within_a_single_batch() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(10), create_node(11), create_node(12), Op::Append {
parent: ROOT_ID,
child: 10,
},
Op::Append {
parent: 10,
child: 12,
}, Op::Insert {
parent: 10,
child: 11,
before: 12,
}, ])
.unwrap();
app.update();
let parent = ent(&app, 10);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 11), ent(&app, 12)],
"X must precede Y even though Children was unreadable mid-batch"
);
}
#[test]
fn mixed_batch_orders_correctly() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
tx.send(vec![
create_node(5),
Op::Append {
parent: 1,
child: 5,
},
Op::Insert {
parent: 1,
child: 4,
before: 2,
},
Op::Remove {
parent: 1,
child: 3,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 4), ent(&app, 2), ent(&app, 5)],
"append + move + remove in one batch must land as [4, 2, 5]"
);
}
#[test]
fn move_between_parents_in_one_batch() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1), create_node(2), create_node(3),
create_node(4),
create_node(5),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: ROOT_ID,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
Op::Append {
parent: 2,
child: 5,
},
])
.unwrap();
app.update();
tx.send(vec![Op::Append {
parent: 2,
child: 3,
}])
.unwrap();
app.update();
let (a, b) = (ent(&app, 1), ent(&app, 2));
assert_eq!(
children_of(&app, a),
vec![ent(&app, 4)],
"the moved child must leave the old parent's Children"
);
assert_eq!(children_of(&app, b), vec![ent(&app, 5), ent(&app, 3)]);
assert_eq!(
app.world()
.entity(ent(&app, 3))
.get::<ChildOf>()
.map(|c| c.parent()),
Some(b),
"the moved child's ChildOf must point at the new parent"
);
}
#[test]
fn same_batch_create_under_removed_parent_despawns() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
Op::Append {
parent: ROOT_ID,
child: 1,
},
])
.unwrap();
app.update();
tx.send(vec![
create_node(2),
Op::Append {
parent: 1,
child: 2,
},
Op::Remove {
parent: ROOT_ID,
child: 1,
},
])
.unwrap();
app.update();
let survivors = app
.world_mut()
.query::<&ReactNode>()
.iter(app.world())
.count();
assert_eq!(
survivors, 0,
"the same-batch child must be despawned with its removed parent, not \
leaked as an orphaned root"
);
}
#[test]
fn remove_then_reorder_same_parent() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
tx.send(vec![
Op::Remove {
parent: 1,
child: 3,
},
Op::Insert {
parent: 1,
child: 4,
before: 2,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(children_of(&app, parent), vec![ent(&app, 4), ent(&app, 2)]);
}
#[test]
fn fresh_inserts_before_first_and_before_same_batch_insert() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
])
.unwrap();
app.update();
tx.send(vec![
create_node(4),
create_node(5),
Op::Insert {
parent: 1,
child: 4,
before: 2,
},
Op::Insert {
parent: 1,
child: 5,
before: 4,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 5), ent(&app, 4), ent(&app, 2), ent(&app, 3)],
"fresh inserts before the head and before a same-batch insert: [D, C, A, B]"
);
}
#[test]
fn fresh_insert_then_remove_of_its_anchor_rebuilds() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
create_node(3),
create_node(4),
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 1,
child: 3,
},
Op::Append {
parent: 1,
child: 4,
},
])
.unwrap();
app.update();
tx.send(vec![
create_node(5),
Op::Insert {
parent: 1,
child: 5,
before: 3,
},
Op::Remove {
parent: 1,
child: 3,
},
])
.unwrap();
app.update();
let parent = ent(&app, 1);
assert_eq!(
children_of(&app, parent),
vec![ent(&app, 2), ent(&app, 5), ent(&app, 4)],
"the inserted node must take its removed anchor's slot: [A, D, C]"
);
}
#[test]
fn update_text_on_inline_span_keeps_textspan() {
let (mut app, ops_tx, _root) = ordering_app();
ops_tx
.send(vec![
Op::Create {
id: 1,
kind: "text".into(),
props: Box::default(),
text: None,
},
Op::Create {
id: 2,
kind: "textSpan".into(),
props: Box::default(),
text: Some("0".into()),
},
Op::Append {
parent: 1,
child: 2,
},
])
.unwrap();
app.update();
ops_tx
.send(vec![Op::UpdateText {
id: 2,
text: "1".into(),
}])
.unwrap();
app.update();
let span = ent(&app, 2);
assert_eq!(
app.world().entity(span).get::<TextSpan>().map(|s| &*s.0),
Some("1"),
"the span's TextSpan must hold the updated text"
);
assert!(
app.world().entity(span).get::<Text>().is_none(),
"a span must never gain a Text component (that renders a duplicate)"
);
}
#[test]
fn reset_despawns_detached_roots() {
let (mut app, tx, _ui_root) = ordering_app();
tx.send(vec![
Op::Create {
id: 1,
kind: "root".into(),
props: Box::default(),
text: None,
},
Op::Append {
parent: ROOT_ID,
child: 1,
},
])
.unwrap();
app.update();
let root_e = ent(&app, 1);
tx.send(vec![Op::Reset]).unwrap();
app.update();
assert!(
!app.world().entities().contains(root_e),
"Op::Reset must despawn detached <root>s"
);
assert!(
app.world().resource::<JsBridge>().detached.is_empty(),
"Op::Reset must clear the detached set"
);
}
#[test]
fn reset_keeps_infrastructure_children() {
let (mut app, tx, root) = ordering_app();
let infra = app.world_mut().spawn(ChildOf(root)).id();
tx.send(vec![
create_node(1),
Op::Append {
parent: ROOT_ID,
child: 1,
},
])
.unwrap();
app.update();
let node = ent(&app, 1);
tx.send(vec![Op::Reset]).unwrap();
app.update();
assert!(
app.world().entities().contains(infra),
"Op::Reset must keep non-reconciler children of the root"
);
assert!(
!app.world().entities().contains(node),
"Op::Reset must despawn reconciler nodes"
);
}
#[test]
fn remove_ancestor_despawns_nested_detached_root() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1), super::super::test_util::create(2, "root", serde_json::json!({})),
create_node(3), Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
}, Op::Append {
parent: 2,
child: 3,
}, ])
.unwrap();
app.update();
let wrapper = ent(&app, 1);
let detached = ent(&app, 2);
let inner = ent(&app, 3);
assert!(app.world().entities().contains(detached));
tx.send(vec![Op::Remove {
parent: ROOT_ID,
child: 1,
}])
.unwrap();
app.update();
assert!(
!app.world().entities().contains(wrapper),
"the removed wrapper is despawned"
);
assert!(
!app.world().entities().contains(detached),
"the detached root nested under the removed wrapper must be despawned"
);
assert!(
!app.world().entities().contains(inner),
"the detached root's own subtree is despawned with it"
);
let bridge = app.world().resource::<JsBridge>();
assert!(
bridge.detached.is_empty(),
"detached bookkeeping is cleared"
);
assert!(
!bridge.nodes.contains_key(&2),
"the detached node id is forgotten"
);
assert!(
bridge.child_detached.is_empty() && bridge.detached_parent.is_empty(),
"detached parentage maps are cleared"
);
}
#[test]
fn remove_subtree_forgets_descendant_node_data() {
let (mut app, tx, _root) = ordering_app();
tx.send(vec![
create_node(1),
create_node(2),
Op::Create {
id: 3,
kind: "text".into(),
props: Box::default(),
text: Some("leaf".into()),
},
Op::Append {
parent: ROOT_ID,
child: 1,
},
Op::Append {
parent: 1,
child: 2,
},
Op::Append {
parent: 2,
child: 3,
},
])
.unwrap();
app.update();
let mid = ent(&app, 2);
let leaf = ent(&app, 3);
assert!(
app.world()
.resource::<JsBridge>()
.text_styles
.contains_key(&3),
"the text descendant is tracked before removal"
);
tx.send(vec![Op::Remove {
parent: ROOT_ID,
child: 1,
}])
.unwrap();
app.update();
assert!(
!app.world().entities().contains(mid),
"the descendant mid node is despawned with the subtree"
);
assert!(
!app.world().entities().contains(leaf),
"the descendant leaf node is despawned with the subtree"
);
let bridge = app.world().resource::<JsBridge>();
assert!(
!bridge.nodes.contains_key(&1),
"the removed root is forgotten"
);
assert!(
!bridge.nodes.contains_key(&2),
"the descendant mid node id is forgotten (no stale entity handle)"
);
assert!(
!bridge.nodes.contains_key(&3),
"the descendant leaf node id is forgotten (no stale entity handle)"
);
assert!(
!bridge.text_styles.contains_key(&3),
"the descendant text is dropped from the text_styles table"
);
}
#[test]
fn mass_removal_detaches_children_in_one_pass() {
let (mut app, tx, _root) = ordering_app();
let n = 100u32;
assert!(n as usize / 2 >= ParentDirt::MASS_REMOVAL_MIN);
let mut ops = vec![
create_node(1),
Op::Append {
parent: ROOT_ID,
child: 1,
},
];
for i in 0..n {
ops.push(create_node(10 + i));
ops.push(Op::Append {
parent: 1,
child: 10 + i,
});
}
ops.push(create_node(1000));
ops.push(Op::Append {
parent: 11,
child: 1000,
});
tx.send(ops).unwrap();
app.update();
let parent = ent(&app, 1);
let grandchild = ent(&app, 1000);
assert_eq!(children_of(&app, parent).len(), n as usize);
let removed: Vec<Entity> = (0..n)
.filter(|i| i % 2 == 1)
.map(|i| ent(&app, 10 + i))
.collect();
let expected: Vec<Entity> = (0..n)
.filter(|i| i % 2 == 0)
.map(|i| ent(&app, 10 + i))
.collect();
tx.send(
(0..n)
.filter(|i| i % 2 == 1)
.map(|i| Op::Remove {
parent: 1,
child: 10 + i,
})
.collect(),
)
.unwrap();
app.update();
assert_eq!(
children_of(&app, parent),
expected,
"survivors keep their order"
);
for e in &removed {
assert!(
!app.world().entities().contains(*e),
"removed child despawned"
);
}
assert!(
!app.world().entities().contains(grandchild),
"a removed child's subtree is despawned with it"
);
for e in &expected {
assert_eq!(
app.world().get::<ChildOf>(*e).map(|c| c.parent()),
Some(parent),
"survivors stay ChildOf(parent)"
);
}
let bridge = app.world().resource::<JsBridge>();
assert!(
!bridge.nodes.contains_key(&11) && !bridge.nodes.contains_key(&1000),
"removed ids are forgotten"
);
tx.send(vec![Op::Remove {
parent: 1,
child: 10,
}])
.unwrap();
app.update();
assert_eq!(children_of(&app, parent), expected[1..].to_vec());
tx.send(
(2..n)
.filter(|i| i % 2 == 0)
.map(|i| Op::Remove {
parent: 1,
child: 10 + i,
})
.collect(),
)
.unwrap();
app.update();
assert!(
app.world().get::<Children>(parent).is_none(),
"an emptied Children is removed"
);
assert_eq!(
app.world_mut()
.query::<&ReactNode>()
.iter(app.world())
.count(),
1,
"only the container remains"
);
}
}