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.x += clip_inset.left;
113        clip_rect.min.y += clip_inset.top;
114        clip_rect.max.x -= clip_inset.right + computed_node.scrollbar_size.x;
115        clip_rect.max.y -= clip_inset.bottom + computed_node.scrollbar_size.y;
116
117        clip_rect = clip_rect
118            .inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor);
119
120        if node.overflow.x == OverflowAxis::Visible {
121            clip_rect.min.x = -f32::INFINITY;
122            clip_rect.max.x = f32::INFINITY;
123        }
124        if node.overflow.y == OverflowAxis::Visible {
125            clip_rect.min.y = -f32::INFINITY;
126            clip_rect.max.y = f32::INFINITY;
127        }
128        Some(maybe_inherited_clip.map_or(clip_rect, |c| c.intersect(clip_rect)))
129    };
130
131    for child in ui_children.iter_ui_children(entity) {
132        update_clipping(commands, ui_children, node_query, child, children_clip);
133    }
134}
135
136pub fn propagate_ui_target_cameras(
137    mut commands: Commands,
138    default_ui_camera: DefaultUiCamera,
139    ui_scale: Res<UiScale>,
140    camera_query: Query<&Camera>,
141    target_camera_query: Query<&UiTargetCamera>,
142    ui_root_nodes: UiRootNodes,
143) {
144    let default_camera_entity = default_ui_camera.get();
145
146    for root_entity in ui_root_nodes.iter() {
147        let camera = target_camera_query
148            .get(root_entity)
149            .ok()
150            .map(UiTargetCamera::entity)
151            .or(default_camera_entity)
152            .unwrap_or(Entity::PLACEHOLDER);
153
154        commands
155            .entity(root_entity)
156            .insert(Propagate(ComputedUiTargetCamera { camera }));
157
158        let (scale_factor, physical_size) = camera_query
159            .get(camera)
160            .ok()
161            .map(|camera| {
162                (
163                    camera.target_scaling_factor().unwrap_or(1.) * ui_scale.0,
164                    camera.physical_viewport_size().unwrap_or(UVec2::ZERO),
165                )
166            })
167            .unwrap_or((1., UVec2::ZERO));
168
169        commands
170            .entity(root_entity)
171            .insert(Propagate(ComputedUiRenderTargetInfo {
172                scale_factor,
173                physical_size,
174            }));
175    }
176}
177
178/// Update each `Camera`'s `RenderTargetInfo` from its associated `Window` render target.
179/// Cameras with non-window render targets are ignored.
180#[cfg(test)]
181pub(crate) fn update_cameras_test_system(
182    primary_window: Query<Entity, bevy_ecs::query::With<bevy_window::PrimaryWindow>>,
183    window_query: Query<&bevy_window::Window>,
184    mut camera_query: Query<&mut Camera>,
185) {
186    let primary_window = primary_window.single().ok();
187    for mut camera in camera_query.iter_mut() {
188        let Some(camera_target) = camera.target.normalize(primary_window) else {
189            continue;
190        };
191        let bevy_camera::NormalizedRenderTarget::Window(window_ref) = camera_target else {
192            continue;
193        };
194        let Ok(window) = window_query.get(bevy_ecs::entity::ContainsEntity::entity(&window_ref))
195        else {
196            continue;
197        };
198
199        let render_target_info = bevy_camera::RenderTargetInfo {
200            physical_size: window.physical_size(),
201            scale_factor: window.scale_factor(),
202        };
203        camera.computed.target_info = Some(render_target_info);
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use crate::update::propagate_ui_target_cameras;
210    use crate::ComputedUiRenderTargetInfo;
211    use crate::ComputedUiTargetCamera;
212    use crate::IsDefaultUiCamera;
213    use crate::Node;
214    use crate::UiScale;
215    use crate::UiTargetCamera;
216    use bevy_app::App;
217    use bevy_app::HierarchyPropagatePlugin;
218    use bevy_app::PostUpdate;
219    use bevy_app::PropagateSet;
220    use bevy_camera::Camera;
221    use bevy_camera::Camera2d;
222    use bevy_camera::RenderTarget;
223    use bevy_ecs::hierarchy::ChildOf;
224    use bevy_ecs::schedule::IntoScheduleConfigs;
225    use bevy_math::UVec2;
226    use bevy_utils::default;
227    use bevy_window::PrimaryWindow;
228    use bevy_window::Window;
229    use bevy_window::WindowRef;
230    use bevy_window::WindowResolution;
231
232    fn setup_test_app() -> App {
233        let mut app = App::new();
234
235        app.init_resource::<UiScale>();
236
237        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiTargetCamera>::new(
238            PostUpdate,
239        ));
240        app.configure_sets(
241            PostUpdate,
242            PropagateSet::<ComputedUiTargetCamera>::default(),
243        );
244
245        app.add_plugins(HierarchyPropagatePlugin::<ComputedUiRenderTargetInfo>::new(
246            PostUpdate,
247        ));
248        app.configure_sets(
249            PostUpdate,
250            PropagateSet::<ComputedUiRenderTargetInfo>::default(),
251        );
252
253        app.add_systems(
254            bevy_app::Update,
255            (
256                super::update_cameras_test_system,
257                propagate_ui_target_cameras,
258            )
259                .chain(),
260        );
261
262        app
263    }
264
265    #[test]
266    fn update_context_for_single_ui_root() {
267        let mut app = setup_test_app();
268        let world = app.world_mut();
269
270        let scale_factor = 10.;
271        let physical_size = UVec2::new(1000, 500);
272
273        world.spawn((
274            Window {
275                resolution: WindowResolution::from(physical_size).with_scale_factor_override(10.),
276                ..Default::default()
277            },
278            PrimaryWindow,
279        ));
280
281        let camera = world.spawn(Camera2d).id();
282
283        let uinode = world.spawn(Node::default()).id();
284
285        app.update();
286        let world = app.world_mut();
287
288        assert_eq!(
289            *world.get::<ComputedUiTargetCamera>(uinode).unwrap(),
290            ComputedUiTargetCamera { camera }
291        );
292
293        assert_eq!(
294            *world.get::<ComputedUiRenderTargetInfo>(uinode).unwrap(),
295            ComputedUiRenderTargetInfo {
296                physical_size,
297                scale_factor,
298            }
299        );
300    }
301
302    #[test]
303    fn update_multiple_context_for_multiple_ui_roots() {
304        let mut app = setup_test_app();
305        let world = app.world_mut();
306
307        let scale1 = 1.;
308        let size1 = UVec2::new(100, 100);
309        let scale2 = 2.;
310        let size2 = UVec2::new(200, 200);
311
312        world.spawn((
313            Window {
314                resolution: WindowResolution::from(size1).with_scale_factor_override(scale1),
315                ..Default::default()
316            },
317            PrimaryWindow,
318        ));
319
320        let window_2 = world
321            .spawn((Window {
322                resolution: WindowResolution::from(size2).with_scale_factor_override(scale2),
323                ..Default::default()
324            },))
325            .id();
326
327        let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
328        let camera2 = world
329            .spawn((
330                Camera2d,
331                Camera {
332                    target: RenderTarget::Window(WindowRef::Entity(window_2)),
333                    ..default()
334                },
335            ))
336            .id();
337
338        let uinode1a = world.spawn(Node::default()).id();
339        let uinode2a = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
340        let uinode2b = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
341        let uinode2c = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
342        let uinode1b = world.spawn(Node::default()).id();
343
344        app.update();
345        let world = app.world_mut();
346
347        for (uinode, camera, scale_factor, physical_size) in [
348            (uinode1a, camera1, scale1, size1),
349            (uinode1b, camera1, scale1, size1),
350            (uinode2a, camera2, scale2, size2),
351            (uinode2b, camera2, scale2, size2),
352            (uinode2c, camera2, scale2, size2),
353        ] {
354            assert_eq!(
355                *world.get::<ComputedUiTargetCamera>(uinode).unwrap(),
356                ComputedUiTargetCamera { camera }
357            );
358
359            assert_eq!(
360                *world.get::<ComputedUiRenderTargetInfo>(uinode).unwrap(),
361                ComputedUiRenderTargetInfo {
362                    physical_size,
363                    scale_factor,
364                }
365            );
366        }
367    }
368
369    #[test]
370    fn update_context_on_changed_camera() {
371        let mut app = setup_test_app();
372        let world = app.world_mut();
373
374        let scale1 = 1.;
375        let size1 = UVec2::new(100, 100);
376        let scale2 = 2.;
377        let size2 = UVec2::new(200, 200);
378
379        world.spawn((
380            Window {
381                resolution: WindowResolution::from(size1).with_scale_factor_override(scale1),
382                ..Default::default()
383            },
384            PrimaryWindow,
385        ));
386
387        let window_2 = world
388            .spawn((Window {
389                resolution: WindowResolution::from(size2).with_scale_factor_override(scale2),
390                ..Default::default()
391            },))
392            .id();
393
394        let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
395        let camera2 = world
396            .spawn((
397                Camera2d,
398                Camera {
399                    target: RenderTarget::Window(WindowRef::Entity(window_2)),
400                    ..default()
401                },
402            ))
403            .id();
404
405        let uinode = world.spawn(Node::default()).id();
406
407        app.update();
408        let world = app.world_mut();
409
410        assert_eq!(
411            world
412                .get::<ComputedUiRenderTargetInfo>(uinode)
413                .unwrap()
414                .scale_factor,
415            scale1
416        );
417
418        assert_eq!(
419            world
420                .get::<ComputedUiRenderTargetInfo>(uinode)
421                .unwrap()
422                .physical_size,
423            size1
424        );
425
426        assert_eq!(
427            world
428                .get::<ComputedUiTargetCamera>(uinode)
429                .unwrap()
430                .get()
431                .unwrap(),
432            camera1
433        );
434
435        world.entity_mut(uinode).insert(UiTargetCamera(camera2));
436
437        app.update();
438        let world = app.world_mut();
439
440        assert_eq!(
441            world
442                .get::<ComputedUiRenderTargetInfo>(uinode)
443                .unwrap()
444                .scale_factor,
445            scale2
446        );
447
448        assert_eq!(
449            world
450                .get::<ComputedUiRenderTargetInfo>(uinode)
451                .unwrap()
452                .physical_size,
453            size2
454        );
455
456        assert_eq!(
457            world
458                .get::<ComputedUiTargetCamera>(uinode)
459                .unwrap()
460                .get()
461                .unwrap(),
462            camera2
463        );
464    }
465
466    #[test]
467    fn update_context_after_parent_removed() {
468        let mut app = setup_test_app();
469        let world = app.world_mut();
470
471        let scale1 = 1.;
472        let size1 = UVec2::new(100, 100);
473        let scale2 = 2.;
474        let size2 = UVec2::new(200, 200);
475
476        world.spawn((
477            Window {
478                resolution: WindowResolution::from(size1).with_scale_factor_override(scale1),
479                ..Default::default()
480            },
481            PrimaryWindow,
482        ));
483
484        let window_2 = world
485            .spawn((Window {
486                resolution: WindowResolution::from(size2).with_scale_factor_override(scale2),
487                ..Default::default()
488            },))
489            .id();
490
491        let camera1 = world.spawn((Camera2d, IsDefaultUiCamera)).id();
492        let camera2 = world
493            .spawn((
494                Camera2d,
495                Camera {
496                    target: RenderTarget::Window(WindowRef::Entity(window_2)),
497                    ..default()
498                },
499            ))
500            .id();
501
502        // `UiTargetCamera` is ignored on non-root UI nodes
503        let uinode1 = world.spawn((Node::default(), UiTargetCamera(camera2))).id();
504        let uinode2 = world.spawn(Node::default()).add_child(uinode1).id();
505
506        app.update();
507        let world = app.world_mut();
508
509        assert_eq!(
510            world
511                .get::<ComputedUiRenderTargetInfo>(uinode1)
512                .unwrap()
513                .scale_factor(),
514            scale1
515        );
516
517        assert_eq!(
518            world
519                .get::<ComputedUiRenderTargetInfo>(uinode1)
520                .unwrap()
521                .physical_size(),
522            size1
523        );
524
525        assert_eq!(
526            world
527                .get::<ComputedUiTargetCamera>(uinode1)
528                .unwrap()
529                .get()
530                .unwrap(),
531            camera1
532        );
533
534        assert_eq!(
535            world
536                .get::<ComputedUiTargetCamera>(uinode2)
537                .unwrap()
538                .get()
539                .unwrap(),
540            camera1
541        );
542
543        // Now `uinode1` is a root UI node its `UiTargetCamera` component will be used and its camera target set to `camera2`.
544        world.entity_mut(uinode1).remove::<ChildOf>();
545
546        app.update();
547        let world = app.world_mut();
548
549        assert_eq!(
550            world
551                .get::<ComputedUiRenderTargetInfo>(uinode1)
552                .unwrap()
553                .scale_factor(),
554            scale2
555        );
556
557        assert_eq!(
558            world
559                .get::<ComputedUiRenderTargetInfo>(uinode1)
560                .unwrap()
561                .physical_size(),
562            size2
563        );
564
565        assert_eq!(
566            world
567                .get::<ComputedUiTargetCamera>(uinode1)
568                .unwrap()
569                .get()
570                .unwrap(),
571            camera2
572        );
573
574        assert_eq!(
575            world
576                .get::<ComputedUiTargetCamera>(uinode2)
577                .unwrap()
578                .get()
579                .unwrap(),
580            camera1
581        );
582    }
583
584    #[test]
585    fn update_great_grandchild() {
586        let mut app = setup_test_app();
587        let world = app.world_mut();
588
589        let scale = 1.;
590        let size = UVec2::new(100, 100);
591
592        world.spawn((
593            Window {
594                resolution: WindowResolution::from(size).with_scale_factor_override(scale),
595                ..Default::default()
596            },
597            PrimaryWindow,
598        ));
599
600        let camera = world.spawn(Camera2d).id();
601
602        let uinode = world.spawn(Node::default()).id();
603        world.spawn(Node::default()).with_children(|builder| {
604            builder.spawn(Node::default()).with_children(|builder| {
605                builder.spawn(Node::default()).add_child(uinode);
606            });
607        });
608
609        app.update();
610        let world = app.world_mut();
611
612        assert_eq!(
613            world
614                .get::<ComputedUiRenderTargetInfo>(uinode)
615                .unwrap()
616                .scale_factor,
617            scale
618        );
619
620        assert_eq!(
621            world
622                .get::<ComputedUiRenderTargetInfo>(uinode)
623                .unwrap()
624                .physical_size,
625            size
626        );
627
628        assert_eq!(
629            world
630                .get::<ComputedUiTargetCamera>(uinode)
631                .unwrap()
632                .get()
633                .unwrap(),
634            camera
635        );
636
637        world.resource_mut::<UiScale>().0 = 2.;
638
639        app.update();
640        let world = app.world_mut();
641
642        assert_eq!(
643            world
644                .get::<ComputedUiRenderTargetInfo>(uinode)
645                .unwrap()
646                .scale_factor(),
647            2.
648        );
649    }
650}