1use crate::{
2 experimental::{UiChildren, UiRootNodes},
3 ui_transform::{UiGlobalTransform, UiTransform},
4 BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Display, 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(crate) 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::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<&BorderRadius>,
91 Option<&Outline>,
92 Option<&ScrollPosition>,
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<&BorderRadius>,
202 Option<&Outline>,
203 Option<&ScrollPosition>,
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_border_radius,
217 maybe_outline,
218 maybe_scroll_position,
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 local_center =
236 layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
237
238 if node.size != layout_size
240 || node.unrounded_size != unrounded_size
241 || node.inverse_scale_factor != inverse_target_scale_factor
242 {
243 node.size = layout_size;
244 node.unrounded_size = unrounded_size;
245 node.inverse_scale_factor = inverse_target_scale_factor;
246 }
247
248 let content_size = Vec2::new(layout.content_size.width, layout.content_size.height);
249 node.bypass_change_detection().content_size = content_size;
250
251 let taffy_rect_to_border_rect = |rect: taffy::Rect<f32>| BorderRect {
252 left: rect.left,
253 right: rect.right,
254 top: rect.top,
255 bottom: rect.bottom,
256 };
257
258 node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border);
259 node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding);
260
261 let mut local_transform = transform.compute_affine(
263 inverse_target_scale_factor.recip(),
264 layout_size,
265 target_size,
266 );
267 local_transform.translation += local_center;
268 inherited_transform *= local_transform;
269
270 if inherited_transform != **global_transform {
271 *global_transform = inherited_transform.into();
272 }
273
274 if let Some(border_radius) = maybe_border_radius {
275 node.bypass_change_detection().border_radius = border_radius.resolve(
277 inverse_target_scale_factor.recip(),
278 node.size,
279 target_size,
280 );
281 }
282
283 if let Some(outline) = maybe_outline {
284 let node = node.bypass_change_detection();
286 node.outline_width = if style.display != Display::None {
287 outline
288 .width
289 .resolve(
290 inverse_target_scale_factor.recip(),
291 node.size().x,
292 target_size,
293 )
294 .unwrap_or(0.)
295 .max(0.)
296 } else {
297 0.
298 };
299
300 node.outline_offset = outline
301 .offset
302 .resolve(
303 inverse_target_scale_factor.recip(),
304 node.size().x,
305 target_size,
306 )
307 .unwrap_or(0.)
308 .max(0.);
309 }
310
311 node.bypass_change_detection().scrollbar_size =
312 Vec2::new(layout.scrollbar_size.width, layout.scrollbar_size.height);
313
314 let scroll_position: Vec2 = maybe_scroll_position
315 .map(|scroll_pos| {
316 Vec2::new(
317 if style.overflow.x == OverflowAxis::Scroll {
318 scroll_pos.x * inverse_target_scale_factor.recip()
319 } else {
320 0.0
321 },
322 if style.overflow.y == OverflowAxis::Scroll {
323 scroll_pos.y * inverse_target_scale_factor.recip()
324 } else {
325 0.0
326 },
327 )
328 })
329 .unwrap_or_default();
330
331 let max_possible_offset =
332 (content_size - layout_size + node.scrollbar_size).max(Vec2::ZERO);
333 let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);
334
335 let physical_scroll_position = clamped_scroll_position.floor();
336
337 node.bypass_change_detection().scroll_position = physical_scroll_position;
338
339 for child_uinode in ui_children.iter_ui_children(entity) {
340 update_uinode_geometry_recursive(
341 child_uinode,
342 ui_surface,
343 use_rounding,
344 target_size,
345 inherited_transform,
346 node_update_query,
347 ui_children,
348 inverse_target_scale_factor,
349 layout_size,
350 physical_scroll_position,
351 );
352 }
353 }
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use crate::update::update_cameras_test_system;
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};
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 use bevy_window::{PrimaryWindow, Window, WindowResolution};
373 use taffy::TraversePartialTree;
374
375 const WINDOW_WIDTH: u32 = 1000;
377 const WINDOW_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
394 app.add_systems(
395 PostUpdate,
396 (
397 update_cameras_test_system,
398 propagate_ui_target_cameras,
399 ApplyDeferred,
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 Window {
426 resolution: WindowResolution::new(WINDOW_WIDTH, WINDOW_HEIGHT),
427 ..default()
428 },
429 PrimaryWindow,
430 ));
431 world.spawn(Camera2d);
432
433 app
434 }
435
436 #[test]
437 fn ui_nodes_with_percent_100_dimensions_should_fill_their_parent() {
438 let mut app = setup_ui_test_app();
439
440 let world = app.world_mut();
441
442 let ui_root = world
444 .spawn(Node {
445 width: Val::Percent(100.),
446 height: Val::Percent(100.),
447 ..default()
448 })
449 .id();
450
451 let ui_child = world
452 .spawn(Node {
453 width: Val::Percent(100.),
454 height: Val::Percent(100.),
455 ..default()
456 })
457 .id();
458
459 world.entity_mut(ui_root).add_child(ui_child);
460
461 app.update();
462
463 let mut ui_surface = app.world_mut().resource_mut::<UiSurface>();
464
465 for ui_entity in [ui_root, ui_child] {
466 let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
467 assert_eq!(layout.size.width, WINDOW_WIDTH as f32);
468 assert_eq!(layout.size.height, WINDOW_HEIGHT as f32);
469 }
470 }
471
472 #[test]
473 fn ui_surface_tracks_ui_entities() {
474 let mut app = setup_ui_test_app();
475
476 let world = app.world_mut();
477 let ui_surface = world.resource::<UiSurface>();
479 assert!(ui_surface.entity_to_taffy.is_empty());
480
481 let ui_entity = world.spawn(Node::default()).id();
482
483 app.update();
484 let world = app.world_mut();
485
486 let ui_surface = world.resource::<UiSurface>();
487 assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
488 assert_eq!(ui_surface.entity_to_taffy.len(), 1);
489
490 world.despawn(ui_entity);
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!(ui_surface.entity_to_taffy.is_empty());
498 }
499
500 #[test]
501 #[should_panic]
502 fn despawning_a_ui_entity_should_remove_its_corresponding_ui_node() {
503 let mut app = setup_ui_test_app();
504 let world = app.world_mut();
505
506 let ui_entity = world.spawn(Node::default()).id();
507
508 app.update();
510 let world = app.world_mut();
511
512 let ui_surface = world.resource::<UiSurface>();
514 let ui_node = ui_surface.entity_to_taffy[&ui_entity];
515
516 world.despawn(ui_entity);
517
518 app.update();
521 let world = app.world_mut();
522
523 let ui_surface = world.resource::<UiSurface>();
524
525 let _ = ui_surface.taffy.style(ui_node.id);
527 }
528
529 #[test]
530 fn changes_to_children_of_a_ui_entity_change_its_corresponding_ui_nodes_children() {
531 let mut app = setup_ui_test_app();
532 let world = app.world_mut();
533
534 let ui_parent_entity = world.spawn(Node::default()).id();
535
536 app.update();
538 let world = app.world_mut();
539
540 let ui_surface = world.resource::<UiSurface>();
541 let ui_parent_node = ui_surface.entity_to_taffy[&ui_parent_entity];
542
543 assert_eq!(ui_surface.taffy.child_count(ui_parent_node.id), 0);
545
546 let mut ui_child_entities = (0..10)
547 .map(|_| {
548 let child = world.spawn(Node::default()).id();
549 world.entity_mut(ui_parent_entity).add_child(child);
550 child
551 })
552 .collect::<Vec<_>>();
553
554 app.update();
555 let world = app.world_mut();
556
557 let ui_surface = world.resource::<UiSurface>();
559 assert_eq!(
560 ui_surface.entity_to_taffy.len(),
561 1 + ui_child_entities.len()
562 );
563 assert_eq!(
564 ui_surface.taffy.child_count(ui_parent_node.id),
565 ui_child_entities.len()
566 );
567
568 let child_node_map = <HashMap<_, _>>::from_iter(
569 ui_child_entities
570 .iter()
571 .map(|child_entity| (*child_entity, ui_surface.entity_to_taffy[child_entity])),
572 );
573
574 for node in child_node_map.values() {
576 assert_eq!(ui_surface.taffy.parent(node.id), Some(ui_parent_node.id));
577 }
578
579 let mut deleted_children = vec![];
581 for i in (0..ui_child_entities.len()).rev().step_by(2) {
582 let child = ui_child_entities.remove(i);
583 world.despawn(child);
584 deleted_children.push(child);
585 }
586
587 app.update();
588 let world = app.world_mut();
589
590 let ui_surface = world.resource::<UiSurface>();
591 assert_eq!(
592 ui_surface.entity_to_taffy.len(),
593 1 + ui_child_entities.len()
594 );
595 assert_eq!(
596 ui_surface.taffy.child_count(ui_parent_node.id),
597 ui_child_entities.len()
598 );
599
600 for child_entity in &ui_child_entities {
602 let child_node = child_node_map[child_entity];
603 assert_eq!(ui_surface.entity_to_taffy[child_entity], child_node);
604 assert_eq!(
605 ui_surface.taffy.parent(child_node.id),
606 Some(ui_parent_node.id)
607 );
608 assert!(ui_surface
609 .taffy
610 .children(ui_parent_node.id)
611 .unwrap()
612 .contains(&child_node.id));
613 }
614
615 for deleted_child_entity in &deleted_children {
617 assert!(!ui_surface
618 .entity_to_taffy
619 .contains_key(deleted_child_entity));
620 let deleted_child_node = child_node_map[deleted_child_entity];
621 assert!(!ui_surface
622 .taffy
623 .children(ui_parent_node.id)
624 .unwrap()
625 .contains(&deleted_child_node.id));
626 }
627
628 world.entity_mut(ui_parent_entity).despawn();
630
631 app.update();
632 let world = app.world_mut();
633
634 let ui_surface = world.resource::<UiSurface>();
636 assert!(ui_surface.entity_to_taffy.is_empty());
637 }
638
639 #[test]
641 fn node_removal_and_reinsert_should_work() {
642 let mut app = setup_ui_test_app();
643
644 app.update();
645 let world = app.world_mut();
646
647 let ui_surface = world.resource::<UiSurface>();
649 assert!(ui_surface.entity_to_taffy.is_empty());
650
651 let ui_entity = world.spawn(Node::default()).id();
652
653 app.update();
655 let world = app.world_mut();
656
657 let ui_surface = world.resource::<UiSurface>();
658 assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
659 assert_eq!(ui_surface.entity_to_taffy.len(), 1);
660
661 world.entity_mut(ui_entity).remove::<Node>();
663 world.entity_mut(ui_entity).insert(Node::default());
664
665 app.update();
667 let world = app.world_mut();
668
669 let ui_surface = world.resource::<UiSurface>();
670 assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
671 assert_eq!(ui_surface.entity_to_taffy.len(), 1);
672 }
673
674 #[test]
675 fn node_addition_should_sync_children() {
676 let mut app = setup_ui_test_app();
677 let world = app.world_mut();
678
679 let root_node = world.spawn(()).with_child(Node::default()).id();
681
682 app.update();
683 let world = app.world_mut();
684
685 world.entity_mut(root_node).insert(Node::default());
687
688 app.update();
689 let world = app.world_mut();
690
691 let ui_surface = world.resource_mut::<UiSurface>();
692 let taffy_root = ui_surface.entity_to_taffy[&root_node];
693
694 assert_eq!(ui_surface.taffy.child_count(taffy_root.id), 1);
696 }
697
698 #[test]
699 fn node_addition_should_sync_parent_and_children() {
700 let mut app = setup_ui_test_app();
701 let world = app.world_mut();
702
703 let d = world.spawn(Node::default()).id();
704 let c = world.spawn(()).add_child(d).id();
705 let b = world.spawn(Node::default()).id();
706 let a = world.spawn(Node::default()).add_children(&[b, c]).id();
707
708 app.update();
709 let world = app.world_mut();
710
711 world.entity_mut(c).insert(Node::default());
713
714 app.update();
715 let world = app.world_mut();
716
717 let ui_surface = world.resource::<UiSurface>();
718 for (entity, n) in [(a, 2), (b, 0), (c, 1), (d, 0)] {
719 let taffy_id = ui_surface.entity_to_taffy[&entity].id;
720 assert_eq!(ui_surface.taffy.child_count(taffy_id), n);
721 }
722 }
723
724 #[test]
728 fn ui_root_node_should_act_like_position_absolute() {
729 let mut app = setup_ui_test_app();
730 let world = app.world_mut();
731
732 let mut size = 150.;
733
734 world.spawn(Node {
735 width: Val::Px(size),
738 height: Val::Px(size),
739 ..default()
740 });
741
742 size -= 50.;
743
744 world.spawn(Node {
745 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 app.update();
761 let world = app.world_mut();
762
763 let overlap_check = world
764 .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without<ChildOf>>()
765 .iter(world)
766 .fold(
767 Option::<(Rect, bool)>::None,
768 |option_rect, (entity, node, transform)| {
769 let current_rect = Rect::from_center_size(transform.translation, node.size());
770 assert!(
771 current_rect.height().abs() + current_rect.width().abs() > 0.,
772 "root ui node {entity} doesn't have a logical size"
773 );
774 assert_ne!(
775 *transform,
776 UiGlobalTransform::default(),
777 "root ui node {entity} transform is not populated"
778 );
779 let Some((rect, is_overlapping)) = option_rect else {
780 return Some((current_rect, false));
781 };
782 if rect.contains(current_rect.center()) {
783 Some((current_rect, true))
784 } else {
785 Some((current_rect, is_overlapping))
786 }
787 },
788 );
789
790 let Some((_rect, is_overlapping)) = overlap_check else {
791 unreachable!("test not setup properly");
792 };
793 assert!(is_overlapping, "root ui nodes are expected to behave like they have absolute position and be independent from each other");
794 }
795
796 #[test]
797 fn ui_node_should_properly_update_when_changing_target_camera() {
798 #[derive(Component)]
799 struct MovingUiNode;
800
801 fn update_camera_viewports(
802 primary_window_query: Query<&Window, With<PrimaryWindow>>,
803 mut cameras: Query<&mut Camera>,
804 ) {
805 let primary_window = primary_window_query
806 .single()
807 .expect("missing primary window");
808 let camera_count = cameras.iter().len();
809 for (camera_index, mut camera) in cameras.iter_mut().enumerate() {
810 let viewport_width =
811 primary_window.resolution.physical_width() / camera_count as u32;
812 let viewport_height = primary_window.resolution.physical_height();
813 let physical_position = UVec2::new(viewport_width * camera_index as u32, 0);
814 let physical_size = UVec2::new(viewport_width, viewport_height);
815 camera.viewport = Some(bevy_camera::Viewport {
816 physical_position,
817 physical_size,
818 ..default()
819 });
820 }
821 }
822
823 fn move_ui_node(
824 In(pos): In<Vec2>,
825 mut commands: Commands,
826 cameras: Query<(Entity, &Camera)>,
827 moving_ui_query: Query<Entity, With<MovingUiNode>>,
828 ) {
829 let (target_camera_entity, _) = cameras
830 .iter()
831 .find(|(_, camera)| {
832 let Some(logical_viewport_rect) = camera.logical_viewport_rect() else {
833 panic!("missing logical viewport")
834 };
835 logical_viewport_rect.contains(pos)
837 && logical_viewport_rect.max.cmpge(Vec2::splat(0.)).any()
838 })
839 .expect("cursor position outside of camera viewport");
840 for moving_ui_entity in moving_ui_query.iter() {
841 commands
842 .entity(moving_ui_entity)
843 .insert(UiTargetCamera(target_camera_entity))
844 .insert(Node {
845 position_type: PositionType::Absolute,
846 top: Val::Px(pos.y),
847 left: Val::Px(pos.x),
848 ..default()
849 });
850 }
851 }
852
853 fn do_move_and_test(app: &mut App, new_pos: Vec2, expected_camera_entity: &Entity) {
854 let world = app.world_mut();
855 world.run_system_once_with(move_ui_node, new_pos).unwrap();
856 app.update();
857 let world = app.world_mut();
858 let (ui_node_entity, UiTargetCamera(target_camera_entity)) = world
859 .query_filtered::<(Entity, &UiTargetCamera), With<MovingUiNode>>()
860 .single(world)
861 .expect("missing MovingUiNode");
862 assert_eq!(expected_camera_entity, target_camera_entity);
863 let mut ui_surface = world.resource_mut::<UiSurface>();
864
865 let layout = ui_surface
866 .get_layout(ui_node_entity, true)
867 .expect("failed to get layout")
868 .0;
869
870 assert_eq!(Vec2::new(layout.location.x, layout.location.y), new_pos);
872 }
873
874 fn get_taffy_node_count(world: &World) -> usize {
875 world.resource::<UiSurface>().taffy.total_node_count()
876 }
877
878 let mut app = setup_ui_test_app();
879 let world = app.world_mut();
880
881 world.spawn((
882 Camera2d,
883 Camera {
884 order: 1,
885 ..default()
886 },
887 ));
888
889 world.spawn((
890 Node {
891 position_type: PositionType::Absolute,
892 top: Val::Px(0.),
893 left: Val::Px(0.),
894 ..default()
895 },
896 MovingUiNode,
897 ));
898
899 app.update();
900 let world = app.world_mut();
901
902 let pos_inc = Vec2::splat(1.);
903 let total_cameras = world.query::<&Camera>().iter(world).len();
904 let expected_max_taffy_node_count = get_taffy_node_count(world) + total_cameras - 1;
906
907 world.run_system_once(update_camera_viewports).unwrap();
908
909 app.update();
910 let world = app.world_mut();
911
912 let viewport_rects = world
913 .query::<(Entity, &Camera)>()
914 .iter(world)
915 .map(|(e, c)| (e, c.logical_viewport_rect().expect("missing viewport")))
916 .collect::<Vec<_>>();
917
918 for (camera_entity, viewport) in viewport_rects.iter() {
919 let target_pos = viewport.min + pos_inc;
920 do_move_and_test(&mut app, target_pos, camera_entity);
921 }
922
923 let mut viewport_rects = viewport_rects.clone();
925 viewport_rects.reverse();
926 for (camera_entity, viewport) in viewport_rects.iter() {
927 let target_pos = viewport.max - pos_inc;
928 do_move_and_test(&mut app, target_pos, camera_entity);
929 }
930
931 let world = app.world();
932 let current_taffy_node_count = get_taffy_node_count(world);
933 if current_taffy_node_count > expected_max_taffy_node_count {
934 panic!("extra taffy nodes detected: current: {current_taffy_node_count} max expected: {expected_max_taffy_node_count}");
935 }
936 }
937
938 #[test]
939 fn ui_node_should_be_set_to_its_content_size() {
940 let mut app = setup_ui_test_app();
941 let world = app.world_mut();
942
943 let content_size = Vec2::new(50., 25.);
944
945 let ui_entity = world
946 .spawn((
947 Node {
948 align_self: AlignSelf::Start,
949 ..default()
950 },
951 ContentSize::fixed_size(content_size),
952 ))
953 .id();
954
955 app.update();
956 let world = app.world_mut();
957
958 let mut ui_surface = world.resource_mut::<UiSurface>();
959 let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
960
961 assert_eq!(layout.size.width, content_size.x);
963 assert_eq!(layout.size.height, content_size.y);
964 }
965
966 #[test]
967 fn measure_funcs_should_be_removed_on_content_size_removal() {
968 let mut app = setup_ui_test_app();
969 let world = app.world_mut();
970
971 let content_size = Vec2::new(50., 25.);
972 let ui_entity = world
973 .spawn((
974 Node {
975 align_self: AlignSelf::Start,
976 ..Default::default()
977 },
978 ContentSize::fixed_size(content_size),
979 ))
980 .id();
981
982 app.update();
983 let world = app.world_mut();
984
985 let mut ui_surface = world.resource_mut::<UiSurface>();
986 let ui_node = ui_surface.entity_to_taffy[&ui_entity];
987
988 assert!(ui_surface.taffy.get_node_context(ui_node.id).is_some());
990 let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
991 assert_eq!(layout.size.width, content_size.x);
992 assert_eq!(layout.size.height, content_size.y);
993
994 world.entity_mut(ui_entity).remove::<ContentSize>();
995
996 app.update();
997 let world = app.world_mut();
998
999 let mut ui_surface = world.resource_mut::<UiSurface>();
1000 assert!(ui_surface.taffy.get_node_context(ui_node.id).is_none());
1002
1003 let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
1005 assert_eq!(layout.size.width, 0.);
1006 assert_eq!(layout.size.height, 0.);
1007 }
1008
1009 #[test]
1010 fn ui_rounding_test() {
1011 let mut app = setup_ui_test_app();
1012 let world = app.world_mut();
1013
1014 let parent = world
1015 .spawn(Node {
1016 display: Display::Grid,
1017 grid_template_columns: RepeatedGridTrack::min_content(2),
1018 margin: UiRect::all(Val::Px(4.0)),
1019 ..default()
1020 })
1021 .with_children(|commands| {
1022 for _ in 0..2 {
1023 commands.spawn(Node {
1024 display: Display::Grid,
1025 width: Val::Px(160.),
1026 height: Val::Px(160.),
1027 ..default()
1028 });
1029 }
1030 })
1031 .id();
1032
1033 let children = world
1034 .entity(parent)
1035 .get::<Children>()
1036 .unwrap()
1037 .iter()
1038 .collect::<Vec<Entity>>();
1039
1040 for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) {
1041 let mut s = 1. - r;
1043 while s <= 5. {
1044 app.world_mut().resource_mut::<UiScale>().0 = s;
1045 app.update();
1046 let world = app.world_mut();
1047 let width_sum: f32 = children
1048 .iter()
1049 .map(|child| world.get::<ComputedNode>(*child).unwrap().size.x)
1050 .sum();
1051 let parent_width = world.get::<ComputedNode>(parent).unwrap().size.x;
1052 assert!((width_sum - parent_width).abs() < 0.001);
1053 assert!((width_sum - 320. * s).abs() <= 1.);
1054 s += r;
1055 }
1056 }
1057 }
1058
1059 #[test]
1060 fn no_camera_ui() {
1061 let mut app = App::new();
1062
1063 app.add_systems(
1064 PostUpdate,
1065 (propagate_ui_target_cameras, ApplyDeferred, ui_layout_system).chain(),
1066 );
1067
1068 app.add_plugins(HierarchyPropagatePlugin::<ComputedUiTargetCamera>::new(
1069 PostUpdate,
1070 ));
1071
1072 app.configure_sets(
1073 PostUpdate,
1074 PropagateSet::<ComputedUiTargetCamera>::default()
1075 .after(propagate_ui_target_cameras)
1076 .before(ui_layout_system),
1077 );
1078
1079 let world = app.world_mut();
1080 world.init_resource::<UiScale>();
1081 world.init_resource::<UiSurface>();
1082
1083 world.init_resource::<bevy_text::TextPipeline>();
1084
1085 world.init_resource::<bevy_text::CosmicFontSystem>();
1086
1087 world.init_resource::<bevy_text::SwashCache>();
1088
1089 let ui_root = world
1090 .spawn(Node {
1091 width: Val::Percent(100.),
1092 height: Val::Percent(100.),
1093 ..default()
1094 })
1095 .id();
1096
1097 let ui_child = world
1098 .spawn(Node {
1099 width: Val::Percent(100.),
1100 height: Val::Percent(100.),
1101 ..default()
1102 })
1103 .id();
1104
1105 world.entity_mut(ui_root).add_child(ui_child);
1106
1107 app.update();
1108 }
1109
1110 #[test]
1111 fn test_ui_surface_compute_camera_layout() {
1112 use bevy_ecs::prelude::ResMut;
1113
1114 let mut app = setup_ui_test_app();
1115 let world = app.world_mut();
1116
1117 let root_node_entity = Entity::from_raw_u32(1).unwrap();
1118
1119 struct TestSystemParam {
1120 root_node_entity: Entity,
1121 }
1122
1123 fn test_system(
1124 params: In<TestSystemParam>,
1125 mut ui_surface: ResMut<UiSurface>,
1126 mut computed_text_block_query: Query<&mut bevy_text::ComputedTextBlock>,
1127 mut font_system: ResMut<bevy_text::CosmicFontSystem>,
1128 ) {
1129 ui_surface.upsert_node(
1130 &LayoutContext::TEST_CONTEXT,
1131 params.root_node_entity,
1132 &Node::default(),
1133 None,
1134 );
1135
1136 ui_surface.compute_layout(
1137 params.root_node_entity,
1138 UVec2::new(800, 600),
1139 &mut computed_text_block_query,
1140 &mut font_system,
1141 );
1142 }
1143
1144 let _ = world.run_system_once_with(test_system, TestSystemParam { root_node_entity });
1145
1146 let ui_surface = world.resource::<UiSurface>();
1147
1148 let taffy_node = ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
1149 assert!(ui_surface.taffy.layout(taffy_node.id).is_ok());
1150 }
1151
1152 #[test]
1153 fn no_viewport_node_leak_on_root_despawned() {
1154 let mut app = setup_ui_test_app();
1155 let world = app.world_mut();
1156
1157 let ui_root_entity = world.spawn(Node::default()).id();
1158
1159 app.update();
1162 let world = app.world_mut();
1163
1164 assert_eq!(
1168 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1169 2
1170 );
1171
1172 world.despawn(ui_root_entity);
1173
1174 app.update();
1177 let world = app.world_mut();
1178
1179 assert_eq!(
1181 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1182 0
1183 );
1184 }
1185
1186 #[test]
1187 fn no_viewport_node_leak_on_parented_root() {
1188 let mut app = setup_ui_test_app();
1189 let world = app.world_mut();
1190
1191 let ui_root_entity_1 = world.spawn(Node::default()).id();
1192 let ui_root_entity_2 = world.spawn(Node::default()).id();
1193
1194 app.update();
1195 let world = app.world_mut();
1196
1197 assert_eq!(
1200 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1201 4
1202 );
1203
1204 world
1207 .entity_mut(ui_root_entity_1)
1208 .add_child(ui_root_entity_2);
1209
1210 app.update();
1213 let world = app.world_mut();
1214
1215 assert_eq!(
1217 world.resource_mut::<UiSurface>().taffy.total_node_count(),
1218 3
1219 );
1220 }
1221}