1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)]
59pub struct NodePseudoState {
60 pub pressed: bool,
62 pub hovered: bool,
64 pub focused: bool,
66 pub focused_and_visible: bool,
68 pub disabled: bool,
70 pub checked: bool,
72}
73
74impl NodePseudoState {
75 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 pub fn is_pressed(&self) -> bool {
86 self.pressed
87 }
88
89 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum NodePseudoStateSelector {
105 Pressed,
107 Hovered,
109 Focused,
111 FocusedAndVisible,
113 Disabled,
115 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)]
134#[reflect(Debug, Default, PartialEq, Clone)]
135pub enum ColorScheme {
136 #[default]
137 Light,
139
140 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#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, SystemSet)]
161pub enum StyleSystems {
162 Prepare,
164
165 SetStyleData,
167
168 MarkNodesForRecalculation,
170
171 TickAnimations,
175
176 CalculateStyles,
179
180 SetPropertyValues,
182
183 ComputeProperties,
185
186 ResolveAnimations,
188
189 SetAnimationValues,
191
192 EmitAnimationEvents,
194
195 ApplyComputedProperties,
197
198 EmitRedrawEvent,
200}
201
202#[derive(Copy, Clone, PartialEq, Debug)]
204pub enum TransitionEventType {
205 Started,
207 Replaced,
209 Canceled,
211 Finished,
213}
214
215#[derive(Clone, PartialEq, Debug, EntityEvent)]
218pub struct TransitionEvent {
219 pub entity: Entity,
221 pub event_type: TransitionEventType,
223 pub property_id: ComponentPropertyId,
225 pub from: ReflectValue,
227 pub to: ReflectValue,
229}
230
231impl TransitionEvent {
232 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#[derive(Copy, Clone, PartialEq, Debug)]
252pub enum AnimationEventType {
253 Started,
255 Paused,
257 Canceled,
259 Finished,
261}
262
263#[derive(Clone, PartialEq, Debug, EntityEvent)]
265pub struct AnimationEvent {
266 pub entity: Entity,
268 pub event_type: AnimationEventType,
270 pub name: Arc<str>,
272 pub property_id: ComponentPropertyId,
274}
275
276#[derive(Resource)]
277struct StyleAnimationsMarker(&'static str);
278
279pub 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 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#[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#[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 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 app.register_type::<NodeStyleSheet>()
569 .register_type::<NodeStyleData>()
570 .register_type::<Siblings>();
571
572 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 {
890 app.update();
891 assert_eq!(num_nodes_needs_style_recalculation(app), 10);
892 clear_all_style_recalculation(app);
894 }
895
896 {
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 assert_eq!(num_nodes_needs_style_recalculation(app), 1);
913
914 clear_all_style_recalculation(app);
915 }
916
917 {
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 {
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}