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