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