Skip to main content

bevy_flair_style/
lib.rs

1//! # Bevy Flair Style
2//! Bevy Flair Style is a styling system for Bevy UI. It allows you to style your UI using CSS-like syntax.
3//!
4//! This crate contains all the necessary components, systems and plugins to style your UI.
5
6use bevy_app::prelude::*;
7use bevy_asset::prelude::*;
8use bevy_ecs::intern::Interned;
9use bevy_ecs::prelude::*;
10use bevy_ecs::schedule::ScheduleLabel;
11use bevy_flair_core::*;
12use bevy_reflect::prelude::*;
13use bevy_text::TextSpan;
14use bevy_ui::prelude::*;
15use std::fmt;
16use std::fmt::Write;
17use std::marker::PhantomData;
18use std::sync::Arc;
19
20mod builder;
21pub mod components;
22mod style_sheet;
23
24pub mod animations;
25
26#[cfg(test)]
27mod testing;
28
29pub mod css_selector;
30
31pub(crate) mod custom_iterators;
32mod layers;
33mod media_selector;
34pub mod placeholder;
35mod systems;
36mod to_css;
37mod vars;
38
39use crate::animations::{ReflectAnimationsPlugin, Transition};
40use crate::components::*;
41
42pub use builder::*;
43pub use media_selector::*;
44pub use style_sheet::*;
45pub use to_css::*;
46pub use vars::*;
47
48pub(crate) type IdName = std::borrow::Cow<'static, str>;
49pub(crate) type ClassName = std::borrow::Cow<'static, str>;
50pub(crate) type AttributeKey = std::borrow::Cow<'static, str>;
51pub(crate) type AttributeValue = std::borrow::Cow<'static, str>;
52pub(crate) type VarName = Arc<str>;
53
54// TODO: Add support to CoreWidgets added in bevy 0.17
55
56/// Represents the current pseudo state of an entity.
57/// By default, it supports only the basic pseudo classes like `:hover`, `:active`, and `:focus`.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)]
59pub struct NodePseudoState {
60    /// If the entity is pressed
61    pub pressed: bool,
62    /// If the entity is hovered
63    pub hovered: bool,
64    /// If the entity is focused
65    pub focused: bool,
66    /// If the entity is focused and the focus visibility is active
67    pub focused_and_visible: bool,
68    /// If the entity is disabled (`:disabled`)
69    pub disabled: bool,
70    /// If the entity is checked (`:checked`)
71    pub checked: bool,
72}
73
74impl NodePseudoState {
75    /// Empty StylePseudoState.
76    pub const EMPTY: NodePseudoState = NodePseudoState {
77        pressed: false,
78        hovered: false,
79        focused: false,
80        focused_and_visible: false,
81        disabled: false,
82        checked: false,
83    };
84    /// Returns true if state represents a pressed state.
85    pub fn is_pressed(&self) -> bool {
86        self.pressed
87    }
88
89    /// Checks if the state matches against a [`NodePseudoStateSelector`] .
90    pub fn matches(&self, selector: NodePseudoStateSelector) -> bool {
91        match selector {
92            NodePseudoStateSelector::Pressed => self.pressed,
93            NodePseudoStateSelector::Hovered => self.hovered,
94            NodePseudoStateSelector::Focused => self.focused,
95            NodePseudoStateSelector::FocusedAndVisible => self.focused_and_visible,
96            NodePseudoStateSelector::Disabled => self.disabled,
97            NodePseudoStateSelector::Checked => self.checked,
98        }
99    }
100}
101
102/// Helper class that can be used to match against a node state.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum NodePseudoStateSelector {
105    /// If the entity is pressed (`:active`)
106    Pressed,
107    /// If the entity is hovered (`:hover`)
108    Hovered,
109    /// If the entity is focused (`:focus`)
110    Focused,
111    /// If the entity is focused and the focus indicator is visible  (`:focus-visible`)
112    FocusedAndVisible,
113    /// If the entity is disabled (`:disabled`)
114    Disabled,
115    /// If the entity is checked (`:checked`)
116    Checked,
117}
118
119impl ToCss for NodePseudoStateSelector {
120    fn to_css<W: Write>(&self, dest: &mut W) -> fmt::Result {
121        match self {
122            NodePseudoStateSelector::Pressed => dest.write_str(":active"),
123            NodePseudoStateSelector::Hovered => dest.write_str(":hover"),
124            NodePseudoStateSelector::Focused => dest.write_str(":focus"),
125            NodePseudoStateSelector::FocusedAndVisible => dest.write_str(":focus-visible"),
126            NodePseudoStateSelector::Disabled => dest.write_str(":disabled"),
127            NodePseudoStateSelector::Checked => dest.write_str(":checked"),
128        }
129    }
130}
131
132/// A color scheme that represents `light` or `dark` themes.
133#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)]
134#[reflect(Debug, Default, PartialEq, Clone)]
135pub enum ColorScheme {
136    #[default]
137    /// Use the light variant.
138    Light,
139
140    /// Use the dark variant.
141    Dark,
142}
143
144impl From<bevy_window::WindowTheme> for ColorScheme {
145    fn from(theme: bevy_window::WindowTheme) -> Self {
146        match theme {
147            bevy_window::WindowTheme::Light => Self::Light,
148            bevy_window::WindowTheme::Dark => Self::Dark,
149        }
150    }
151}
152
153#[derive(Resource, Default, Debug)]
154pub(crate) struct GlobalChangeDetection {
155    pub any_property_value_changed: bool,
156    pub any_animation_active: bool,
157}
158
159/// System sets for the [`FlairStylePlugin`] plugin.
160#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, SystemSet)]
161pub enum StyleSystems {
162    /// Any pre-requisite before start calculating any style.
163    Prepare,
164
165    /// Apply changes to [`NodeStyleData`] and [`RawInlineStyle`] components.
166    SetStyleData,
167
168    /// Mark all nodes that needs their style recalculated.
169    MarkNodesForRecalculation,
170
171    /// Moves transitions and animations forward.
172    /// Happens before CalculateStyle, where new transitions and animations are added,
173    /// so new animations are not moved on the frame they are added.
174    TickAnimations,
175
176    /// Calculates active stylesheet sets for nodes that are marked for recalculation.
177    /// Also sets vars and further marks nodes as changed when a var changes.
178    CalculateStyles,
179
180    /// Sets the corresponding [`PropertyValue`]'s, depending on the styles calculated in [`StyleSystems::CalculateStyles`].
181    SetPropertyValues,
182
183    /// Converts properties from [`PropertyValue`] into [`ComputedValue`].
184    ComputeProperties,
185
186    /// Resolve pending animations. It needs to happen after [`StyleSystems::ComputeProperties`]
187    ResolveAnimations,
188
189    /// Sets [`ComputedValue`]'s generated from transitions and animations.
190    SetAnimationValues,
191
192    /// Emits animation events, like `TransitionEvent`.
193    EmitAnimationEvents,
194
195    /// Effectively applies changes from animations and style changes into the Components.
196    ApplyComputedProperties,
197
198    /// Emits a [`bevy_window::RequestRedraw`] if any animation is active.
199    EmitRedrawEvent,
200}
201
202/// Represents the type of event that occurred during a property transition change.
203#[derive(Copy, Clone, PartialEq, Debug)]
204pub enum TransitionEventType {
205    /// Indicates that a transition has started.
206    Started,
207    /// Indicates that an existing transition has been replaced by a new one.
208    Replaced,
209    /// Indicates that a transition has been canceled.
210    Canceled,
211    /// Indicates that a transition has completed.
212    Finished,
213}
214
215/// An event representing a change (or attempted change) in a component property,
216/// triggered during a transition process.
217#[derive(Clone, PartialEq, Debug, EntityEvent)]
218pub struct TransitionEvent {
219    /// Entity that caused the event to happen.
220    pub entity: Entity,
221    /// The type of transition event (e.g., Started, Replaced, Finished).
222    pub event_type: TransitionEventType,
223    /// The property involved in the transition.
224    pub property_id: ComponentPropertyId,
225    /// The original value of the property before the transition.
226    pub from: ReflectValue,
227    /// The target value of the property after the transition.
228    pub to: ReflectValue,
229}
230
231impl TransitionEvent {
232    /// Creates a new [`TransitionEvent`] from a given [`Transition`] and event type.
233    pub(crate) fn new_from_transition(
234        event_type: TransitionEventType,
235        transition: &Transition,
236    ) -> Self {
237        let property_id = transition.property_id;
238        let from = transition.from.clone();
239        let to = transition.to.clone();
240        Self {
241            entity: Entity::PLACEHOLDER,
242            event_type,
243            property_id,
244            from,
245            to,
246        }
247    }
248}
249
250/// Represents the type of event that occurred during an animation change.
251#[derive(Copy, Clone, PartialEq, Debug)]
252pub enum AnimationEventType {
253    /// Indicates that an animation has started.
254    Started,
255    /// Indicates that an animation has been paused.
256    Paused,
257    /// Indicates that an animation has been canceled.
258    Canceled,
259    /// Indicates that an animation has completed.
260    Finished,
261}
262
263/// An event representing a change in an animation.
264#[derive(Clone, PartialEq, Debug, EntityEvent)]
265pub struct AnimationEvent {
266    /// Entity that caused the event to happen.
267    pub entity: Entity,
268    /// The type of animation event (e.g., Started, Finished).
269    pub event_type: AnimationEventType,
270    /// Name of the animation.
271    pub name: Arc<str>,
272    /// The property involved in the animation.
273    pub property_id: ComponentPropertyId,
274}
275
276#[derive(Resource)]
277struct StyleAnimationsMarker(&'static str);
278
279/// Enables bevy_flair to work on a custom time and schedule.
280pub struct FlairStyleAnimationsPlugin<T: Default + Send + Sync + 'static> {
281    _ph: PhantomData<fn() -> T>,
282    schedule: Interned<dyn ScheduleLabel>,
283}
284
285impl<T: Default + Send + Sync + 'static> FlairStyleAnimationsPlugin<T> {
286    /// Creates a new [`FlairStyleAnimationsPlugin`] by specifying in which [`ScheduleLabel`]
287    /// the animations should run. By default, [`PostUpdate`] is used.
288    pub fn new(schedule: impl ScheduleLabel) -> Self {
289        Self {
290            _ph: PhantomData,
291            schedule: schedule.intern(),
292        }
293    }
294}
295
296impl<T: Default + Send + Sync + 'static> Default for FlairStyleAnimationsPlugin<T> {
297    fn default() -> Self {
298        Self::new(PostUpdate)
299    }
300}
301
302impl<T: Default + Send + Sync + 'static> Plugin for FlairStyleAnimationsPlugin<T> {
303    fn build(&self, app: &mut App) {
304        if let Some(StyleAnimationsMarker(other_plugin)) =
305            app.world().get_resource::<StyleAnimationsMarker>()
306        {
307            panic!(
308                "FlairStyleAnimationsPlugin has been instantiated already. Tried to add '{this_plugin}' when '{other_plugin}' has been inserted already",
309                this_plugin = std::any::type_name::<Self>()
310            )
311        }
312
313        app.insert_resource(StyleAnimationsMarker(std::any::type_name::<Self>()));
314
315        app.add_systems(
316            self.schedule,
317            systems::tick_animations::<T>.in_set(StyleSystems::TickAnimations),
318        );
319    }
320}
321
322/// Enables animations by using `Time<Real>` in the [`PostUpdate`] schedule label,
323/// which is the default behaviour.
324#[derive(Default)]
325pub struct FlairDefaultStyleAnimationsPlugin;
326
327impl Plugin for FlairDefaultStyleAnimationsPlugin {
328    fn build(&self, app: &mut App) {
329        app.add_plugins(FlairStyleAnimationsPlugin::<bevy_time::Real>::default());
330    }
331}
332
333/// Plugin that adds the styling systems to Bevy.
334
335#[derive(Default)]
336pub struct FlairStylePlugin;
337
338impl Plugin for FlairStylePlugin {
339    fn build(&self, app: &mut App) {
340        app.init_asset::<StyleSheet>()
341            .init_resource::<GlobalChangeDetection>()
342            .register_required_components::<Node, NodeStyleSheet>()
343            .register_required_components::<TextSpan, NodeStyleSheet>()
344            .register_required_components_with::<Button, TypeName>(|| {
345                TypeName("button")
346            })
347            .register_required_components_with::<Text, TypeName>(|| {
348                TypeName("text")
349            })
350            .register_required_components_with::<TextSpan, TypeName>(|| {
351                TypeName("span")
352            })
353            .register_required_components_with::<Label, TypeName>(|| {
354                TypeName("label")
355            })
356            .add_plugins((ReflectAnimationsPlugin, placeholder::PlaceholderResolvePlugin))
357            .add_observer(systems::observe_on_component_auto_inserted)
358            .configure_sets(
359                PostUpdate,
360                (
361                    (
362                        StyleSystems::Prepare,
363                        StyleSystems::SetStyleData,
364                        StyleSystems::MarkNodesForRecalculation,
365                        StyleSystems::CalculateStyles,
366                        StyleSystems::SetPropertyValues,
367                        StyleSystems::ComputeProperties,
368                        StyleSystems::ResolveAnimations,
369                        StyleSystems::SetAnimationValues,
370                        StyleSystems::ApplyComputedProperties.before(bevy_ui::UiSystems::Content),
371                        StyleSystems::EmitRedrawEvent,
372                    )
373                        .chain(),
374                    StyleSystems::TickAnimations.before(StyleSystems::SetPropertyValues),
375                    StyleSystems::EmitAnimationEvents.after(StyleSystems::SetAnimationValues),
376                ),
377            )
378            .add_systems(PreStartup, |mut commands: Commands| {
379                // This is initialized on PreStartup to make sure all properties are registered.
380                commands.init_resource::<StaticPropertyMaps>();
381            })
382            .add_systems(
383                PreUpdate,
384                (
385                    systems::mark_as_changed_on_style_sheet_change,
386                    systems::clear_global_change_detection,
387                    systems::set_window_theme_on_change_event,
388                ),
389            )
390            .add_systems(PostStartup, systems::compute_window_media_features)
391            .add_systems(
392                PostUpdate,
393                (
394                    (
395                        systems::calculate_effective_style_sheet,
396                        systems::compute_window_media_features,
397                        (systems::sort_pseudo_elements, systems::sync_siblings_system).chain(),
398                        systems::reset_properties,
399                    )
400                        .in_set(StyleSystems::Prepare),
401                    (
402                        systems::calculate_is_root,
403                        systems::apply_classes,
404                        systems::apply_attributes,
405                        systems::sync_marker_component_system::<bevy_ui::Pressed>(|state, value| { state.pressed = value; }),
406                        systems::sync_marker_component_system::<bevy_ui::InteractionDisabled>(|state, value| { state.disabled = value; }),
407                        systems::sync_marker_component_system::<bevy_ui::Checked>(|state, value| { state.checked = value; }),
408                        systems::sync_hovered_system,
409                        systems::sync_interaction_system,
410                        systems::track_name_changes,
411                        systems::sync_input_focus,
412                    )
413                        .in_set(StyleSystems::SetStyleData),
414                    (
415                        systems::set_related_nodes_for_style_recalculation,
416                        systems::mark_changed_nodes_for_recalculation,
417                        (
418                            systems::set_nodes_for_style_recalculation_on_render_target_info_change,
419                            systems::set_nodes_for_style_recalculation_on_window_media_features_change
420                        ).after(bevy_ui::UiSystems::Propagate),
421                    )
422                        .chain()
423                        .in_set(StyleSystems::MarkNodesForRecalculation),
424                    systems::calculate_style_and_set_vars.in_set(StyleSystems::CalculateStyles),
425                    systems::set_property_values.in_set(StyleSystems::SetPropertyValues),
426                    (
427                        systems::compute_property_values
428                            .run_if(systems::compute_property_values_condition),
429                        systems::set_pending_compute_property_values
430                            .run_if(not(systems::compute_property_values_condition))
431                    ).in_set(StyleSystems::ComputeProperties),
432                    systems::resolve_animations.in_set(StyleSystems::ResolveAnimations),
433                    systems::set_animation_computed_values.in_set(StyleSystems::SetAnimationValues),
434                    systems::emit_animation_events.in_set(StyleSystems::EmitAnimationEvents),
435                    systems::emit_redraw_event.in_set(StyleSystems::EmitRedrawEvent),
436                    (
437                        systems::resolve_placeholders,
438                        systems::apply_computed_properties,
439                        systems::auto_remove_components
440                            .run_if(systems::auto_remove_components_condition)
441                    )
442                        .chain()
443                        .in_set(StyleSystems::ApplyComputedProperties),
444                ),
445            );
446    }
447}
448
449#[cfg(test)]
450pub(crate) mod test_utils {
451    use crate::{VarResolver, VarTokens};
452    use std::sync::Arc;
453
454    pub(crate) struct NoVarsSupportedResolver;
455
456    impl VarResolver for NoVarsSupportedResolver {
457        fn get_all_names(&self) -> Vec<Arc<str>> {
458            panic!("No vars support on this test")
459        }
460
461        fn get_var_tokens(&self, _var_name: &str) -> Option<&'_ VarTokens> {
462            panic!("No vars support on this test")
463        }
464    }
465}
466
467#[cfg(all(test, not(miri)))]
468mod tests {
469    use super::*;
470    use bevy_app::App;
471    use bevy_asset::uuid_handle;
472    use bevy_ecs::system::RunSystemOnce;
473    use bevy_ui::Node;
474
475    const TEST_STYLE_SHEET: NodeStyleSheet =
476        NodeStyleSheet::new(uuid_handle!("fe981062-17ce-46e4-999a-5a61ea8fe722"));
477
478    const ROOT: (TestNode, NodeStyleSheet) = (TestNode, TEST_STYLE_SHEET);
479
480    #[derive(Copy, Clone, Component)]
481    #[require(Node)]
482    struct TestNode;
483
484    #[derive(Component)]
485    struct FirstChild;
486
487    #[derive(Copy, Clone, Component, Reflect)]
488    #[reflect(Component)]
489    #[require(Node)]
490    struct Child;
491
492    #[derive(Copy, Clone, Component, Reflect)]
493    #[reflect(Component)]
494    #[require(Node)]
495    struct GrandChild;
496
497    #[derive(Copy, Clone, Component, Reflect)]
498    #[reflect(Component)]
499    #[require(Node, TypeName("custom-type"))]
500    struct CustomType;
501
502    #[derive(Copy, Clone, Component, Reflect)]
503    #[reflect(Component)]
504    #[require(Button, TypeName("custom-button"))]
505    struct CustomButton;
506
507    macro_rules! query_len {
508        ($app:expr, $filter:ty) => {{
509            let world = $app.world_mut();
510            let mut query_state = world.query_filtered::<(), $filter>();
511            query_state.iter(world).len()
512        }};
513    }
514
515    macro_rules! query {
516        ($app:expr, $components:ty) => {{
517            let world: &mut World = $app.world_mut();
518            let mut query_state = world.query::<$components>();
519            query_state.iter(world).cloned().collect::<Vec<_>>()
520        }};
521
522        ($app:expr, $components:ty, $filter:ty) => {{
523            let world: &mut World = $app.world_mut();
524            let mut query_state = world.query_filtered::<$components, $filter>();
525            query_state.iter(world).collect::<Vec<_>>()
526        }};
527    }
528
529    macro_rules! assert_world_snapshot {
530        ($app:expr, ( $($components:path),*) ) => {{
531            let all_entities = query!($app, Entity, With<NodeStyleData>);
532            let type_registry = $app.world().resource::<AppTypeRegistry>().0.read();
533
534            let mut scene_builder = bevy_scene::DynamicSceneBuilder::from_world($app.world());
535            scene_builder = scene_builder
536                .allow_component::<Name>()
537                .allow_component::<ChildOf>()
538                .allow_component::<Children>()
539                .allow_component::<NodeStyleSheet>()
540                $(.allow_component::<$components>())*
541                .extract_entities(all_entities.into_iter())
542                .remove_empty_entities()
543            ;
544
545            let scene = scene_builder.build();
546            let scene_serializer = bevy_scene::serde::SceneSerializer::new(&scene, &type_registry);
547
548            insta::assert_ron_snapshot!(scene_serializer);
549        }};
550    }
551
552    fn app() -> App {
553        let mut app = App::new();
554
555        app.add_plugins((
556            bevy_time::TimePlugin,
557            bevy_window::WindowPlugin {
558                primary_window: None,
559                ..Default::default()
560            },
561            AssetPlugin::default(),
562            PropertyRegistryPlugin,
563            ImplComponentPropertiesPlugin,
564            FlairStylePlugin,
565        ));
566
567        // Not registered, but needed for testing.
568        app.register_type::<NodeStyleSheet>()
569            .register_type::<NodeStyleData>()
570            .register_type::<Siblings>();
571
572        // Bevy uses auto register for these, but auto register is not enable in testing.
573        app.register_type::<ChildOf>()
574            .register_type::<Children>()
575            .register_type::<Name>();
576
577        app.register_type::<Child>()
578            .register_type::<GrandChild>()
579            .register_type::<CustomType>()
580            .register_type::<CustomButton>();
581
582        app.finish();
583
584        app
585    }
586
587    #[test]
588    fn custom_type_name() {
589        let mut app = app();
590        app.add_systems(Startup, |mut commands: Commands| {
591            commands.spawn((ROOT, CustomType));
592        });
593        app.update();
594        assert_world_snapshot!(app, (CustomType, NodeStyleData));
595    }
596
597    #[test]
598    fn custom_button() {
599        let mut app = app();
600        app.add_systems(Startup, |mut commands: Commands| {
601            commands.spawn((ROOT, CustomButton));
602        });
603        app.update();
604        assert_world_snapshot!(app, (CustomButton, NodeStyleData));
605    }
606
607    macro_rules! node_pseudo_state {
608        ($app:ident, $entity:ident) => {
609            &$app
610                .world()
611                .get::<NodeStyleData>($entity)
612                .unwrap()
613                .pseudo_state
614        };
615    }
616
617    #[test]
618    fn state_marker_components() {
619        use bevy_ui::{Checked, InteractionDisabled, Pressed};
620        let mut app = app();
621
622        let entity = app.world_mut().spawn(ROOT).id();
623        app.update();
624
625        let pseudo_state = node_pseudo_state!(app, entity);
626        assert!(!pseudo_state.disabled);
627        assert!(!pseudo_state.checked);
628        assert!(!pseudo_state.pressed);
629
630        app.world_mut()
631            .entity_mut(entity)
632            .insert((Pressed, Checked));
633        app.update();
634
635        let pseudo_state = node_pseudo_state!(app, entity);
636        assert!(!pseudo_state.disabled);
637        assert!(pseudo_state.checked);
638        assert!(pseudo_state.pressed);
639
640        app.world_mut()
641            .entity_mut(entity)
642            .remove::<(Pressed, Checked)>()
643            .insert(InteractionDisabled);
644        app.update();
645
646        let pseudo_state = node_pseudo_state!(app, entity);
647        assert!(pseudo_state.disabled);
648        assert!(!pseudo_state.checked);
649        assert!(!pseudo_state.pressed);
650
651        app.world_mut()
652            .entity_mut(entity)
653            .remove::<(InteractionDisabled,)>()
654            .insert(Checked);
655        app.world_mut()
656            .entity_mut(entity)
657            .remove::<(Checked,)>()
658            .insert((InteractionDisabled, Pressed));
659        app.update();
660
661        let pseudo_state = node_pseudo_state!(app, entity);
662        assert!(pseudo_state.disabled);
663        assert!(!pseudo_state.checked);
664        assert!(pseudo_state.pressed);
665    }
666
667    #[test]
668    fn focus_state() {
669        use bevy_input_focus::{InputFocus, InputFocusVisible};
670        let mut app = app();
671
672        let entity = app.world_mut().spawn(ROOT).id();
673        app.update();
674
675        let pseudo_state = node_pseudo_state!(app, entity);
676        assert!(!pseudo_state.focused);
677        assert!(!pseudo_state.focused_and_visible);
678
679        app.world_mut().insert_resource(InputFocus(Some(entity)));
680        app.update();
681
682        let pseudo_state = node_pseudo_state!(app, entity);
683        assert!(pseudo_state.focused);
684        assert!(!pseudo_state.focused_and_visible);
685
686        app.world_mut().insert_resource(InputFocusVisible(true));
687        app.update();
688
689        let pseudo_state = node_pseudo_state!(app, entity);
690        assert!(pseudo_state.focused);
691        assert!(pseudo_state.focused_and_visible);
692
693        app.world_mut().resource_mut::<InputFocus>().0 = None;
694        app.update();
695
696        let pseudo_state = node_pseudo_state!(app, entity);
697        assert!(!pseudo_state.focused);
698        assert!(!pseudo_state.focused_and_visible);
699
700        app.world_mut().resource_mut::<InputFocus>().0 = Some(entity);
701        app.world_mut().resource_mut::<InputFocusVisible>().0 = false;
702        app.update();
703
704        let pseudo_state = node_pseudo_state!(app, entity);
705        assert!(pseudo_state.focused);
706        assert!(!pseudo_state.focused_and_visible);
707    }
708
709    #[test]
710    fn hovered_state() {
711        use bevy_picking::hover::Hovered;
712        let mut app = app();
713
714        let entity = app.world_mut().spawn(ROOT).id();
715        app.update();
716
717        let pseudo_state = node_pseudo_state!(app, entity);
718        assert!(!pseudo_state.hovered);
719
720        app.world_mut().entity_mut(entity).insert(Hovered(true));
721        app.update();
722
723        let pseudo_state = node_pseudo_state!(app, entity);
724        assert!(pseudo_state.hovered);
725
726        app.world_mut().entity_mut(entity).insert(Hovered(false));
727        app.update();
728
729        let pseudo_state = node_pseudo_state!(app, entity);
730        assert!(!pseudo_state.hovered);
731    }
732
733    #[test]
734    fn spawn_many_children() {
735        let mut app = app();
736
737        app.add_systems(Startup, |mut commands: Commands| {
738            commands
739                .spawn((Name::new("Root"), ROOT))
740                .with_children(|children| {
741                    children.spawn(Child);
742                    children.spawn(Child);
743                    children.spawn(Child);
744                });
745        });
746
747        app.update();
748
749        assert_world_snapshot!(app, (Child, Siblings));
750    }
751
752    #[test]
753    fn spawn_with_pseudo_elements() {
754        let mut app = app();
755
756        app.add_systems(Startup, |mut commands: Commands| {
757            commands
758                .spawn((Name::new("Root"), ROOT))
759                .with_children(|children| {
760                    children.spawn(Child);
761                    children.spawn((Child, PseudoElementsSupport, children![GrandChild]));
762                    children.spawn((Child, PseudoElementsSupport));
763                });
764        });
765
766        app.update();
767
768        assert_world_snapshot!(app, (Child, Siblings, PseudoElement));
769    }
770
771    #[test]
772    fn append_child_after_initial_spawn() {
773        let mut app = app();
774
775        let root_entity_id = app
776            .world_mut()
777            .spawn((Name::new("Root"), ROOT, children![(Child,), (Child,)]))
778            .id();
779
780        app.update();
781        app.world_mut().entity_mut(root_entity_id).with_child(Child);
782        app.update();
783
784        assert_world_snapshot!(app, (Child, Siblings, NodeStyleData));
785    }
786
787    #[test]
788    fn spawn_many_children_and_grandchildren() {
789        let mut app = app();
790
791        app.world_mut().spawn((
792            Name::new("Root"),
793            ROOT,
794            children![
795                (Child, children![GrandChild, GrandChild, GrandChild]),
796                (Child,),
797                (
798                    Child,
799                    NodeStyleSheet::Block,
800                    children![GrandChild, GrandChild, GrandChild]
801                )
802            ],
803        ));
804
805        app.update();
806        assert_world_snapshot!(app, (Child, GrandChild, NodeStyleData, Siblings));
807    }
808
809    #[test]
810    fn append_grand_children_after_initial_spawn() {
811        let mut app = app();
812
813        let root_entity_id = app
814            .world_mut()
815            .spawn((
816                Name::new("Root"),
817                ROOT,
818                children![(Child,), (Child, NodeStyleSheet::Block)],
819            ))
820            .id();
821
822        app.update();
823
824        let num_children = query_len!(app, (With<NodeStyleSheet>, With<Child>));
825        assert_eq!(num_children, 2);
826
827        app.world_mut()
828            .run_system_once(
829                move |mut commands: Commands, children_query: Query<&Children>| {
830                    for child in children_query.iter_leaves(root_entity_id) {
831                        commands
832                            .entity(child)
833                            .with_child(GrandChild)
834                            .with_child(GrandChild);
835                    }
836                },
837            )
838            .unwrap();
839
840        app.update();
841
842        let num_grand_children = query_len!(app, (With<NodeStyleSheet>, With<GrandChild>));
843        assert_eq!(num_grand_children, 4);
844
845        assert_world_snapshot!(app, (Child, GrandChild, NodeStyleData));
846    }
847
848    #[test]
849    fn descendants_marked_for_style_recalculation() {
850        let mut app = app();
851
852        let app = &mut app;
853
854        app.add_systems(Startup, |mut commands: Commands| {
855            commands.spawn((
856                Name::new("Root"),
857                ROOT,
858                children![
859                    (
860                        FirstChild,
861                        Child,
862                        children![GrandChild, GrandChild, GrandChild]
863                    ),
864                    (Child,),
865                    (Child, children![GrandChild, GrandChild, GrandChild])
866                ],
867            ));
868        });
869
870        fn num_nodes_needs_style_recalculation(app: &mut App) -> usize {
871            let markers = query!(app, &NodeStyleMarker);
872            markers
873                .iter()
874                .filter(|c| c.needs_style_recalculation())
875                .count()
876        }
877
878        fn clear_all_style_recalculation(app: &mut App) {
879            app.world_mut()
880                .run_system_once(|mut query: Query<&mut NodeStyleMarker>| {
881                    for mut computed_style in &mut query {
882                        computed_style.clear_style_recalculation();
883                    }
884                })
885                .unwrap();
886        }
887
888        // First run everything is marked for recalculation
889        {
890            app.update();
891            assert_eq!(num_nodes_needs_style_recalculation(app), 10);
892            // We need to manually clear everything
893            clear_all_style_recalculation(app);
894        }
895
896        // With not updates, the number of nodes for recalculation should be still zero
897        {
898            app.update();
899            assert_eq!(num_nodes_needs_style_recalculation(app), 0);
900        }
901
902        {
903            app.world_mut()
904                .run_system_once(|mut query: Query<&mut NodeStyleMarker, Without<ChildOf>>| {
905                    query.single_mut().unwrap().set_needs_style_recalculation();
906                })
907                .unwrap();
908
909            app.update();
910
911            // Only root should be marked for recalculation again, since the flags says no recalculation is needed
912            assert_eq!(num_nodes_needs_style_recalculation(app), 1);
913
914            clear_all_style_recalculation(app);
915        }
916
917        // Only elements marked with Child should be marked
918        {
919            app.world_mut()
920                .run_system_once(
921                    |mut query: Query<
922                        (&NodeStyleSelectorFlags, &mut NodeStyleMarker),
923                        With<FirstChild>,
924                    >| {
925                        let (selector_flags, mut marker) = query.single_mut().unwrap();
926                        marker.set_needs_style_recalculation();
927                        selector_flags
928                            .recalculate_on_change_flags
929                            .insert(RecalculateOnChangeFlags::RECALCULATE_SIBLINGS);
930                    },
931                )
932                .unwrap();
933
934            app.update();
935
936            assert_eq!(num_nodes_needs_style_recalculation(app), 3);
937
938            clear_all_style_recalculation(app);
939        }
940
941        // All elements should be marked
942        {
943            app.world_mut()
944                .run_system_once(
945                    |mut query: Query<
946                        (&NodeStyleSelectorFlags, &mut NodeStyleMarker),
947                        Without<ChildOf>,
948                    >| {
949                        let (selector_flags, mut marker) = query.single_mut().unwrap();
950                        marker.set_needs_style_recalculation();
951                        selector_flags
952                            .recalculate_on_change_flags
953                            .insert(RecalculateOnChangeFlags::RECALCULATE_DESCENDANTS);
954                    },
955                )
956                .unwrap();
957
958            app.update();
959
960            assert_eq!(num_nodes_needs_style_recalculation(app), 10);
961
962            clear_all_style_recalculation(app);
963        }
964    }
965}