Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/layout/ui_surface.rs
9367 views
1
use core::fmt;
2
use core::ops::{Deref, DerefMut};
3
4
use bevy_platform::collections::hash_map::Entry;
5
use taffy::TaffyTree;
6
7
use bevy_ecs::{
8
entity::{Entity, EntityHashMap},
9
prelude::Resource,
10
};
11
use bevy_math::{UVec2, Vec2};
12
use bevy_utils::default;
13
14
use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};
15
use bevy_text::CosmicFontSystem;
16
17
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18
pub struct LayoutNode {
19
// Implicit "viewport" node if this `LayoutNode` corresponds to a root UI node entity
20
pub(super) viewport_id: Option<taffy::NodeId>,
21
// The id of the node in the taffy tree
22
pub(super) id: taffy::NodeId,
23
}
24
25
impl From<taffy::NodeId> for LayoutNode {
26
fn from(value: taffy::NodeId) -> Self {
27
LayoutNode {
28
viewport_id: None,
29
id: value,
30
}
31
}
32
}
33
34
pub(crate) struct UiTree<T>(TaffyTree<T>);
35
36
#[expect(unsafe_code, reason = "TaffyTree is safe as long as calc is not used")]
37
// SAFETY: Taffy Tree becomes thread unsafe when you use the calc feature, which we do not implement
38
unsafe impl Send for UiTree<NodeMeasure> {}
39
40
#[expect(unsafe_code, reason = "TaffyTree is safe as long as calc is not used")]
41
// SAFETY: Taffy Tree becomes thread unsafe when you use the calc feature, which we do not implement
42
unsafe impl Sync for UiTree<NodeMeasure> {}
43
44
impl<T> Deref for UiTree<T> {
45
type Target = TaffyTree<T>;
46
fn deref(&self) -> &Self::Target {
47
&self.0
48
}
49
}
50
51
impl<T> DerefMut for UiTree<T> {
52
fn deref_mut(&mut self) -> &mut Self::Target {
53
&mut self.0
54
}
55
}
56
57
#[derive(Resource)]
58
pub struct UiSurface {
59
pub root_entity_to_viewport_node: EntityHashMap<taffy::NodeId>,
60
pub(super) entity_to_taffy: EntityHashMap<LayoutNode>,
61
pub(super) taffy: UiTree<NodeMeasure>,
62
taffy_children_scratch: Vec<taffy::NodeId>,
63
}
64
65
fn _assert_send_sync_ui_surface_impl_safe() {
66
fn _assert_send_sync<T: Send + Sync>() {}
67
_assert_send_sync::<EntityHashMap<taffy::NodeId>>();
68
_assert_send_sync::<UiTree<NodeMeasure>>();
69
_assert_send_sync::<UiSurface>();
70
}
71
72
impl fmt::Debug for UiSurface {
73
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74
f.debug_struct("UiSurface")
75
.field("entity_to_taffy", &self.entity_to_taffy)
76
.field("taffy_children_scratch", &self.taffy_children_scratch)
77
.finish()
78
}
79
}
80
81
impl Default for UiSurface {
82
fn default() -> Self {
83
let taffy: UiTree<NodeMeasure> = UiTree(TaffyTree::new());
84
Self {
85
root_entity_to_viewport_node: Default::default(),
86
entity_to_taffy: Default::default(),
87
taffy,
88
taffy_children_scratch: Vec::new(),
89
}
90
}
91
}
92
93
impl UiSurface {
94
/// Retrieves the Taffy node associated with the given UI node entity and updates its style.
95
/// If no associated Taffy node exists a new Taffy node is inserted into the Taffy layout.
96
pub fn upsert_node(
97
&mut self,
98
layout_context: &LayoutContext,
99
entity: Entity,
100
node: &Node,
101
mut new_node_context: Option<NodeMeasure>,
102
) {
103
let taffy = &mut self.taffy;
104
105
match self.entity_to_taffy.entry(entity) {
106
Entry::Occupied(entry) => {
107
let taffy_node = *entry.get();
108
let has_measure = if new_node_context.is_some() {
109
taffy
110
.set_node_context(taffy_node.id, new_node_context)
111
.unwrap();
112
true
113
} else {
114
taffy.get_node_context(taffy_node.id).is_some()
115
};
116
117
taffy
118
.set_style(
119
taffy_node.id,
120
convert::from_node(node, layout_context, has_measure),
121
)
122
.unwrap();
123
}
124
Entry::Vacant(entry) => {
125
let taffy_node = if let Some(measure) = new_node_context.take() {
126
taffy.new_leaf_with_context(
127
convert::from_node(node, layout_context, true),
128
measure,
129
)
130
} else {
131
taffy.new_leaf(convert::from_node(node, layout_context, false))
132
};
133
entry.insert(taffy_node.unwrap().into());
134
}
135
}
136
}
137
138
/// Update the `MeasureFunc` of the taffy node corresponding to the given [`Entity`] if the node exists.
139
pub fn update_node_context(&mut self, entity: Entity, context: NodeMeasure) -> Option<()> {
140
let taffy_node = self.entity_to_taffy.get(&entity)?;
141
self.taffy
142
.set_node_context(taffy_node.id, Some(context))
143
.ok()
144
}
145
146
/// Update the children of the taffy node corresponding to the given [`Entity`].
147
pub fn update_children(&mut self, entity: Entity, children: impl Iterator<Item = Entity>) {
148
self.taffy_children_scratch.clear();
149
150
for child in children {
151
if let Some(taffy_node) = self.entity_to_taffy.get_mut(&child) {
152
self.taffy_children_scratch.push(taffy_node.id);
153
if let Some(viewport_id) = taffy_node.viewport_id.take() {
154
self.taffy.remove(viewport_id).ok();
155
}
156
}
157
}
158
159
let taffy_node = self.entity_to_taffy.get(&entity).unwrap();
160
self.taffy
161
.set_children(taffy_node.id, &self.taffy_children_scratch)
162
.unwrap();
163
}
164
165
/// Removes children from the entity's taffy node if it exists. Does nothing otherwise.
166
pub fn try_remove_children(&mut self, entity: Entity) {
167
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
168
self.taffy.set_children(taffy_node.id, &[]).unwrap();
169
}
170
}
171
172
/// Removes the measure from the entity's taffy node if it exists. Does nothing otherwise.
173
pub fn try_remove_node_context(&mut self, entity: Entity) {
174
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
175
self.taffy.set_node_context(taffy_node.id, None).unwrap();
176
}
177
}
178
179
/// Gets or inserts an implicit taffy viewport node corresponding to the given UI root entity
180
pub fn get_or_insert_taffy_viewport_node(&mut self, ui_root_entity: Entity) -> taffy::NodeId {
181
*self
182
.root_entity_to_viewport_node
183
.entry(ui_root_entity)
184
.or_insert_with(|| {
185
let root_node = self.entity_to_taffy.get_mut(&ui_root_entity).unwrap();
186
let implicit_root = self
187
.taffy
188
.new_leaf(taffy::style::Style {
189
display: taffy::style::Display::Grid,
190
// Note: Taffy percentages are floats ranging from 0.0 to 1.0.
191
// So this is setting width:100% and height:100%
192
size: taffy::geometry::Size {
193
width: taffy::style_helpers::percent(1.0),
194
height: taffy::style_helpers::percent(1.0),
195
},
196
align_items: Some(taffy::style::AlignItems::Start),
197
justify_items: Some(taffy::style::JustifyItems::Start),
198
..default()
199
})
200
.unwrap();
201
self.taffy.add_child(implicit_root, root_node.id).unwrap();
202
root_node.viewport_id = Some(implicit_root);
203
implicit_root
204
})
205
}
206
207
/// Compute the layout for the given implicit taffy viewport node
208
pub fn compute_layout<'a>(
209
&mut self,
210
ui_root_entity: Entity,
211
render_target_resolution: UVec2,
212
buffer_query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
213
font_system: &'a mut CosmicFontSystem,
214
) {
215
let implicit_viewport_node = self.get_or_insert_taffy_viewport_node(ui_root_entity);
216
217
let available_space = taffy::geometry::Size {
218
width: taffy::style::AvailableSpace::Definite(render_target_resolution.x as f32),
219
height: taffy::style::AvailableSpace::Definite(render_target_resolution.y as f32),
220
};
221
222
self.taffy
223
.compute_layout_with_measure(
224
implicit_viewport_node,
225
available_space,
226
|known_dimensions: taffy::Size<Option<f32>>,
227
available_space: taffy::Size<taffy::AvailableSpace>,
228
_node_id: taffy::NodeId,
229
context: Option<&mut NodeMeasure>,
230
style: &taffy::Style|
231
-> taffy::Size<f32> {
232
context
233
.map(|ctx| {
234
let buffer = get_text_buffer(
235
crate::widget::TextMeasure::needs_buffer(
236
known_dimensions.height,
237
available_space.width,
238
),
239
ctx,
240
buffer_query,
241
);
242
let size = ctx.measure(
243
MeasureArgs {
244
width: known_dimensions.width,
245
height: known_dimensions.height,
246
available_width: available_space.width,
247
available_height: available_space.height,
248
font_system,
249
buffer,
250
},
251
style,
252
);
253
taffy::Size {
254
width: size.x,
255
height: size.y,
256
}
257
})
258
.unwrap_or(taffy::Size::ZERO)
259
},
260
)
261
.unwrap();
262
}
263
264
/// Removes each entity from the internal map and then removes their associated nodes from taffy
265
pub fn remove_entities(&mut self, entities: impl IntoIterator<Item = Entity>) {
266
for entity in entities {
267
if let Some(node) = self.entity_to_taffy.remove(&entity) {
268
self.taffy.remove(node.id).unwrap();
269
if let Some(viewport_node) = node.viewport_id {
270
self.taffy.remove(viewport_node).ok();
271
}
272
}
273
}
274
}
275
276
/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
277
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
278
/// On success returns a pair consisting of the final resolved layout values after rounding
279
/// and the size of the node after layout resolution but before rounding.
280
pub fn get_layout(
281
&mut self,
282
entity: Entity,
283
use_rounding: bool,
284
) -> Result<(taffy::Layout, Vec2), LayoutError> {
285
let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
286
return Err(LayoutError::InvalidHierarchy);
287
};
288
289
if use_rounding {
290
self.taffy.enable_rounding();
291
} else {
292
self.taffy.disable_rounding();
293
}
294
295
let out = match self.taffy.layout(taffy_node.id).cloned() {
296
Ok(layout) => {
297
self.taffy.disable_rounding();
298
let taffy_size = self.taffy.layout(taffy_node.id).unwrap().size;
299
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
300
Ok((layout, unrounded_size))
301
}
302
Err(taffy_error) => Err(LayoutError::TaffyError(taffy_error)),
303
};
304
305
self.taffy.enable_rounding();
306
out
307
}
308
}
309
310
pub fn get_text_buffer<'a>(
311
needs_buffer: bool,
312
ctx: &mut NodeMeasure,
313
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::ComputedTextBlock>,
314
) -> Option<&'a mut bevy_text::ComputedTextBlock> {
315
// We avoid a query lookup whenever the buffer is not required.
316
if !needs_buffer {
317
return None;
318
}
319
let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {
320
return None;
321
};
322
let Ok(computed) = query.get_mut(info.entity) else {
323
return None;
324
};
325
Some(computed.into_inner())
326
}
327
328
#[cfg(test)]
329
mod tests {
330
use super::*;
331
use crate::{ContentSize, FixedMeasure};
332
use bevy_math::Vec2;
333
use taffy::TraversePartialTree;
334
335
#[test]
336
fn test_initialization() {
337
let ui_surface = UiSurface::default();
338
assert!(ui_surface.entity_to_taffy.is_empty());
339
assert_eq!(ui_surface.taffy.total_node_count(), 0);
340
}
341
342
#[test]
343
fn test_upsert() {
344
let mut ui_surface = UiSurface::default();
345
let root_node_entity = Entity::from_raw_u32(1).unwrap();
346
let node = Node::default();
347
348
// standard upsert
349
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
350
351
// should be inserted into taffy
352
assert_eq!(ui_surface.taffy.total_node_count(), 1);
353
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
354
355
// test duplicate insert 1
356
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
357
358
// node count should not have increased
359
assert_eq!(ui_surface.taffy.total_node_count(), 1);
360
361
// assign root node to camera
362
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
363
364
// each root node will create 2 taffy nodes
365
assert_eq!(ui_surface.taffy.total_node_count(), 2);
366
367
// test duplicate insert 2
368
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
369
370
// node count should not have increased
371
assert_eq!(ui_surface.taffy.total_node_count(), 2);
372
}
373
374
#[test]
375
fn test_remove_entities() {
376
let mut ui_surface = UiSurface::default();
377
let root_node_entity = Entity::from_raw_u32(1).unwrap();
378
let node = Node::default();
379
380
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
381
382
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
383
384
assert!(ui_surface.entity_to_taffy.contains_key(&root_node_entity));
385
386
ui_surface.remove_entities([root_node_entity]);
387
assert!(!ui_surface.entity_to_taffy.contains_key(&root_node_entity));
388
}
389
390
#[test]
391
fn test_try_update_measure() {
392
let mut ui_surface = UiSurface::default();
393
let root_node_entity = Entity::from_raw_u32(1).unwrap();
394
let node = Node::default();
395
396
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
397
let mut content_size = ContentSize::default();
398
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: Vec2::ONE }));
399
let measure_func = content_size.measure.take().unwrap();
400
assert!(ui_surface
401
.update_node_context(root_node_entity, measure_func)
402
.is_some());
403
}
404
405
#[test]
406
fn test_update_children() {
407
let mut ui_surface = UiSurface::default();
408
let root_node_entity = Entity::from_raw_u32(1).unwrap();
409
let child_entity = Entity::from_raw_u32(2).unwrap();
410
let node = Node::default();
411
412
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
413
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);
414
415
ui_surface.update_children(root_node_entity, vec![child_entity].into_iter());
416
417
let parent_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
418
let child_node = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
419
assert_eq!(ui_surface.taffy.parent(child_node.id), Some(parent_node.id));
420
}
421
422
#[expect(
423
unreachable_code,
424
reason = "Certain pieces of code tested here cause the test to fail if made reachable; see #16362 for progress on fixing this"
425
)]
426
#[test]
427
fn test_set_camera_children() {
428
let mut ui_surface = UiSurface::default();
429
let root_node_entity = Entity::from_raw_u32(1).unwrap();
430
let child_entity = Entity::from_raw_u32(2).unwrap();
431
let node = Node::default();
432
433
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, root_node_entity, &node, None);
434
ui_surface.upsert_node(&LayoutContext::TEST_CONTEXT, child_entity, &node, None);
435
436
let root_taffy_node = *ui_surface.entity_to_taffy.get(&root_node_entity).unwrap();
437
let child_taffy = *ui_surface.entity_to_taffy.get(&child_entity).unwrap();
438
439
// set up the relationship manually
440
ui_surface
441
.taffy
442
.add_child(root_taffy_node.id, child_taffy.id)
443
.unwrap();
444
445
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
446
447
assert_eq!(
448
ui_surface.taffy.parent(child_taffy.id),
449
Some(root_taffy_node.id)
450
);
451
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
452
assert!(
453
root_taffy_children.contains(&child_taffy.id),
454
"root node is not a parent of child node"
455
);
456
assert_eq!(
457
ui_surface.taffy.child_count(root_taffy_node.id),
458
1,
459
"expected root node child count to be 1"
460
);
461
462
// clear camera's root nodes
463
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
464
465
return; // TODO: can't pass the test if we continue - not implemented (remove allow(unreachable_code))
466
467
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
468
assert!(
469
root_taffy_children.contains(&child_taffy.id),
470
"root node is not a parent of child node"
471
);
472
assert_eq!(
473
ui_surface.taffy.child_count(root_taffy_node.id),
474
1,
475
"expected root node child count to be 1"
476
);
477
478
// re-associate root node with viewport node
479
ui_surface.get_or_insert_taffy_viewport_node(root_node_entity);
480
481
let child_taffy = ui_surface.entity_to_taffy.get(&child_entity).unwrap();
482
let root_taffy_children = ui_surface.taffy.children(root_taffy_node.id).unwrap();
483
assert!(
484
root_taffy_children.contains(&child_taffy.id),
485
"root node is not a parent of child node"
486
);
487
assert_eq!(
488
ui_surface.taffy.child_count(root_taffy_node.id),
489
1,
490
"expected root node child count to be 1"
491
);
492
}
493
}
494
495