bevy_ui/layout/
ui_surface.rs

1use core::fmt;
2use core::ops::{Deref, DerefMut};
3
4use bevy_platform::collections::hash_map::Entry;
5use taffy::TaffyTree;
6
7use bevy_ecs::{
8    entity::{Entity, EntityHashMap},
9    prelude::Resource,
10};
11use bevy_math::{UVec2, Vec2};
12use bevy_utils::default;
13
14use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};
15use bevy_text::CosmicFontSystem;
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18pub 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
25impl 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
34pub(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
38unsafe 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
42unsafe impl Sync for UiTree<NodeMeasure> {}
43
44impl<T> Deref for UiTree<T> {
45    type Target = TaffyTree<T>;
46    fn deref(&self) -> &Self::Target {
47        &self.0
48    }
49}
50
51impl<T> DerefMut for UiTree<T> {
52    fn deref_mut(&mut self) -> &mut Self::Target {
53        &mut self.0
54    }
55}
56
57#[derive(Resource)]
58pub 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
65fn _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
72impl 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
81impl 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
93impl 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
310pub 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)]
329mod 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}