bevy_ecss/
system.rs

1use bevy::{
2    ecs::{
3        component::ComponentTicks,
4        system::{SystemParam, SystemState},
5    },
6    log::{debug, error, trace},
7    prelude::{
8        AssetEvent, AssetId, Assets, Changed, Children, Component, Deref, DerefMut, Entity,
9        EventReader, Mut, Name, Query, Res, ResMut, Resource, With, World,
10    },
11    ui::{Interaction, Node},
12    utils::HashMap,
13};
14use smallvec::SmallVec;
15
16use crate::{
17    component::{Class, MatchSelectorElement, StyleSheet},
18    property::{SelectedEntities, StyleSheetState, TrackedEntities},
19    selector::{PseudoClassElement, Selector, SelectorElement},
20    StyleSheetAsset,
21};
22
23/// Utility trait which helps to deal with dynamic components
24/// Each trait is implemented for a [`SystemState<T>`] with a single `[Component]`
25pub(crate) trait ComponentFilter {
26    /// Query the world and returns only the which has the component.
27    fn filter(&mut self, world: &World) -> SmallVec<[Entity; 8]>;
28
29    /// Return the change ticks of the component on the given entity.
30    fn get_change_ticks(&self, world: &World, entity: Entity) -> Option<ComponentTicks>;
31}
32
33impl<'w, 's, T: Component> ComponentFilter for SystemState<Query<'w, 's, Entity, With<T>>> {
34    fn filter(&mut self, world: &World) -> SmallVec<[Entity; 8]> {
35        self.get(world).iter().collect()
36    }
37
38    fn get_change_ticks(&self, world: &World, entity: Entity) -> Option<ComponentTicks> {
39        world.entity(entity).get_change_ticks::<T>()
40    }
41}
42
43/// Holds the registered [`ComponentFilter`] using the component name as key.
44#[derive(Default, Resource, Deref, DerefMut)]
45pub(crate) struct ComponentFilterRegistry(
46    pub HashMap<&'static str, Box<dyn ComponentFilter + Send + Sync>>,
47);
48
49/// An utility [`SystemParam`] query which is used in [`prepare`] system.
50#[derive(SystemParam)]
51pub(crate) struct CssQueryParam<'w, 's> {
52    assets: Res<'w, Assets<StyleSheetAsset>>,
53    nodes: Query<
54        'w,
55        's,
56        (Entity, Option<&'static Children>, &'static StyleSheet),
57        Changed<StyleSheet>,
58    >,
59    names: Query<'w, 's, (Entity, &'static Name)>,
60    classes: Query<'w, 's, (Entity, &'static Class)>,
61    children: Query<'w, 's, &'static Children, With<Node>>,
62    any: Query<'w, 's, Entity, With<Node>>,
63}
64
65/// Holds an previous prepared [`CssQueryParam`];
66#[derive(Deref, DerefMut, Resource)]
67pub(crate) struct PrepareParams(SystemState<CssQueryParam<'static, 'static>>);
68
69impl PrepareParams {
70    pub fn new(world: &mut World) -> Self {
71        Self(SystemState::new(world))
72    }
73}
74
75/// Exclusive system which selects all entities and prepare the internal state used by [`Property`](crate::Property) systems.
76pub(crate) fn prepare(world: &mut World) {
77    world.resource_scope(|world, mut params: Mut<PrepareParams>| {
78        world.resource_scope(|world, mut registry: Mut<ComponentFilterRegistry>| {
79            let css_query = params.get(world);
80            let state = prepare_state(world, css_query, &mut registry);
81
82            if state.has_any_selected_entities() {
83                let mut state_res = world
84                    .get_resource_mut::<StyleSheetState>()
85                    .expect("Should be added by plugin");
86
87                *state_res = state;
88            }
89        });
90    });
91}
92
93/// Prepare state to be used by [`Property`](crate::Property) systems
94pub(crate) fn prepare_state(
95    world: &World,
96    css_query: CssQueryParam,
97    registry: &mut ComponentFilterRegistry,
98) -> StyleSheetState {
99    let mut state = StyleSheetState::default();
100
101    for (root, maybe_children, sheet_handle) in &css_query.nodes {
102        for id in sheet_handle.handles().iter().map(|h| h.id()) {
103            if let Some(sheet) = css_query.assets.get(id) {
104                let mut tracked_entities = TrackedEntities::default();
105                let mut selected_entities = SelectedEntities::default();
106                debug!("Applying style {}", sheet.path());
107
108                for rule in sheet.iter() {
109                    let entities = select_entities(
110                        root,
111                        maybe_children,
112                        &rule.selector,
113                        world,
114                        &css_query,
115                        registry,
116                        &mut tracked_entities,
117                    );
118
119                    trace!(
120                        "Applying rule ({}) on {} entities",
121                        rule.selector.to_string(),
122                        entities.len()
123                    );
124
125                    selected_entities.push((rule.selector.clone(), entities));
126                }
127
128                selected_entities.sort_by(|(a, _), (b, _)| a.weight.cmp(&b.weight));
129                state.push((id, tracked_entities, selected_entities));
130            }
131        }
132    }
133
134    state
135}
136
137/// Select all entities using the given [`Selector`](crate::Selector).
138///
139/// If no [`Children`] is supplied, then the selector is applied only on root entity.
140fn select_entities(
141    root: Entity,
142    maybe_children: Option<&Children>,
143    selector: &Selector,
144    world: &World,
145    css_query: &CssQueryParam,
146    registry: &mut ComponentFilterRegistry,
147    tracked_entities: &mut TrackedEntities,
148) -> SmallVec<[Entity; 8]> {
149    let mut parent_tree = selector.get_parent_tree();
150
151    if parent_tree.is_empty() {
152        return SmallVec::new();
153    }
154
155    // Build an entity tree with all entities that may be selected.
156    // This tree is composed of the entity root and all descendants entities.
157    let mut entity_tree = std::iter::once(root)
158        .chain(
159            maybe_children
160                .map(|children| get_children_recursively(children, &css_query.children))
161                .unwrap_or_default(),
162        )
163        .collect::<SmallVec<_>>();
164
165    loop {
166        // TODO: Rework this to use a index to avoid recreating parent_tree every time the systems runs.
167        // This is has little to no impact on performance, since this system doesn't runs often.
168        let node = parent_tree.remove(0);
169
170        let entities = select_entities_node(
171            node,
172            world,
173            css_query,
174            registry,
175            entity_tree.clone(),
176            tracked_entities,
177        );
178
179        if parent_tree.is_empty() {
180            break entities;
181        } else {
182            entity_tree = entities
183                .into_iter()
184                .filter_map(|e| css_query.children.get(e).ok())
185                .flat_map(|children| get_children_recursively(children, &css_query.children))
186                .collect();
187        }
188    }
189}
190
191#[derive(Debug, Default, Clone, Deref, DerefMut)]
192struct FilteredEntities(SmallVec<[Entity; 8]>);
193
194#[derive(Debug, Default, Clone, Deref, DerefMut)]
195struct MatchedEntities(SmallVec<[Entity; 8]>);
196
197/// Filter entities matching the given selectors.
198/// This function is called once per node on tree returned by [`get_parent_tree`](Selector::get_parent_tree)
199fn select_entities_node(
200    node: SmallVec<[&SelectorElement; 8]>,
201    world: &World,
202    css_query: &CssQueryParam,
203    registry: &mut ComponentFilterRegistry,
204    entities: SmallVec<[Entity; 8]>,
205    tracked_entities: &mut TrackedEntities,
206) -> SmallVec<[Entity; 8]> {
207    node.into_iter().fold(entities, |entities, element| {
208        let (filtered, matched) = match element {
209            SelectorElement::Name(name) => {
210                get_entities_with(name.as_str(), &css_query.names, entities)
211            }
212            SelectorElement::Class(class) => {
213                get_entities_with(class.as_str(), &css_query.classes, entities)
214            }
215            SelectorElement::Component(component) => {
216                get_entities_with_component(component.as_str(), world, registry, entities)
217            }
218            SelectorElement::PseudoClass(pseudo_class) => {
219                get_entities_with_pseudo_class(world, *pseudo_class, entities.clone())
220            }
221            SelectorElement::Any => get_entities_with_any_component(&css_query.any, entities),
222            // All child elements are filtered by [`get_parent_tree`](Selector::get_parent_tree)
223            SelectorElement::Child => unreachable!(),
224        };
225
226        if !matched.is_empty() {
227            trace!("Tracking element {:?}: {}", element, matched.len());
228
229            tracked_entities
230                .entry(element.clone())
231                .or_default()
232                .extend(matched.0);
233        }
234
235        filtered.0
236    })
237}
238
239/// Utility function to filter any entities by using a component with implements [`MatchSelectorElement`]
240/// Returns new filtered list of entities and a list of entities matched by the query.
241fn get_entities_with<T>(
242    name: &str,
243    query: &Query<(Entity, &'static T)>,
244    entities: SmallVec<[Entity; 8]>,
245) -> (FilteredEntities, MatchedEntities)
246where
247    T: Component + MatchSelectorElement,
248{
249    let entities = query
250        .iter()
251        .filter_map(|(e, rhs)| {
252            if entities.contains(&e) && rhs.matches(name) {
253                Some(e)
254            } else {
255                None
256            }
257        })
258        .collect::<SmallVec<_>>();
259
260    (
261        FilteredEntities(entities.clone()),
262        MatchedEntities(entities),
263    )
264}
265
266/// Utility function to filter any entities matching a [`PseudoClassElement`]
267/// Returns new filtered list of entities and a list of entities matched by the query.
268fn get_entities_with_pseudo_class(
269    world: &World,
270    pseudo_class: PseudoClassElement,
271    entities: SmallVec<[Entity; 8]>,
272) -> (FilteredEntities, MatchedEntities) {
273    match pseudo_class {
274        PseudoClassElement::Hover => get_entities_with_pseudo_class_hover(world, entities),
275        PseudoClassElement::Unsupported => (FilteredEntities(entities), Default::default()),
276    }
277}
278
279/// Utility function to filter any entities matching a [`PseudoClassElement::Hover`] variant
280/// This function looks for [`Interaction`] component with [`Interaction::Hovered`] variant.
281/// Returns a list with entities which are hovered and a list of entities which where matched.
282fn get_entities_with_pseudo_class_hover(
283    world: &World,
284    entities: SmallVec<[Entity; 8]>,
285) -> (FilteredEntities, MatchedEntities) {
286    let filtered = entities
287        .iter()
288        .copied()
289        .filter(|&e| {
290            world
291                .entity(e)
292                .get::<Interaction>()
293                .is_some_and(|interaction| matches!(interaction, Interaction::Hovered))
294        })
295        .collect::<SmallVec<_>>();
296
297    (FilteredEntities(filtered), MatchedEntities(entities))
298}
299
300/// Filters entities which have the components specified on selector, like "a" or "button".
301///
302/// The component must be registered on [`ComponentFilterRegistry`]
303fn get_entities_with_component(
304    name: &str,
305    world: &World,
306    components: &mut ComponentFilterRegistry,
307    entities: SmallVec<[Entity; 8]>,
308) -> (FilteredEntities, MatchedEntities) {
309    if let Some(query) = components.0.get_mut(name) {
310        let filtered = query
311            .filter(world)
312            .into_iter()
313            .filter(|e| entities.contains(e))
314            .collect::<SmallVec<_>>();
315
316        (
317            FilteredEntities(filtered.clone()),
318            MatchedEntities(filtered),
319        )
320    } else {
321        error!("Unregistered component selector {}", name);
322        Default::default()
323    }
324}
325
326/// Filters entities which have a [`Node`] component.
327/// This is to mimic the "*" selector on CSS.
328fn get_entities_with_any_component(
329    query: &Query<Entity, With<Node>>,
330    entities: SmallVec<[Entity; 8]>,
331) -> (FilteredEntities, MatchedEntities) {
332    let filtered = query
333        .iter()
334        .filter(|e| entities.contains(e))
335        .collect::<SmallVec<_>>();
336
337    (
338        FilteredEntities(filtered.clone()),
339        MatchedEntities(filtered),
340    )
341}
342
343/// Traverse the children hierarchy three and returns all entities.
344fn get_children_recursively(
345    children: &Children,
346    q_childs: &Query<&Children, With<Node>>,
347) -> SmallVec<[Entity; 8]> {
348    children
349        .iter()
350        .flat_map(|&e| {
351            std::iter::once(e).chain(
352                q_childs
353                    .get(e)
354                    .map_or(SmallVec::new(), |gc| get_children_recursively(gc, q_childs)),
355            )
356        })
357        .collect()
358}
359
360/// Auto reapply style sheets when hot reloading is enabled
361pub(crate) fn hot_reload_style_sheets(
362    mut assets_events: EventReader<AssetEvent<StyleSheetAsset>>,
363    mut q_sheets: Query<&mut StyleSheet>,
364) {
365    for evt in assets_events.read() {
366        if let AssetEvent::Modified { id } = evt {
367            q_sheets
368                .iter_mut()
369                .filter(|sheet| sheet.handles().iter().any(|h| h.id() == *id))
370                .for_each(|mut sheet| {
371                    debug!("Refreshing sheet {:?} due to asset reload", sheet);
372                    sheet.refresh();
373                });
374        }
375    }
376}
377
378/// Clear selected entities, but keep tracked ones.
379pub(crate) fn clear_state(mut sheet_rule: ResMut<StyleSheetState>) {
380    if sheet_rule.has_any_selected_entities() {
381        debug!("Finished applying style sheet.");
382        sheet_rule.clear_selected_entities();
383    }
384}
385
386/// Watch for changes on entities which is children of a Entith with [`StyleSheet`].
387/// This system uses a cached list of entities which was matched by some [`SelectorElement`]
388/// when applying some [`StyleSheetAsset`].
389///
390/// Whenever a single child has a single component changed, the entire style sheet is applied again.
391pub(crate) fn watch_tracked_entities(world: &mut World) {
392    if world.is_resource_changed::<StyleSheetState>() {
393        trace!("StyleSheetState resource changed! Skipping watch tracked entities");
394        return;
395    }
396
397    let Some(state) = world.get_resource::<StyleSheetState>() else {
398        return;
399    };
400
401    let changed_assets = check_for_changed_assets(state, world);
402
403    // This is done separated to isolate where we need &mut World.
404    if !changed_assets.is_empty() {
405        let mut query_state: SystemState<Query<&mut StyleSheet>> = SystemState::new(world);
406        for asset_id in changed_assets {
407            let mut query = query_state.get_mut(world);
408            for mut stylesheet in query.iter_mut() {
409                if stylesheet.handles().iter().any(|h| h.id() == asset_id) {
410                    debug!("Refreshing sheet {:?} due to changed entities", stylesheet);
411                    stylesheet.refresh();
412                }
413            }
414        }
415    }
416}
417
418/// Check if any entity has a component which is styled by any asset, was changed.
419/// If it does, return the [`AssetId<T>`] so it can be refreshed.
420fn check_for_changed_assets(
421    state: &StyleSheetState,
422    world: &World,
423) -> Vec<AssetId<StyleSheetAsset>> {
424    let mut changed_assets = vec![];
425    for (asset_id, tracked_entities, _) in state.iter() {
426        for (element, entities) in tracked_entities.iter() {
427            if entities.is_empty() {
428                continue;
429            }
430
431            let changed = match element {
432                SelectorElement::Name(_) => any_component::<Name>(world, entities),
433                SelectorElement::Component(c) => any_component_changed_by_name(world, entities, c),
434                SelectorElement::Class(_) => any_component::<Class>(world, entities),
435                SelectorElement::PseudoClass(pseudo_class) => {
436                    any_component_changed_by_pseudo_class(world, entities, *pseudo_class)
437                }
438                SelectorElement::Any => any_component::<Node>(world, entities),
439                _ => unreachable!(),
440            };
441
442            if changed {
443                trace!("Changed! {:?}", element);
444                changed_assets.push(*asset_id);
445                break;
446            }
447        }
448    }
449
450    changed_assets
451}
452
453/// Checks if any entity on the given list has it's component changed.
454fn any_component<T: Component>(world: &World, entities: &SmallVec<[Entity; 8]>) -> bool {
455    let this_run = world.read_change_tick();
456    let last_run = world.last_change_tick();
457    for e in entities {
458        if let Some(ticks) = world.entity(*e).get_change_ticks::<T>() {
459            if ticks.is_changed(last_run, this_run) {
460                return true;
461            }
462        }
463    }
464    false
465}
466
467/// Checks if any entity on the given list has it's component changed.
468fn any_component_changed_by_name(
469    world: &World,
470    entities: &SmallVec<[Entity; 8]>,
471    component_name: &str,
472) -> bool {
473    let this_run = world.read_change_tick();
474    let last_run = world.last_change_tick();
475
476    let Some(registry) = world.get_resource::<ComponentFilterRegistry>() else {
477        return false;
478    };
479    let Some(boxed_state) = registry.get(component_name) else {
480        return false;
481    };
482
483    for e in entities {
484        if let Some(ticks) = boxed_state.get_change_ticks(world, *e) {
485            if ticks.is_changed(last_run, this_run) {
486                return true;
487            }
488        }
489    }
490    false
491}
492
493/// Checks if any entity on the given list has it's component changed.
494fn any_component_changed_by_pseudo_class(
495    world: &World,
496    entities: &SmallVec<[Entity; 8]>,
497    pseudo_class: PseudoClassElement,
498) -> bool {
499    match pseudo_class {
500        PseudoClassElement::Hover => any_component::<Interaction>(world, entities),
501        PseudoClassElement::Unsupported => false,
502    }
503}