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 #[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
72pub 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 for entity in removed_content_sizes.read() {
102 ui_surface.try_remove_node_context(entity);
103 }
104
105 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 for entity in removed_children.read() {
126 ui_surface.try_remove_children(entity);
127 }
128
129 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 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 let layout_location = Vec2::new(layout.location.x, layout.location.y);
233
234 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 let local_center =
241 layout_location - effective_parent_scroll + 0.5 * (layout_size - parent_size);
242
243 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 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 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 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 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 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 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 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 app.update();
519 let world = app.world_mut();
520
521 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 app.update();
530 let world = app.world_mut();
531
532 let ui_surface = world.resource::<UiSurface>();
533
534 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 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 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 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 for node in child_node_map.values() {
585 assert_eq!(ui_surface.taffy.parent(node.id), Some(ui_parent_node.id));
586 }
587
588 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 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 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 world.entity_mut(ui_parent_entity).despawn();
639
640 app.update();
641 let world = app.world_mut();
642
643 let ui_surface = world.resource::<UiSurface>();
645 assert!(ui_surface.entity_to_taffy.is_empty());
646 }
647
648 #[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 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 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 world.entity_mut(ui_entity).remove::<Node>();
672 world.entity_mut(ui_entity).insert(Node::default());
673
674 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 let root_node = world.spawn(()).with_child(Node::default()).id();
690
691 app.update();
692 let world = app.world_mut();
693
694 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 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 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 #[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 width: Val::Px(size),
747 height: Val::Px(size),
748 ..default()
749 });
750
751 size -= 50.;
752
753 world.spawn(Node {
754 width: Val::Px(size),
756 height: Val::Px(size),
757 ..default()
758 });
759
760 size -= 50.;
761
762 world.spawn(Node {
763 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 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 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 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 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 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 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 assert!(ui_surface.taffy.get_node_context(ui_node.id).is_none());
1015
1016 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 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 app.update();
1175 let world = app.world_mut();
1176
1177 assert_eq!(
1181 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1182 2
1183 );
1184
1185 world.despawn(ui_root_entity);
1186
1187 app.update();
1190 let world = app.world_mut();
1191
1192 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 assert_eq!(
1213 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1214 4
1215 );
1216
1217 world
1220 .entity_mut(ui_root_entity_1)
1221 .add_child(ui_root_entity_2);
1222
1223 app.update();
1226 let world = app.world_mut();
1227
1228 assert_eq!(
1230 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1231 3
1232 );
1233 }
1234}