bevy_ui/
update.rs

1//! This module contains systems that update the UI when something changes
2
3use crate::{
4    experimental::{UiChildren, UiRootNodes},
5    ui_transform::UiGlobalTransform,
6    CalculatedClip, ComputedUiRenderTargetInfo, ComputedUiTargetCamera, DefaultUiCamera, Display,
7    Node, OverflowAxis, OverrideClip, UiScale, UiTargetCamera,
8};
9
10use super::ComputedNode;
11use bevy_app::Propagate;
12use bevy_camera::Camera;
13use bevy_ecs::{
14    entity::Entity,
15    query::Has,
16    system::{Commands, Query, Res},
17};
18use bevy_math::{Rect, UVec2};
19use bevy_sprite::BorderRect;
20
21/// Updates clipping for all nodes
22pub fn update_clipping_system(
23    mut commands: Commands,
24    root_nodes: UiRootNodes,
25    mut node_query: Query<(
26        &Node,
27        &ComputedNode,
28        &UiGlobalTransform,
29        Option<&mut CalculatedClip>,
30        Has<OverrideClip>,
31    )>,
32    ui_children: UiChildren,
33) {
34    for root_node in root_nodes.iter() {
35        update_clipping(
36            &mut commands,
37            &ui_children,
38            &mut node_query,
39            root_node,
40            None,
41        );
42    }
43}
44
45fn update_clipping(
46    commands: &mut Commands,
47    ui_children: &UiChildren,
48    node_query: &mut Query<(
49        &Node,
50        &ComputedNode,
51        &UiGlobalTransform,
52        Option<&mut CalculatedClip>,
53        Has<OverrideClip>,
54    )>,
55    entity: Entity,
56    mut maybe_inherited_clip: Option<Rect>,
57) {
58    let Ok((node, computed_node, transform, maybe_calculated_clip, has_override_clip)) =
59        node_query.get_mut(entity)
60    else {
61        return;
62    };
63
64    // If the UI node entity has an `OverrideClip` component, discard any inherited clip rect
65    if has_override_clip {
66        maybe_inherited_clip = None;
67    }
68
69    // If `display` is None, clip the entire node and all its descendants by replacing the inherited clip with a default rect (which is empty)
70    if node.display == Display::None {
71        maybe_inherited_clip = Some(Rect::default());
72    }
73
74    // Update this node's CalculatedClip component
75    if let Some(mut calculated_clip) = maybe_calculated_clip {
76        if let Some(inherited_clip) = maybe_inherited_clip {
77            // Replace the previous calculated clip with the inherited clipping rect
78            if calculated_clip.clip != inherited_clip {
79                *calculated_clip = CalculatedClip {
80                    clip: inherited_clip,
81                };
82            }
83        } else {
84            // No inherited clipping rect, remove the component
85            commands.entity(entity).remove::<CalculatedClip>();
86        }
87    } else if let Some(inherited_clip) = maybe_inherited_clip {
88        // No previous calculated clip, add a new CalculatedClip component with the inherited clipping rect
89        commands.entity(entity).try_insert(CalculatedClip {
90            clip: inherited_clip,
91        });
92    }
93
94    // Calculate new clip rectangle for children nodes
95    let children_clip = if node.overflow.is_visible() {
96        // The current node doesn't clip, propagate the optional inherited clipping rect to any children
97        maybe_inherited_clip
98    } else {
99        // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists
100        let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size());
101
102        // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`].
103        //
104        // `clip_inset` should always fit inside `node_rect`.
105        // Even if `clip_inset` were to overflow, we won't return a degenerate result as `Rect::intersect` will clamp the intersection, leaving it empty.
106        let clip_inset = match node.overflow_clip_margin.visual_box {
107            crate::OverflowClipBox::BorderBox => BorderRect::ZERO,
108            crate::OverflowClipBox::ContentBox => computed_node.content_inset(),
109            crate::OverflowClipBox::PaddingBox => computed_node.border(),
110        };
111
112        clip_rect.min += clip_inset.min_inset;
113        clip_rect.max -= clip_inset.max_inset;
114
115        clip_rect = clip_rect
116            .inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor);
117
118        if node.overflow.x == OverflowAxis::Visible {
119            clip_rect.min.x = -f32::INFINITY;
120            clip_rect.max.x = f32::INFINITY;
121        }
122        if node.overflow.y == OverflowAxis::Visible {
123            clip_rect.min.y = -f32::INFINITY;
124            clip_rect.max.y = f32::INFINITY;
125        }
126        Some(maybe_inherited_clip.map_or(clip_rect, |c| c.intersect(clip_rect)))
127    };
128
129    for child in ui_children.iter_ui_children(entity) {
130        update_clipping(commands, ui_children, node_query, child, children_clip);
131    }
132}
133
134pub fn propagate_ui_target_cameras(
135    mut commands: Commands,
136    default_ui_camera: DefaultUiCamera,
137    ui_scale: Res<UiScale>,
138    camera_query: Query<&Camera>,
139    target_camera_query: Query<&UiTargetCamera>,
140    ui_root_nodes: UiRootNodes,
141) {
142    let default_camera_entity = default_ui_camera.get();
143
144    for root_entity in ui_root_nodes.iter() {
145        let camera = target_camera_query
146            .get(root_entity)
147            .ok()
148            .map(UiTargetCamera::entity)
149            .or(default_camera_entity)
150            .unwrap_or(Entity::PLACEHOLDER);
151
152        commands
153            .entity(root_entity)
154            .try_insert(Propagate(ComputedUiTargetCamera { camera }));
155
156        let (scale_factor, physical_size) = camera_query
157            .get(camera)
158            .ok()
159            .map(|camera| {
160                (
161                    camera.target_scaling_factor().unwrap_or(1.) * ui_scale.0,
162                    camera.physical_viewport_size().unwrap_or(UVec2::ZERO),
163                )
164            })
165            .unwrap_or((1., UVec2::ZERO));
166
167        commands
168            .entity(root_entity)
169            .try_insert(Propagate(ComputedUiRenderTargetInfo {
170                scale_factor,
171                physical_size,
172            }));
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::update::propagate_ui_target_cameras;
179    use crate::ComputedUiRenderTargetInfo;
180    use crate::ComputedUiTargetCamera;
181    use crate::IsDefaultUiCamera;
182    use crate::Node;
183    use crate::UiScale;
184    use crate::UiTargetCamera;
185    use bevy_app::App;
186    use bevy_app::HierarchyPropagatePlugin;
187    use bevy_app::PostUpdate;
188    use bevy_app::PropagateSet;
189    use bevy_camera::Camera;
190    use bevy_camera::Camera2d;
191    use bevy_camera::ComputedCameraValues;
192    use bevy_camera::RenderTargetInfo;
193    use bevy_ecs::hierarchy::ChildOf;
194    use bevy_math::UVec2;
195    use bevy_utils::default;
196
197    fn setup_test_app() -> App {
198        let mut app = App::new();
199
200        app.init_resource::<UiScale>();
201
202        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiTargetCamera>::new(
203            PostUpdate,
204        ));
205        app.configure_sets(
206            PostUpdate,
207            PropagateSet::<ComputedUiTargetCamera>::default(),
208        );
209
210        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiRenderTargetInfo>::new(
211            PostUpdate,
212        ));
213        app.configure_sets(
214            PostUpdate,
215            PropagateSet::<ComputedUiRenderTargetInfo>::default(),
216        );
217
218        app.add_systems(bevy_app::Update, propagate_ui_target_cameras);
219
220        app
221    }
222
223    #[test]
224    fn update_context_for_single_ui_root() {
225        let mut app = setup_test_app();
226        let world = app.world_mut();
227
228        let scale_factor = 10.;
229        let physical_size = UVec2::new(1000, 500);
230
231        let camera = world
232            .spawn((
233                Camera2d,
234                Camera {
235                    computed: ComputedCameraValues {
236                        target_info: Some(RenderTargetInfo {
237                            physical_size,
238                            scale_factor,
239                        }),
240                        ..Default::default()
241                    },
242                    ..Default::default()
243                },
244            ))
245            .id();
246
247        let uinode = world.spawn(Node::default()).id();
248
249        app.update();
250        let world = app.world_mut();
251
252        assert_eq!(
253            *world.get::<ComputedUiTargetCamera>(uinode).unwrap(),
254            ComputedUiTargetCamera { camera }
255        );
256
257        assert_eq!(
258            *world.get::<ComputedUiRenderTargetInfo>(uinode).unwrap(),
259            ComputedUiRenderTargetInfo {
260                physical_size,
261                scale_factor,
262            }
263        );
264    }
265
266    #[test]
267    fn update_multiple_context_for_multiple_ui_roots() {
268        let mut app = setup_test_app();
269        let world = app.world_mut();
270
271        let scale1 = 1.;
272        let size1 = UVec2::new(100, 100);
273        let scale2 = 2.;
274        let size2 = UVec2::new(200, 200);
275
276        let camera1 = world
277            .spawn((
278                Camera2d,
279                IsDefaultUiCamera,
280                Camera {
281                    computed: ComputedCameraValues {
282                        target_info: Some(RenderTargetInfo {
283                            physical_size: size1,
284                            scale_factor: scale1,
285                        }),
286                        ..Default::default()
287                    },
288                    ..Default::default()
289                },
290            ))
291            .id();
292        let camera2 = world
293            .spawn((
294                Camera2d,
295                Camera {
296                    computed: ComputedCameraValues {
297                        target_info: Some(RenderTargetInfo {
298                            physical_size: size2,
299                            scale_factor: scale2,
300                        }),
301                        ..Default::default()
302                    },
303                    ..default()
304                },
305            ))
306            .id();
307
308        let uinode1a = world.spawn(Node::default()).id();
309        let uinode2a = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
310        let uinode2b = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
311        let uinode2c = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
312        let uinode1b = world.spawn(Node::default()).id();
313
314        app.update();
315        let world = app.world_mut();
316
317        for (uinode, camera, scale_factor, physical_size) in [
318            (uinode1a, camera1, scale1, size1),
319            (uinode1b, camera1, scale1, size1),
320            (uinode2a, camera2, scale2, size2),
321            (uinode2b, camera2, scale2, size2),
322            (uinode2c, camera2, scale2, size2),
323        ] {
324            assert_eq!(
325                *world.get::<ComputedUiTargetCamera>(uinode).unwrap(),
326                ComputedUiTargetCamera { camera }
327            );
328
329            assert_eq!(
330                *world.get::<ComputedUiRenderTargetInfo>(uinode).unwrap(),
331                ComputedUiRenderTargetInfo {
332                    physical_size,
333                    scale_factor,
334                }
335            );
336        }
337    }
338
339    #[test]
340    fn update_context_on_changed_camera() {
341        let mut app = setup_test_app();
342        let world = app.world_mut();
343
344        let scale1 = 1.;
345        let size1 = UVec2::new(100, 100);
346        let scale2 = 2.;
347        let size2 = UVec2::new(200, 200);
348
349        let camera1 = world
350            .spawn((
351                Camera2d,
352                IsDefaultUiCamera,
353                Camera {
354                    computed: ComputedCameraValues {
355                        target_info: Some(RenderTargetInfo {
356                            physical_size: size1,
357                            scale_factor: scale1,
358                        }),
359                        ..Default::default()
360                    },
361                    ..Default::default()
362                },
363            ))
364            .id();
365        let camera2 = world
366            .spawn((
367                Camera2d,
368                Camera {
369                    computed: ComputedCameraValues {
370                        target_info: Some(RenderTargetInfo {
371                            physical_size: size2,
372                            scale_factor: scale2,
373                        }),
374                        ..Default::default()
375                    },
376                    ..default()
377                },
378            ))
379            .id();
380
381        let uinode = world.spawn(Node::default()).id();
382
383        app.update();
384        let world = app.world_mut();
385
386        assert_eq!(
387            world
388                .get::<ComputedUiRenderTargetInfo>(uinode)
389                .unwrap()
390                .scale_factor,
391            scale1
392        );
393
394        assert_eq!(
395            world
396                .get::<ComputedUiRenderTargetInfo>(uinode)
397                .unwrap()
398                .physical_size,
399            size1
400        );
401
402        assert_eq!(
403            world
404                .get::<ComputedUiTargetCamera>(uinode)
405                .unwrap()
406                .get()
407                .unwrap(),
408            camera1
409        );
410
411        world.entity_mut(uinode).insert(UiTargetCamera(camera2));
412
413        app.update();
414        let world = app.world_mut();
415
416        assert_eq!(
417            world
418                .get::<ComputedUiRenderTargetInfo>(uinode)
419                .unwrap()
420                .scale_factor,
421            scale2
422        );
423
424        assert_eq!(
425            world
426                .get::<ComputedUiRenderTargetInfo>(uinode)
427                .unwrap()
428                .physical_size,
429            size2
430        );
431
432        assert_eq!(
433            world
434                .get::<ComputedUiTargetCamera>(uinode)
435                .unwrap()
436                .get()
437                .unwrap(),
438            camera2
439        );
440    }
441
442    #[test]
443    fn update_context_after_parent_removed() {
444        let mut app = setup_test_app();
445        let world = app.world_mut();
446
447        let scale1 = 1.;
448        let size1 = UVec2::new(100, 100);
449        let scale2 = 2.;
450        let size2 = UVec2::new(200, 200);
451
452        let camera1 = world
453            .spawn((
454                Camera2d,
455                IsDefaultUiCamera,
456                Camera {
457                    computed: ComputedCameraValues {
458                        target_info: Some(RenderTargetInfo {
459                            physical_size: size1,
460                            scale_factor: scale1,
461                        }),
462                        ..Default::default()
463                    },
464                    ..Default::default()
465                },
466            ))
467            .id();
468        let camera2 = world
469            .spawn((
470                Camera2d,
471                Camera {
472                    computed: ComputedCameraValues {
473                        target_info: Some(RenderTargetInfo {
474                            physical_size: size2,
475                            scale_factor: scale2,
476                        }),
477                        ..Default::default()
478                    },
479                    ..default()
480                },
481            ))
482            .id();
483
484        // `UiTargetCamera` is ignored on non-root UI nodes
485        let uinode1 = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
486        let uinode2 = world.spawn(Node::default()).add_child(uinode1).id();
487
488        app.update();
489        let world = app.world_mut();
490
491        assert_eq!(
492            world
493                .get::<ComputedUiRenderTargetInfo>(uinode1)
494                .unwrap()
495                .scale_factor(),
496            scale1
497        );
498
499        assert_eq!(
500            world
501                .get::<ComputedUiRenderTargetInfo>(uinode1)
502                .unwrap()
503                .physical_size(),
504            size1
505        );
506
507        assert_eq!(
508            world
509                .get::<ComputedUiTargetCamera>(uinode1)
510                .unwrap()
511                .get()
512                .unwrap(),
513            camera1
514        );
515
516        assert_eq!(
517            world
518                .get::<ComputedUiTargetCamera>(uinode2)
519                .unwrap()
520                .get()
521                .unwrap(),
522            camera1
523        );
524
525        // Now `uinode1` is a root UI node its `UiTargetCamera` component will be used and its camera target set to `camera2`.
526        world.entity_mut(uinode1).remove::<ChildOf>();
527
528        app.update();
529        let world = app.world_mut();
530
531        assert_eq!(
532            world
533                .get::<ComputedUiRenderTargetInfo>(uinode1)
534                .unwrap()
535                .scale_factor(),
536            scale2
537        );
538
539        assert_eq!(
540            world
541                .get::<ComputedUiRenderTargetInfo>(uinode1)
542                .unwrap()
543                .physical_size(),
544            size2
545        );
546
547        assert_eq!(
548            world
549                .get::<ComputedUiTargetCamera>(uinode1)
550                .unwrap()
551                .get()
552                .unwrap(),
553            camera2
554        );
555
556        assert_eq!(
557            world
558                .get::<ComputedUiTargetCamera>(uinode2)
559                .unwrap()
560                .get()
561                .unwrap(),
562            camera1
563        );
564    }
565
566    #[test]
567    fn update_great_grandchild() {
568        let mut app = setup_test_app();
569        let world = app.world_mut();
570
571        let scale = 1.;
572        let size = UVec2::new(100, 100);
573
574        let camera = world
575            .spawn((
576                Camera2d,
577                Camera {
578                    computed: ComputedCameraValues {
579                        target_info: Some(RenderTargetInfo {
580                            physical_size: size,
581                            scale_factor: scale,
582                        }),
583                        ..Default::default()
584                    },
585                    ..Default::default()
586                },
587            ))
588            .id();
589
590        let uinode = world.spawn(Node::default()).id();
591        world.spawn(Node::default()).with_children(|builder| {
592            builder.spawn(Node::default()).with_children(|builder| {
593                builder.spawn(Node::default()).add_child(uinode);
594            });
595        });
596
597        app.update();
598        let world = app.world_mut();
599
600        assert_eq!(
601            world
602                .get::<ComputedUiRenderTargetInfo>(uinode)
603                .unwrap()
604                .scale_factor,
605            scale
606        );
607
608        assert_eq!(
609            world
610                .get::<ComputedUiRenderTargetInfo>(uinode)
611                .unwrap()
612                .physical_size,
613            size
614        );
615
616        assert_eq!(
617            world
618                .get::<ComputedUiTargetCamera>(uinode)
619                .unwrap()
620                .get()
621                .unwrap(),
622            camera
623        );
624
625        world.resource_mut::<UiScale>().0 = 2.;
626
627        app.update();
628        let world = app.world_mut();
629
630        assert_eq!(
631            world
632                .get::<ComputedUiRenderTargetInfo>(uinode)
633                .unwrap()
634                .scale_factor(),
635            2.
636        );
637    }
638}