bevy_ui/layout/
mod.rs

1use crate::{
2    experimental::{UiChildren, UiRootNodes},
3    ui_transform::{UiGlobalTransform, UiTransform},
4    ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Display, IgnoreScroll, LayoutConfig,
5    Node, Outline, OverflowAxis, ScrollPosition,
6};
7use bevy_ecs::{
8    change_detection::{DetectChanges, DetectChangesMut},
9    entity::Entity,
10    hierarchy::Children,
11    lifecycle::RemovedComponents,
12    query::Added,
13    system::{Query, ResMut},
14    world::Ref,
15};
16
17use bevy_math::{Affine2, Vec2};
18use bevy_sprite::BorderRect;
19use thiserror::Error;
20use ui_surface::UiSurface;
21
22use bevy_text::ComputedTextBlock;
23
24use bevy_text::CosmicFontSystem;
25
26mod convert;
27pub mod debug;
28pub mod ui_surface;
29
30pub struct LayoutContext {
31    pub scale_factor: f32,
32    pub physical_size: Vec2,
33}
34
35impl LayoutContext {
36    pub const DEFAULT: Self = Self {
37        scale_factor: 1.0,
38        physical_size: Vec2::ZERO,
39    };
40    /// Create a new [`LayoutContext`] from the window's physical size and scale factor
41    #[inline]
42    const fn new(scale_factor: f32, physical_size: Vec2) -> Self {
43        Self {
44            scale_factor,
45            physical_size,
46        }
47    }
48}
49
50#[cfg(test)]
51impl LayoutContext {
52    pub const TEST_CONTEXT: Self = Self {
53        scale_factor: 1.0,
54        physical_size: Vec2::new(1000.0, 1000.0),
55    };
56}
57
58impl Default for LayoutContext {
59    fn default() -> Self {
60        Self::DEFAULT
61    }
62}
63
64#[derive(Debug, Error)]
65pub enum LayoutError {
66    #[error("Invalid hierarchy")]
67    InvalidHierarchy,
68    #[error("Taffy error: {0}")]
69    TaffyError(taffy::tree::TaffyError),
70}
71
72/// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes.
73pub fn ui_layout_system(
74    mut ui_surface: ResMut<UiSurface>,
75    ui_root_node_query: UiRootNodes,
76    ui_children: UiChildren,
77    mut node_query: Query<(
78        Entity,
79        Ref<Node>,
80        Option<&mut ContentSize>,
81        Ref<ComputedUiRenderTargetInfo>,
82    )>,
83    added_node_query: Query<(), Added<Node>>,
84    mut node_update_query: Query<(
85        &mut ComputedNode,
86        &UiTransform,
87        &mut UiGlobalTransform,
88        &Node,
89        Option<&LayoutConfig>,
90        Option<&Outline>,
91        Option<&ScrollPosition>,
92        Option<&IgnoreScroll>,
93    )>,
94    mut buffer_query: Query<&mut ComputedTextBlock>,
95    mut font_system: ResMut<CosmicFontSystem>,
96    mut removed_children: RemovedComponents<Children>,
97    mut removed_content_sizes: RemovedComponents<ContentSize>,
98    mut removed_nodes: RemovedComponents<Node>,
99) {
100    // When a `ContentSize` component is removed from an entity, we need to remove the measure from the corresponding taffy node.
101    for entity in removed_content_sizes.read() {
102        ui_surface.try_remove_node_context(entity);
103    }
104
105    // Sync Node and ContentSize to Taffy for all nodes
106    node_query
107        .iter_mut()
108        .for_each(|(entity, node, content_size, computed_target)| {
109            if computed_target.is_changed()
110                || node.is_changed()
111                || content_size
112                    .as_ref()
113                    .is_some_and(|c| c.is_changed() || c.measure.is_some())
114            {
115                let layout_context = LayoutContext::new(
116                    computed_target.scale_factor,
117                    computed_target.physical_size.as_vec2(),
118                );
119                let measure = content_size.and_then(|mut c| c.measure.take());
120                ui_surface.upsert_node(&layout_context, entity, &node, measure);
121            }
122        });
123
124    // update and remove children
125    for entity in removed_children.read() {
126        ui_surface.try_remove_children(entity);
127    }
128
129    // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used)
130    ui_surface.remove_entities(
131        removed_nodes
132            .read()
133            .filter(|entity| !node_query.contains(*entity)),
134    );
135
136    for ui_root_entity in ui_root_node_query.iter() {
137        fn update_children_recursively(
138            ui_surface: &mut UiSurface,
139            ui_children: &UiChildren,
140            added_node_query: &Query<(), Added<Node>>,
141            entity: Entity,
142        ) {
143            if ui_surface.entity_to_taffy.contains_key(&entity)
144                && (added_node_query.contains(entity)
145                    || ui_children.is_changed(entity)
146                    || ui_children
147                        .iter_ui_children(entity)
148                        .any(|child| added_node_query.contains(child)))
149            {
150                ui_surface.update_children(entity, ui_children.iter_ui_children(entity));
151            }
152
153            for child in ui_children.iter_ui_children(entity) {
154                update_children_recursively(ui_surface, ui_children, added_node_query, child);
155            }
156        }
157
158        update_children_recursively(
159            &mut ui_surface,
160            &ui_children,
161            &added_node_query,
162            ui_root_entity,
163        );
164
165        let (_, _, _, computed_target) = node_query.get(ui_root_entity).unwrap();
166
167        ui_surface.compute_layout(
168            ui_root_entity,
169            computed_target.physical_size,
170            &mut buffer_query,
171            &mut font_system,
172        );
173
174        update_uinode_geometry_recursive(
175            ui_root_entity,
176            &mut ui_surface,
177            true,
178            computed_target.physical_size().as_vec2(),
179            Affine2::IDENTITY,
180            &mut node_update_query,
181            &ui_children,
182            computed_target.scale_factor.recip(),
183            Vec2::ZERO,
184            Vec2::ZERO,
185        );
186    }
187
188    // Returns the combined bounding box of the node and any of its overflowing children.
189    fn update_uinode_geometry_recursive(
190        entity: Entity,
191        ui_surface: &mut UiSurface,
192        inherited_use_rounding: bool,
193        target_size: Vec2,
194        mut inherited_transform: Affine2,
195        node_update_query: &mut Query<(
196            &mut ComputedNode,
197            &UiTransform,
198            &mut UiGlobalTransform,
199            &Node,
200            Option<&LayoutConfig>,
201            Option<&Outline>,
202            Option<&ScrollPosition>,
203            Option<&IgnoreScroll>,
204        )>,
205        ui_children: &UiChildren,
206        inverse_target_scale_factor: f32,
207        parent_size: Vec2,
208        parent_scroll_position: Vec2,
209    ) {
210        if let Ok((
211            mut node,
212            transform,
213            mut global_transform,
214            style,
215            maybe_layout_config,
216            maybe_outline,
217            maybe_scroll_position,
218            maybe_scroll_sticky,
219        )) = node_update_query.get_mut(entity)
220        {
221            let use_rounding = maybe_layout_config
222                .map(|layout_config| layout_config.use_rounding)
223                .unwrap_or(inherited_use_rounding);
224
225            let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity, use_rounding) else {
226                return;
227            };
228
229            let layout_size = Vec2::new(layout.size.width, layout.size.height);
230
231            // Taffy layout position of the top-left corner of the node, relative to its parent.
232            let layout_location = Vec2::new(layout.location.x, layout.location.y);
233
234            // If IgnoreScroll is set, parent scroll position is ignored along the specified axes.
235            let effective_parent_scroll = maybe_scroll_sticky
236                .map(|scroll_sticky| parent_scroll_position * Vec2::from(!scroll_sticky.0))
237                .unwrap_or(parent_scroll_position);
238
239            // The position of the center of the node relative to its top-left corner.
240            let local_center =
241                layout_location - effective_parent_scroll + 0.5 * (layout_size - parent_size);
242
243            // only trigger change detection when the new values are different
244            if node.size != layout_size
245                || node.unrounded_size != unrounded_size
246                || node.inverse_scale_factor != inverse_target_scale_factor
247            {
248                node.size = layout_size;
249                node.unrounded_size = unrounded_size;
250                node.inverse_scale_factor = inverse_target_scale_factor;
251            }
252
253            let content_size = Vec2::new(layout.content_size.width, layout.content_size.height);
254            node.bypass_change_detection().content_size = content_size;
255
256            let taffy_rect_to_border_rect = |rect: taffy::Rect<f32>| BorderRect {
257                min_inset: Vec2::new(rect.left, rect.top),
258                max_inset: Vec2::new(rect.right, rect.bottom),
259            };
260
261            node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border);
262            node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding);
263
264            // Compute the node's new global transform
265            let mut local_transform = transform.compute_affine(
266                inverse_target_scale_factor.recip(),
267                layout_size,
268                target_size,
269            );
270            local_transform.translation += local_center;
271            inherited_transform *= local_transform;
272
273            if inherited_transform != **global_transform {
274                *global_transform = inherited_transform.into();
275            }
276
277            // We don't trigger change detection for changes to border radius
278            node.bypass_change_detection().border_radius = style.border_radius.resolve(
279                inverse_target_scale_factor.recip(),
280                node.size,
281                target_size,
282            );
283
284            if let Some(outline) = maybe_outline {
285                // don't trigger change detection when only outlines are changed
286                let node = node.bypass_change_detection();
287                node.outline_width = if style.display != Display::None {
288                    outline
289                        .width
290                        .resolve(
291                            inverse_target_scale_factor.recip(),
292                            node.size().x,
293                            target_size,
294                        )
295                        .unwrap_or(0.)
296                        .max(0.)
297                } else {
298                    0.
299                };
300
301                node.outline_offset = outline
302                    .offset
303                    .resolve(
304                        inverse_target_scale_factor.recip(),
305                        node.size().x,
306                        target_size,
307                    )
308                    .unwrap_or(0.)
309                    .max(0.);
310            }
311
312            node.bypass_change_detection().scrollbar_size =
313                Vec2::new(layout.scrollbar_size.width, layout.scrollbar_size.height);
314
315            let scroll_position: Vec2 = maybe_scroll_position
316                .map(|scroll_pos| {
317                    Vec2::new(
318                        if style.overflow.x == OverflowAxis::Scroll {
319                            scroll_pos.x * inverse_target_scale_factor.recip()
320                        } else {
321                            0.0
322                        },
323                        if style.overflow.y == OverflowAxis::Scroll {
324                            scroll_pos.y * inverse_target_scale_factor.recip()
325                        } else {
326                            0.0
327                        },
328                    )
329                })
330                .unwrap_or_default();
331
332            let max_possible_offset =
333                (content_size - layout_size + node.scrollbar_size).max(Vec2::ZERO);
334            let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);
335
336            let physical_scroll_position = clamped_scroll_position.floor();
337
338            node.bypass_change_detection().scroll_position = physical_scroll_position;
339
340            for child_uinode in ui_children.iter_ui_children(entity) {
341                update_uinode_geometry_recursive(
342                    child_uinode,
343                    ui_surface,
344                    use_rounding,
345                    target_size,
346                    inherited_transform,
347                    node_update_query,
348                    ui_children,
349                    inverse_target_scale_factor,
350                    layout_size,
351                    physical_scroll_position,
352                );
353            }
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use crate::{
361        layout::ui_surface::UiSurface, prelude::*, ui_layout_system,
362        update::propagate_ui_target_cameras, ContentSize, LayoutContext,
363    };
364    use bevy_app::{App, HierarchyPropagatePlugin, PostUpdate, PropagateSet};
365    use bevy_camera::{Camera, Camera2d, ComputedCameraValues, RenderTargetInfo, Viewport};
366    use bevy_ecs::{prelude::*, system::RunSystemOnce};
367    use bevy_math::{Rect, UVec2, Vec2};
368    use bevy_platform::collections::HashMap;
369    use bevy_transform::systems::mark_dirty_trees;
370    use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms};
371    use bevy_utils::prelude::default;
372
373    use taffy::TraversePartialTree;
374
375    // these window dimensions are easy to convert to and from percentage values
376    const TARGET_WIDTH: u32 = 1000;
377    const TARGET_HEIGHT: u32 = 100;
378
379    fn setup_ui_test_app() -> App {
380        let mut app = App::new();
381
382        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiTargetCamera>::new(
383            PostUpdate,
384        ));
385        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiRenderTargetInfo>::new(
386            PostUpdate,
387        ));
388        app.init_resource::<UiScale>();
389        app.init_resource::<UiSurface>();
390        app.init_resource::<bevy_text::TextPipeline>();
391        app.init_resource::<bevy_text::CosmicFontSystem>();
392        app.init_resource::<bevy_text::SwashCache>();
393        app.init_resource::<bevy_transform::StaticTransformOptimizations>();
394
395        app.add_systems(
396            PostUpdate,
397            (
398                ApplyDeferred,
399                propagate_ui_target_cameras,
400                ui_layout_system,
401                mark_dirty_trees,
402                sync_simple_transforms,
403                propagate_parent_transforms,
404            )
405                .chain(),
406        );
407
408        app.configure_sets(
409            PostUpdate,
410            PropagateSet::<ComputedUiTargetCamera>::default()
411                .after(propagate_ui_target_cameras)
412                .before(ui_layout_system),
413        );
414
415        app.configure_sets(
416            PostUpdate,
417            PropagateSet::<ComputedUiRenderTargetInfo>::default()
418                .after(propagate_ui_target_cameras)
419                .before(ui_layout_system),
420        );
421
422        let world = app.world_mut();
423        // spawn a camera with a dummy render target
424        world.spawn((
425            Camera2d,
426            Camera {
427                computed: ComputedCameraValues {
428                    target_info: Some(RenderTargetInfo {
429                        physical_size: UVec2::new(TARGET_WIDTH, TARGET_HEIGHT),
430                        scale_factor: 1.,
431                    }),
432                    ..Default::default()
433                },
434                viewport: Some(Viewport {
435                    physical_size: UVec2::new(TARGET_WIDTH, TARGET_HEIGHT),
436                    ..default()
437                }),
438                ..Default::default()
439            },
440        ));
441
442        app
443    }
444
445    #[test]
446    fn ui_nodes_with_percent_100_dimensions_should_fill_their_parent() {
447        let mut app = setup_ui_test_app();
448
449        let world = app.world_mut();
450
451        // spawn a root entity with width and height set to fill 100% of its parent
452        let ui_root = world
453            .spawn(Node {
454                width: Val::Percent(100.),
455                height: Val::Percent(100.),
456                ..default()
457            })
458            .id();
459
460        let ui_child = world
461            .spawn(Node {
462                width: Val::Percent(100.),
463                height: Val::Percent(100.),
464                ..default()
465            })
466            .id();
467
468        world.entity_mut(ui_root).add_child(ui_child);
469
470        app.update();
471
472        let mut ui_surface = app.world_mut().resource_mut::<UiSurface>();
473
474        for ui_entity in [ui_root, ui_child] {
475            let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
476            assert_eq!(layout.size.width, TARGET_WIDTH as f32);
477            assert_eq!(layout.size.height, TARGET_HEIGHT as f32);
478        }
479    }
480
481    #[test]
482    fn ui_surface_tracks_ui_entities() {
483        let mut app = setup_ui_test_app();
484
485        let world = app.world_mut();
486        // no UI entities in world, none in UiSurface
487        let ui_surface = world.resource::<UiSurface>();
488        assert!(ui_surface.entity_to_taffy.is_empty());
489
490        let ui_entity = world.spawn(Node::default()).id();
491
492        app.update();
493        let world = app.world_mut();
494
495        let ui_surface = world.resource::<UiSurface>();
496        assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
497        assert_eq!(ui_surface.entity_to_taffy.len(), 1);
498
499        world.despawn(ui_entity);
500
501        app.update();
502        let world = app.world_mut();
503
504        let ui_surface = world.resource::<UiSurface>();
505        assert!(!ui_surface.entity_to_taffy.contains_key(&ui_entity));
506        assert!(ui_surface.entity_to_taffy.is_empty());
507    }
508
509    #[test]
510    #[should_panic]
511    fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() {
512        let mut app = setup_ui_test_app();
513        let world = app.world_mut();
514
515        let ui_entity = world.spawn(Node::default()).id();
516
517        // `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
518        app.update();
519        let world = app.world_mut();
520
521        // retrieve the ui node corresponding to `ui_entity` from ui surface
522        let ui_surface = world.resource::<UiSurface>();
523        let ui_node = ui_surface.entity_to_taffy[&ui_entity];
524
525        world.despawn(ui_entity);
526
527        // `ui_layout_system` will receive a `RemovedComponents<Node>` event for `ui_entity`
528        // and remove `ui_entity` from `ui_node` from the internal layout tree
529        app.update();
530        let world = app.world_mut();
531
532        let ui_surface = world.resource::<UiSurface>();
533
534        // `ui_node` is removed, attempting to retrieve a style for `ui_node` panics
535        let _ = ui_surface.taffy.style(ui_node.id);
536    }
537
538    #[test]
539    fn changes_to_children_of_a_ui_entity_change_its_corresponding_ui_nodes_children() {
540        let mut app = setup_ui_test_app();
541        let world = app.world_mut();
542
543        let ui_parent_entity = world.spawn(Node::default()).id();
544
545        // `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
546        app.update();
547        let world = app.world_mut();
548
549        let ui_surface = world.resource::<UiSurface>();
550        let ui_parent_node = ui_surface.entity_to_taffy[&ui_parent_entity];
551
552        // `ui_parent_node` shouldn't have any children yet
553        assert_eq!(ui_surface.taffy.child_count(ui_parent_node.id), 0);
554
555        let mut ui_child_entities = (0..10)
556            .map(|_| {
557                let child = world.spawn(Node::default()).id();
558                world.entity_mut(ui_parent_entity).add_child(child);
559                child
560            })
561            .collect::<Vec<_>>();
562
563        app.update();
564        let world = app.world_mut();
565
566        // `ui_parent_node` should have children now
567        let ui_surface = world.resource::<UiSurface>();
568        assert_eq!(
569            ui_surface.entity_to_taffy.len(),
570            1 + ui_child_entities.len()
571        );
572        assert_eq!(
573            ui_surface.taffy.child_count(ui_parent_node.id),
574            ui_child_entities.len()
575        );
576
577        let child_node_map = <HashMap<_, _>>::from_iter(
578            ui_child_entities
579                .iter()
580                .map(|child_entity| (*child_entity, ui_surface.entity_to_taffy[child_entity])),
581        );
582
583        // the children should have a corresponding ui node and that ui node's parent should be `ui_parent_node`
584        for node in child_node_map.values() {
585            assert_eq!(ui_surface.taffy.parent(node.id), Some(ui_parent_node.id));
586        }
587
588        // delete every second child
589        let mut deleted_children = vec![];
590        for i in (0..ui_child_entities.len()).rev().step_by(2) {
591            let child = ui_child_entities.remove(i);
592            world.despawn(child);
593            deleted_children.push(child);
594        }
595
596        app.update();
597        let world = app.world_mut();
598
599        let ui_surface = world.resource::<UiSurface>();
600        assert_eq!(
601            ui_surface.entity_to_taffy.len(),
602            1 + ui_child_entities.len()
603        );
604        assert_eq!(
605            ui_surface.taffy.child_count(ui_parent_node.id),
606            ui_child_entities.len()
607        );
608
609        // the remaining children should still have nodes in the layout tree
610        for child_entity in &ui_child_entities {
611            let child_node = child_node_map[child_entity];
612            assert_eq!(ui_surface.entity_to_taffy[child_entity], child_node);
613            assert_eq!(
614                ui_surface.taffy.parent(child_node.id),
615                Some(ui_parent_node.id)
616            );
617            assert!(ui_surface
618                .taffy
619                .children(ui_parent_node.id)
620                .unwrap()
621                .contains(&child_node.id));
622        }
623
624        // the nodes of the deleted children should have been removed from the layout tree
625        for deleted_child_entity in &deleted_children {
626            assert!(!ui_surface
627                .entity_to_taffy
628                .contains_key(deleted_child_entity));
629            let deleted_child_node = child_node_map[deleted_child_entity];
630            assert!(!ui_surface
631                .taffy
632                .children(ui_parent_node.id)
633                .unwrap()
634                .contains(&deleted_child_node.id));
635        }
636
637        // despawn the parent entity and its descendants
638        world.entity_mut(ui_parent_entity).despawn();
639
640        app.update();
641        let world = app.world_mut();
642
643        // all nodes should have been deleted
644        let ui_surface = world.resource::<UiSurface>();
645        assert!(ui_surface.entity_to_taffy.is_empty());
646    }
647
648    /// bugfix test, see [#16288](https://github.com/bevyengine/bevy/pull/16288)
649    #[test]
650    fn node_removal_and_reinsert_should_work() {
651        let mut app = setup_ui_test_app();
652
653        app.update();
654        let world = app.world_mut();
655
656        // no UI entities in world, none in UiSurface
657        let ui_surface = world.resource::<UiSurface>();
658        assert!(ui_surface.entity_to_taffy.is_empty());
659
660        let ui_entity = world.spawn(Node::default()).id();
661
662        // `ui_layout_system` should map `ui_entity` to a ui node in `UiSurface::entity_to_taffy`
663        app.update();
664        let world = app.world_mut();
665
666        let ui_surface = world.resource::<UiSurface>();
667        assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
668        assert_eq!(ui_surface.entity_to_taffy.len(), 1);
669
670        // remove and re-insert Node to trigger removal code in `ui_layout_system`
671        world.entity_mut(ui_entity).remove::<Node>();
672        world.entity_mut(ui_entity).insert(Node::default());
673
674        // `ui_layout_system` should still have `ui_entity`
675        app.update();
676        let world = app.world_mut();
677
678        let ui_surface = world.resource::<UiSurface>();
679        assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
680        assert_eq!(ui_surface.entity_to_taffy.len(), 1);
681    }
682
683    #[test]
684    fn node_addition_should_sync_children() {
685        let mut app = setup_ui_test_app();
686        let world = app.world_mut();
687
688        // spawn an invalid UI root node
689        let root_node = world.spawn(()).with_child(Node::default()).id();
690
691        app.update();
692        let world = app.world_mut();
693
694        // fix the invalid root node by inserting a Node
695        world.entity_mut(root_node).insert(Node::default());
696
697        app.update();
698        let world = app.world_mut();
699
700        let ui_surface = world.resource_mut::<UiSurface>();
701        let taffy_root = ui_surface.entity_to_taffy[&root_node];
702
703        // There should be one child of the root node after fixing it
704        assert_eq!(ui_surface.taffy.child_count(taffy_root.id), 1);
705    }
706
707    #[test]
708    fn node_addition_should_sync_parent_and_children() {
709        let mut app = setup_ui_test_app();
710        let world = app.world_mut();
711
712        let d = world.spawn(Node::default()).id();
713        let c = world.spawn(()).add_child(d).id();
714        let b = world.spawn(Node::default()).id();
715        let a = world.spawn(Node::default()).add_children(&[b, c]).id();
716
717        app.update();
718        let world = app.world_mut();
719
720        // fix the invalid middle node by inserting a Node
721        world.entity_mut(c).insert(Node::default());
722
723        app.update();
724        let world = app.world_mut();
725
726        let ui_surface = world.resource::<UiSurface>();
727        for (entity, n) in [(a, 2), (b, 0), (c, 1), (d, 0)] {
728            let taffy_id = ui_surface.entity_to_taffy[&entity].id;
729            assert_eq!(ui_surface.taffy.child_count(taffy_id), n);
730        }
731    }
732
733    /// regression test for >=0.13.1 root node layouts
734    /// ensure root nodes act like they are absolutely positioned
735    /// without explicitly declaring it.
736    #[test]
737    fn ui_root_node_should_act_like_position_absolute() {
738        let mut app = setup_ui_test_app();
739        let world = app.world_mut();
740
741        let mut size = 150.;
742
743        world.spawn(Node {
744            // test should pass without explicitly requiring position_type to be set to Absolute
745            // position_type: PositionType::Absolute,
746            width: Val::Px(size),
747            height: Val::Px(size),
748            ..default()
749        });
750
751        size -= 50.;
752
753        world.spawn(Node {
754            // position_type: PositionType::Absolute,
755            width: Val::Px(size),
756            height: Val::Px(size),
757            ..default()
758        });
759
760        size -= 50.;
761
762        world.spawn(Node {
763            // position_type: PositionType::Absolute,
764            width: Val::Px(size),
765            height: Val::Px(size),
766            ..default()
767        });
768
769        app.update();
770        let world = app.world_mut();
771
772        let overlap_check = world
773            .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without<ChildOf>>()
774            .iter(world)
775            .fold(
776                Option::<(Rect, bool)>::None,
777                |option_rect, (entity, node, transform)| {
778                    let current_rect = Rect::from_center_size(transform.translation, node.size());
779                    assert!(
780                        current_rect.height().abs() + current_rect.width().abs() > 0.,
781                        "root ui node {entity} doesn't have a logical size"
782                    );
783                    assert_ne!(
784                        *transform,
785                        UiGlobalTransform::default(),
786                        "root ui node {entity} transform is not populated"
787                    );
788                    let Some((rect, is_overlapping)) = option_rect else {
789                        return Some((current_rect, false));
790                    };
791                    if rect.contains(current_rect.center()) {
792                        Some((current_rect, true))
793                    } else {
794                        Some((current_rect, is_overlapping))
795                    }
796                },
797            );
798
799        let Some((_rect, is_overlapping)) = overlap_check else {
800            unreachable!("test not setup properly");
801        };
802        assert!(is_overlapping, "root ui nodes are expected to behave like they have absolute position and be independent from each other");
803    }
804
805    #[test]
806    fn ui_node_should_properly_update_when_changing_target_camera() {
807        #[derive(Component)]
808        struct MovingUiNode;
809
810        fn update_camera_viewports(mut cameras: Query<&mut Camera>) {
811            let camera_count = cameras.iter().len();
812            for (camera_index, mut camera) in cameras.iter_mut().enumerate() {
813                let target_size = camera.physical_target_size().unwrap();
814                let viewport_width = target_size.x / camera_count as u32;
815                let physical_position = UVec2::new(viewport_width * camera_index as u32, 0);
816                let physical_size = UVec2::new(target_size.x / camera_count as u32, target_size.y);
817                camera.viewport = Some(Viewport {
818                    physical_position,
819                    physical_size,
820                    ..default()
821                });
822            }
823        }
824
825        fn move_ui_node(
826            In(pos): In<Vec2>,
827            mut commands: Commands,
828            cameras: Query<(Entity, &Camera)>,
829            moving_ui_query: Query<Entity, With<MovingUiNode>>,
830        ) {
831            let (target_camera_entity, _) = cameras
832                .iter()
833                .find(|(_, camera)| {
834                    let Some(logical_viewport_rect) = camera.logical_viewport_rect() else {
835                        panic!("missing logical viewport")
836                    };
837                    // make sure cursor is in viewport and that viewport has at least 1px of size
838                    logical_viewport_rect.contains(pos)
839                        && logical_viewport_rect.max.cmpge(Vec2::splat(0.)).any()
840                })
841                .expect("cursor position outside of camera viewport");
842            for moving_ui_entity in moving_ui_query.iter() {
843                commands
844                    .entity(moving_ui_entity)
845                    .insert(UiTargetCamera(target_camera_entity))
846                    .insert(Node {
847                        position_type: PositionType::Absolute,
848                        top: Val::Px(pos.y),
849                        left: Val::Px(pos.x),
850                        ..default()
851                    });
852            }
853        }
854
855        fn do_move_and_test(app: &mut App, new_pos: Vec2, expected_camera_entity: &Entity) {
856            let world = app.world_mut();
857            world.run_system_once_with(move_ui_node, new_pos).unwrap();
858            app.update();
859            let world = app.world_mut();
860            let (ui_node_entity, UiTargetCamera(target_camera_entity)) = world
861                .query_filtered::<(Entity, &UiTargetCamera), With<MovingUiNode>>()
862                .single(world)
863                .expect("missing MovingUiNode");
864            assert_eq!(expected_camera_entity, target_camera_entity);
865            let mut ui_surface = world.resource_mut::<UiSurface>();
866
867            let layout = ui_surface
868                .get_layout(ui_node_entity, true)
869                .expect("failed to get layout")
870                .0;
871
872            // negative test for #12255
873            assert_eq!(Vec2::new(layout.location.x, layout.location.y), new_pos);
874        }
875
876        fn get_taffy_node_count(world: &World) -> usize {
877            world.resource::<UiSurface>().taffy.total_node_count()
878        }
879
880        let mut app = setup_ui_test_app();
881        let world = app.world_mut();
882
883        world.spawn((
884            Camera2d,
885            Camera {
886                order: 1,
887                computed: ComputedCameraValues {
888                    target_info: Some(RenderTargetInfo {
889                        physical_size: UVec2::new(TARGET_WIDTH, TARGET_HEIGHT),
890                        scale_factor: 1.,
891                    }),
892                    ..default()
893                },
894                viewport: Some(Viewport {
895                    physical_size: UVec2::new(TARGET_WIDTH, TARGET_HEIGHT),
896                    ..default()
897                }),
898                ..default()
899            },
900        ));
901
902        world.spawn((
903            Node {
904                position_type: PositionType::Absolute,
905                top: Val::Px(0.),
906                left: Val::Px(0.),
907                ..default()
908            },
909            MovingUiNode,
910        ));
911
912        app.update();
913        let world = app.world_mut();
914
915        let pos_inc = Vec2::splat(1.);
916        let total_cameras = world.query::<&Camera>().iter(world).len();
917        // add total cameras - 1 (the assumed default) to get an idea for how many nodes we should expect
918        let expected_max_taffy_node_count = get_taffy_node_count(world) + total_cameras - 1;
919
920        world.run_system_once(update_camera_viewports).unwrap();
921
922        app.update();
923        let world = app.world_mut();
924
925        let viewport_rects = world
926            .query::<(Entity, &Camera)>()
927            .iter(world)
928            .map(|(e, c)| (e, c.logical_viewport_rect().expect("missing viewport")))
929            .collect::<Vec<_>>();
930
931        for (camera_entity, viewport) in viewport_rects.iter() {
932            let target_pos = viewport.min + pos_inc;
933            do_move_and_test(&mut app, target_pos, camera_entity);
934        }
935
936        // reverse direction
937        let mut viewport_rects = viewport_rects.clone();
938        viewport_rects.reverse();
939        for (camera_entity, viewport) in viewport_rects.iter() {
940            let target_pos = viewport.max - pos_inc;
941            do_move_and_test(&mut app, target_pos, camera_entity);
942        }
943
944        let world = app.world();
945        let current_taffy_node_count = get_taffy_node_count(world);
946        if current_taffy_node_count > expected_max_taffy_node_count {
947            panic!("extra taffy nodes detected: current: {current_taffy_node_count} max expected: {expected_max_taffy_node_count}");
948        }
949    }
950
951    #[test]
952    fn ui_node_should_be_set_to_its_content_size() {
953        let mut app = setup_ui_test_app();
954        let world = app.world_mut();
955
956        let content_size = Vec2::new(50., 25.);
957
958        let ui_entity = world
959            .spawn((
960                Node {
961                    align_self: AlignSelf::Start,
962                    ..default()
963                },
964                ContentSize::fixed_size(content_size),
965            ))
966            .id();
967
968        app.update();
969        let world = app.world_mut();
970
971        let mut ui_surface = world.resource_mut::<UiSurface>();
972        let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
973
974        // the node should takes its size from the fixed size measure func
975        assert_eq!(layout.size.width, content_size.x);
976        assert_eq!(layout.size.height, content_size.y);
977    }
978
979    #[test]
980    fn measure_funcs_should_be_removed_on_content_size_removal() {
981        let mut app = setup_ui_test_app();
982        let world = app.world_mut();
983
984        let content_size = Vec2::new(50., 25.);
985        let ui_entity = world
986            .spawn((
987                Node {
988                    align_self: AlignSelf::Start,
989                    ..Default::default()
990                },
991                ContentSize::fixed_size(content_size),
992            ))
993            .id();
994
995        app.update();
996        let world = app.world_mut();
997
998        let mut ui_surface = world.resource_mut::<UiSurface>();
999        let ui_node = ui_surface.entity_to_taffy[&ui_entity];
1000
1001        // a node with a content size should have taffy context
1002        assert!(ui_surface.taffy.get_node_context(ui_node.id).is_some());
1003        let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
1004        assert_eq!(layout.size.width, content_size.x);
1005        assert_eq!(layout.size.height, content_size.y);
1006
1007        world.entity_mut(ui_entity).remove::<ContentSize>();
1008
1009        app.update();
1010        let world = app.world_mut();
1011
1012        let mut ui_surface = world.resource_mut::<UiSurface>();
1013        // a node without a content size should not have taffy context
1014        assert!(ui_surface.taffy.get_node_context(ui_node.id).is_none());
1015
1016        // Without a content size, the node has no width or height constraints so the length of both dimensions is 0.
1017        let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
1018        assert_eq!(layout.size.width, 0.);
1019        assert_eq!(layout.size.height, 0.);
1020    }
1021
1022    #[test]
1023    fn ui_rounding_test() {
1024        let mut app = setup_ui_test_app();
1025        let world = app.world_mut();
1026
1027        let parent = world
1028            .spawn(Node {
1029                display: Display::Grid,
1030                grid_template_columns: RepeatedGridTrack::min_content(2),
1031                margin: UiRect::all(Val::Px(4.0)),
1032                ..default()
1033            })
1034            .with_children(|commands| {
1035                for _ in 0..2 {
1036                    commands.spawn(Node {
1037                        display: Display::Grid,
1038                        width: Val::Px(160.),
1039                        height: Val::Px(160.),
1040                        ..default()
1041                    });
1042                }
1043            })
1044            .id();
1045
1046        let children = world
1047            .entity(parent)
1048            .get::<Children>()
1049            .unwrap()
1050            .iter()
1051            .collect::<Vec<Entity>>();
1052
1053        for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) {
1054            // This fails with very small / unrealistic scale values
1055            let mut s = 1. - r;
1056            while s <= 5. {
1057                app.world_mut().resource_mut::<UiScale>().0 = s;
1058                app.update();
1059                let world = app.world_mut();
1060                let width_sum: f32 = children
1061                    .iter()
1062                    .map(|child| world.get::<ComputedNode>(*child).unwrap().size.x)
1063                    .sum();
1064                let parent_width = world.get::<ComputedNode>(parent).unwrap().size.x;
1065                assert!((width_sum - parent_width).abs() < 0.001);
1066                assert!((width_sum - 320. * s).abs() <= 1.);
1067                s += r;
1068            }
1069        }
1070    }
1071
1072    #[test]
1073    fn no_camera_ui() {
1074        let mut app = App::new();
1075
1076        app.add_systems(
1077            PostUpdate,
1078            (propagate_ui_target_cameras, ApplyDeferred, ui_layout_system).chain(),
1079        );
1080
1081        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiTargetCamera>::new(
1082            PostUpdate,
1083        ));
1084
1085        app.configure_sets(
1086            PostUpdate,
1087            PropagateSet::<ComputedUiTargetCamera>::default()
1088                .after(propagate_ui_target_cameras)
1089                .before(ui_layout_system),
1090        );
1091
1092        let world = app.world_mut();
1093        world.init_resource::<UiScale>();
1094        world.init_resource::<UiSurface>();
1095
1096        world.init_resource::<bevy_text::TextPipeline>();
1097
1098        world.init_resource::<bevy_text::CosmicFontSystem>();
1099
1100        world.init_resource::<bevy_text::SwashCache>();
1101
1102        let ui_root = world
1103            .spawn(Node {
1104                width: Val::Percent(100.),
1105                height: Val::Percent(100.),
1106                ..default()
1107            })
1108            .id();
1109
1110        let ui_child = world
1111            .spawn(Node {
1112                width: Val::Percent(100.),
1113                height: Val::Percent(100.),
1114                ..default()
1115            })
1116            .id();
1117
1118        world.entity_mut(ui_root).add_child(ui_child);
1119
1120        app.update();
1121    }
1122
1123    #[test]
1124    fn test_ui_surface_compute_camera_layout() {
1125        use bevy_ecs::prelude::ResMut;
1126
1127        let mut app = setup_ui_test_app();
1128        let world = app.world_mut();
1129
1130        let root_node_entity = Entity::from_raw_u32(1).unwrap();
1131
1132        struct TestSystemParam {
1133            root_node_entity: Entity,
1134        }
1135
1136        fn test_system(
1137            params: In<TestSystemParam>,
1138            mut ui_surface: ResMut<UiSurface>,
1139            mut computed_text_block_query: Query<&mut bevy_text::ComputedTextBlock>,
1140            mut font_system: ResMut<bevy_text::CosmicFontSystem>,
1141        ) {
1142            ui_surface.upsert_node(
1143                &LayoutContext::TEST_CONTEXT,
1144                params.root_node_entity,
1145                &Node::default(),
1146                None,
1147            );
1148
1149            ui_surface.compute_layout(
1150                params.root_node_entity,
1151                UVec2::new(800, 600),
1152                &mut computed_text_block_query,
1153                &mut font_system,
1154            );
1155        }
1156
1157        let _ = world.run_system_once_with(test_system, TestSystemParam { root_node_entity });
1158
1159        let ui_surface = world.resource::<UiSurface>();
1160
1161        let taffy_node = ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
1162        assert!(ui_surface.taffy.layout(taffy_node.id).is_ok());
1163    }
1164
1165    #[test]
1166    fn no_viewport_node_leak_on_root_despawned() {
1167        let mut app = setup_ui_test_app();
1168        let world = app.world_mut();
1169
1170        let ui_root_entity = world.spawn(Node::default()).id();
1171
1172        // The UI schedule synchronizes Bevy UI's internal `TaffyTree` with the
1173        // main world's tree of `Node` entities.
1174        app.update();
1175        let world = app.world_mut();
1176
1177        // Two taffy nodes are added to the internal `TaffyTree` for each root UI entity.
1178        // An implicit taffy node representing the viewport and a taffy node corresponding to the
1179        // root UI entity which is parented to the viewport taffy node.
1180        assert_eq!(
1181            world.resource_mut::<UiSurface>().taffy.total_node_count(),
1182            2
1183        );
1184
1185        world.despawn(ui_root_entity);
1186
1187        // The UI schedule removes both the taffy node corresponding to `ui_root_entity` and its
1188        // parent viewport node.
1189        app.update();
1190        let world = app.world_mut();
1191
1192        // Both taffy nodes should now be removed from the internal `TaffyTree`
1193        assert_eq!(
1194            world.resource_mut::<UiSurface>().taffy.total_node_count(),
1195            0
1196        );
1197    }
1198
1199    #[test]
1200    fn no_viewport_node_leak_on_parented_root() {
1201        let mut app = setup_ui_test_app();
1202        let world = app.world_mut();
1203
1204        let ui_root_entity_1 = world.spawn(Node::default()).id();
1205        let ui_root_entity_2 = world.spawn(Node::default()).id();
1206
1207        app.update();
1208        let world = app.world_mut();
1209
1210        // There are two UI root entities. Each root taffy node is given it's own viewport node parent,
1211        // so a total of four taffy nodes are added to the `TaffyTree` by the UI schedule.
1212        assert_eq!(
1213            world.resource_mut::<UiSurface>().taffy.total_node_count(),
1214            4
1215        );
1216
1217        // Parent `ui_root_entity_2` onto `ui_root_entity_1` so now only `ui_root_entity_1` is a
1218        // UI root entity.
1219        world
1220            .entity_mut(ui_root_entity_1)
1221            .add_child(ui_root_entity_2);
1222
1223        // Now there is only one root node so the second viewport node is removed by
1224        // the UI schedule.
1225        app.update();
1226        let world = app.world_mut();
1227
1228        // There is only one viewport node now, so the `TaffyTree` contains 3 nodes in total.
1229        assert_eq!(
1230            world.resource_mut::<UiSurface>().taffy.total_node_count(),
1231            3
1232        );
1233    }
1234}