Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/layout/mod.rs
6599 views
1
use crate::{
2
experimental::{UiChildren, UiRootNodes},
3
ui_transform::{UiGlobalTransform, UiTransform},
4
BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Display, 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(crate) 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 new a [`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::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<&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
// 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<&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
// 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
// The position of the center of the node relative to its top-left corner.
235
let local_center =
236
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
237
238
// only trigger change detection when the new values are different
239
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
// Compute the node's new global transform
262
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
// We don't trigger change detection for changes to border radius
276
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
// don't trigger change detection when only outlines are changed
285
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)]
358
mod 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
// these window dimensions are easy to convert to and from percentage values
376
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
// spawn a dummy primary window and camera
424
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
// spawn a root entity with width and height set to fill 100% of its parent
443
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
// no UI entities in world, none in UiSurface
478
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
// `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
509
app.update();
510
let world = app.world_mut();
511
512
// retrieve the ui node corresponding to `ui_entity` from ui surface
513
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
// `ui_layout_system` will receive a `RemovedComponents<Node>` event for `ui_entity`
519
// and remove `ui_entity` from `ui_node` from the internal layout tree
520
app.update();
521
let world = app.world_mut();
522
523
let ui_surface = world.resource::<UiSurface>();
524
525
// `ui_node` is removed, attempting to retrieve a style for `ui_node` panics
526
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
// `ui_layout_system` will insert a ui node into the internal layout tree corresponding to `ui_entity`
537
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
// `ui_parent_node` shouldn't have any children yet
544
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
// `ui_parent_node` should have children now
558
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
// the children should have a corresponding ui node and that ui node's parent should be `ui_parent_node`
575
for node in child_node_map.values() {
576
assert_eq!(ui_surface.taffy.parent(node.id), Some(ui_parent_node.id));
577
}
578
579
// delete every second child
580
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
// the remaining children should still have nodes in the layout tree
601
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
// the nodes of the deleted children should have been removed from the layout tree
616
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
// despawn the parent entity and its descendants
629
world.entity_mut(ui_parent_entity).despawn();
630
631
app.update();
632
let world = app.world_mut();
633
634
// all nodes should have been deleted
635
let ui_surface = world.resource::<UiSurface>();
636
assert!(ui_surface.entity_to_taffy.is_empty());
637
}
638
639
/// bugfix test, see [#16288](https://github.com/bevyengine/bevy/pull/16288)
640
#[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
// no UI entities in world, none in UiSurface
648
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
// `ui_layout_system` should map `ui_entity` to a ui node in `UiSurface::entity_to_taffy`
654
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
// remove and re-insert Node to trigger removal code in `ui_layout_system`
662
world.entity_mut(ui_entity).remove::<Node>();
663
world.entity_mut(ui_entity).insert(Node::default());
664
665
// `ui_layout_system` should still have `ui_entity`
666
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
// spawn an invalid UI root node
680
let root_node = world.spawn(()).with_child(Node::default()).id();
681
682
app.update();
683
let world = app.world_mut();
684
685
// fix the invalid root node by inserting a Node
686
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
// There should be one child of the root node after fixing it
695
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
// fix the invalid middle node by inserting a Node
712
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
/// regression test for >=0.13.1 root node layouts
725
/// ensure root nodes act like they are absolutely positioned
726
/// without explicitly declaring it.
727
#[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
// test should pass without explicitly requiring position_type to be set to Absolute
736
// position_type: PositionType::Absolute,
737
width: Val::Px(size),
738
height: Val::Px(size),
739
..default()
740
});
741
742
size -= 50.;
743
744
world.spawn(Node {
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
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
// make sure cursor is in viewport and that viewport has at least 1px of size
836
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
// negative test for #12255
871
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
// add total cameras - 1 (the assumed default) to get an idea for how many nodes we should expect
905
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
// reverse direction
924
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
// the node should takes its size from the fixed size measure func
962
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
// a node with a content size should have taffy context
989
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
// a node without a content size should not have taffy context
1001
assert!(ui_surface.taffy.get_node_context(ui_node.id).is_none());
1002
1003
// Without a content size, the node has no width or height constraints so the length of both dimensions is 0.
1004
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
// This fails with very small / unrealistic scale values
1042
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
// The UI schedule synchronizes Bevy UI's internal `TaffyTree` with the
1160
// main world's tree of `Node` entities.
1161
app.update();
1162
let world = app.world_mut();
1163
1164
// Two taffy nodes are added to the internal `TaffyTree` for each root UI entity.
1165
// An implicit taffy node representing the viewport and a taffy node corresponding to the
1166
// root UI entity which is parented to the viewport taffy node.
1167
assert_eq!(
1168
world.resource_mut::<UiSurface>().taffy.total_node_count(),
1169
2
1170
);
1171
1172
world.despawn(ui_root_entity);
1173
1174
// The UI schedule removes both the taffy node corresponding to `ui_root_entity` and its
1175
// parent viewport node.
1176
app.update();
1177
let world = app.world_mut();
1178
1179
// Both taffy nodes should now be removed from the internal `TaffyTree`
1180
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
// There are two UI root entities. Each root taffy node is given it's own viewport node parent,
1198
// so a total of four taffy nodes are added to the `TaffyTree` by the UI schedule.
1199
assert_eq!(
1200
world.resource_mut::<UiSurface>().taffy.total_node_count(),
1201
4
1202
);
1203
1204
// Parent `ui_root_entity_2` onto `ui_root_entity_1` so now only `ui_root_entity_1` is a
1205
// UI root entity.
1206
world
1207
.entity_mut(ui_root_entity_1)
1208
.add_child(ui_root_entity_2);
1209
1210
// Now there is only one root node so the second viewport node is removed by
1211
// the UI schedule.
1212
app.update();
1213
let world = app.world_mut();
1214
1215
// There is only one viewport node now, so the `TaffyTree` contains 3 nodes in total.
1216
assert_eq!(
1217
world.resource_mut::<UiSurface>().taffy.total_node_count(),
1218
3
1219
);
1220
}
1221
}
1222
1223