tessera_ui/
component_tree.rs

1mod constraint;
2mod node;
3
4use std::{any::TypeId, num::NonZero, sync::Arc, time::Instant};
5
6use parking_lot::RwLock;
7use rayon::prelude::*;
8use tracing::{debug, warn};
9
10use crate::{
11    Clipboard, ComputeResourceManager, Px, PxRect,
12    component_tree::node::measure_node,
13    cursor::CursorEvent,
14    px::{PxPosition, PxSize},
15    renderer::Command,
16};
17
18pub use constraint::{Constraint, DimensionValue};
19pub use node::{
20    ComponentNode, ComponentNodeMetaData, ComponentNodeMetaDatas, ComponentNodeTree, ComputedData,
21    ImeRequest, InputHandlerFn, InputHandlerInput, MeasureFn, MeasureInput, MeasurementError,
22    WindowRequests,
23};
24
25#[derive(Debug, Clone, Copy)]
26struct ScreenSize {
27    width: i32,
28    height: i32,
29}
30
31/// Parameters for the compute function
32pub struct ComputeParams<'a> {
33    pub screen_size: PxSize,
34    pub cursor_position: Option<PxPosition>,
35    pub cursor_events: Vec<CursorEvent>,
36    pub keyboard_events: Vec<winit::event::KeyEvent>,
37    pub ime_events: Vec<winit::event::Ime>,
38    pub modifiers: winit::keyboard::ModifiersState,
39    pub compute_resource_manager: Arc<RwLock<ComputeResourceManager>>,
40    pub gpu: &'a wgpu::Device,
41    pub clipboard: &'a mut Clipboard,
42}
43
44/// Respents a component tree
45pub struct ComponentTree {
46    /// We use indextree as the tree structure
47    tree: indextree::Arena<ComponentNode>,
48    /// Components' metadatas
49    metadatas: ComponentNodeMetaDatas,
50    /// Used to remember the current node
51    node_queue: Vec<indextree::NodeId>,
52}
53
54impl Default for ComponentTree {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl ComponentTree {
61    /// Create a new ComponentTree
62    pub fn new() -> Self {
63        let tree = indextree::Arena::new();
64        let node_queue = Vec::new();
65        let metadatas = ComponentNodeMetaDatas::new();
66        Self {
67            tree,
68            node_queue,
69            metadatas,
70        }
71    }
72
73    /// Clear the component tree
74    pub fn clear(&mut self) {
75        self.tree.clear();
76        self.metadatas.clear();
77        self.node_queue.clear();
78    }
79
80    /// Get node by NodeId
81    pub fn get(&self, node_id: indextree::NodeId) -> Option<&ComponentNode> {
82        self.tree.get(node_id).map(|n| n.get())
83    }
84
85    /// Get mutable node by NodeId
86    pub fn get_mut(&mut self, node_id: indextree::NodeId) -> Option<&mut ComponentNode> {
87        self.tree.get_mut(node_id).map(|n| n.get_mut())
88    }
89
90    /// Get current node
91    pub fn current_node(&self) -> Option<&ComponentNode> {
92        self.node_queue
93            .last()
94            .and_then(|node_id| self.get(*node_id))
95    }
96
97    /// Get mutable current node
98    pub fn current_node_mut(&mut self) -> Option<&mut ComponentNode> {
99        let node_id = self.node_queue.last()?;
100        self.get_mut(*node_id)
101    }
102
103    /// Add a new node to the tree
104    /// Nodes now store their intrinsic constraints in their metadata.
105    /// The `node_component` itself primarily holds the measure_fn.
106    pub fn add_node(&mut self, node_component: ComponentNode) {
107        let new_node_id = self.tree.new_node(node_component);
108        if let Some(current_node_id) = self.node_queue.last_mut() {
109            current_node_id.append(new_node_id, &mut self.tree);
110        }
111        let metadata = ComponentNodeMetaData::none();
112        self.metadatas.insert(new_node_id, metadata);
113        self.node_queue.push(new_node_id);
114    }
115
116    /// Pop the last node from the queue
117    pub fn pop_node(&mut self) {
118        self.node_queue.pop();
119    }
120
121    /// Get a reference to the underlying tree structure.
122    ///
123    /// This is used for accessibility tree building and other introspection needs.
124    pub(crate) fn tree(&self) -> &indextree::Arena<ComponentNode> {
125        &self.tree
126    }
127
128    /// Get a reference to the node metadatas.
129    ///
130    /// This is used for accessibility tree building and other introspection needs.
131    pub(crate) fn metadatas(&self) -> &ComponentNodeMetaDatas {
132        &self.metadatas
133    }
134
135    /// Compute the ComponentTree into a list of rendering commands
136    ///
137    /// This method processes the component tree through three main phases:
138    /// 1. **Measure Phase**: Calculate sizes and positions for all components
139    /// 2. **Command Generation**: Extract draw commands from component metadata
140    /// 3. **State Handling**: Process user interactions and events
141    ///
142    /// Returns a tuple of (commands, window_requests) where commands contain
143    /// the rendering instructions with their associated sizes and positions.
144    #[tracing::instrument(level = "debug", skip(self, params))]
145    pub fn compute(
146        &mut self,
147        params: ComputeParams<'_>,
148    ) -> (Vec<(Command, TypeId, PxSize, PxPosition)>, WindowRequests) {
149        let ComputeParams {
150            screen_size,
151            mut cursor_position,
152            mut cursor_events,
153            mut keyboard_events,
154            mut ime_events,
155            modifiers,
156            compute_resource_manager,
157            gpu,
158            clipboard,
159        } = params;
160        let Some(root_node) = self
161            .tree
162            .get_node_id_at(NonZero::new(1).expect("root node index must be non-zero"))
163        else {
164            return (vec![], WindowRequests::default());
165        };
166        let screen_constraint = Constraint::new(
167            DimensionValue::Fixed(screen_size.width),
168            DimensionValue::Fixed(screen_size.height),
169        );
170
171        let measure_timer = Instant::now();
172        debug!("Start measuring the component tree...");
173
174        // Call measure_node with &self.tree and &self.metadatas
175        // Handle the Result from measure_node
176        match measure_node(
177            root_node,
178            &screen_constraint,
179            &self.tree,
180            &self.metadatas,
181            compute_resource_manager,
182            gpu,
183        ) {
184            Ok(_root_computed_data) => {
185                debug!("Component tree measured in {:?}", measure_timer.elapsed());
186            }
187            Err(e) => {
188                panic!(
189                    "Root node ({root_node:?}) measurement failed: {e:?}. Aborting draw command computation."
190                );
191            }
192        }
193
194        let compute_draw_timer = Instant::now();
195        debug!("Start computing draw commands...");
196        // compute_draw_commands_parallel expects &ComponentNodeTree and &ComponentNodeMetaDatas
197        // It also uses get_mut on metadatas internally, which is fine for DashMap with &self.
198        let commands = compute_draw_commands_parallel(
199            root_node,
200            &self.tree,
201            &self.metadatas,
202            screen_size.width.0,
203            screen_size.height.0,
204        );
205        debug!(
206            "Draw commands computed in {:?}, total commands: {}",
207            compute_draw_timer.elapsed(),
208            commands.len()
209        );
210
211        let input_handler_timer = Instant::now();
212        let mut window_requests = WindowRequests::default();
213        debug!("Start executing input handlers...");
214
215        for node_id in root_node
216            .reverse_traverse(&self.tree)
217            .filter_map(|edge| match edge {
218                indextree::NodeEdge::Start(id) => Some(id),
219                indextree::NodeEdge::End(_) => None,
220            })
221        {
222            let Some(input_handler) = self
223                .tree
224                .get(node_id)
225                .and_then(|n| n.get().input_handler_fn.as_ref())
226            else {
227                continue;
228            };
229
230            let Some(metadata) = self.metadatas.get(&node_id) else {
231                warn!(
232                    "Input handler metadata missing for node {node_id:?}; skipping input handling"
233                );
234                continue;
235            };
236            let Some(abs_pos) = metadata.abs_position else {
237                warn!("Absolute position missing for node {node_id:?}; skipping input handling");
238                continue;
239            };
240            let event_clip_rect = metadata.event_clip_rect;
241            let node_computed_data = metadata.computed_data;
242            drop(metadata); // release DashMap guard so handlers can mutate metadata if needed
243
244            let mut cursor_position_ref = &mut cursor_position;
245            let mut dummy_cursor_position = None;
246            let mut cursor_events_ref = &mut cursor_events;
247            let mut empty_dummy_cursor_events = Vec::new();
248            if let (Some(cursor_pos), Some(clip_rect)) = (*cursor_position_ref, event_clip_rect) {
249                // check if the cursor is inside the clip rect
250                if !clip_rect.contains(cursor_pos) {
251                    // If not, set cursor relative inputs to None
252                    cursor_position_ref = &mut dummy_cursor_position;
253                    cursor_events_ref = &mut empty_dummy_cursor_events;
254                }
255            }
256            let current_cursor_position = cursor_position_ref.map(|pos| pos - abs_pos);
257
258            if let Some(node_computed_data) = node_computed_data {
259                let input = InputHandlerInput {
260                    computed_data: node_computed_data,
261                    cursor_position_rel: current_cursor_position,
262                    cursor_position_abs: cursor_position_ref,
263                    cursor_events: cursor_events_ref,
264                    keyboard_events: &mut keyboard_events,
265                    ime_events: &mut ime_events,
266                    key_modifiers: modifiers,
267                    requests: &mut window_requests,
268                    clipboard,
269                    current_node_id: node_id,
270                    metadatas: &self.metadatas,
271                };
272                input_handler(input);
273                // if input_handler set ime request, it's position must be None, and we set it here
274                if let Some(ref mut ime_request) = window_requests.ime_request
275                    && ime_request.position.is_none()
276                {
277                    ime_request.position = Some(abs_pos);
278                }
279            } else {
280                warn!(
281                    "Computed data not found for node {:?} during input handler execution.",
282                    node_id
283                );
284            }
285        }
286
287        debug!(
288            "Input Handlers executed in {:?}",
289            input_handler_timer.elapsed()
290        );
291        (commands, window_requests)
292    }
293}
294
295/// Parallel computation of draw commands from the component tree
296///
297/// This function traverses the component tree and extracts rendering commands
298/// from each node's metadata. It uses parallel processing for better performance
299/// when dealing with large component trees.
300///
301/// The function maintains thread-safety by using DashMap's concurrent access
302/// capabilities, allowing multiple threads to safely read and modify metadata.
303#[tracing::instrument(level = "trace", skip(tree, metadatas))]
304fn compute_draw_commands_parallel(
305    node_id: indextree::NodeId,
306    tree: &ComponentNodeTree,
307    metadatas: &ComponentNodeMetaDatas,
308    screen_width: i32,
309    screen_height: i32,
310) -> Vec<(Command, TypeId, PxSize, PxPosition)> {
311    compute_draw_commands_inner_parallel(
312        PxPosition::ZERO,
313        true,
314        node_id,
315        tree,
316        metadatas,
317        ScreenSize {
318            width: screen_width,
319            height: screen_height,
320        },
321        None,
322    )
323}
324
325#[tracing::instrument(level = "trace", skip(tree, metadatas))]
326fn compute_draw_commands_inner_parallel(
327    start_pos: PxPosition,
328    is_root: bool,
329    node_id: indextree::NodeId,
330    tree: &ComponentNodeTree,
331    metadatas: &ComponentNodeMetaDatas,
332    screen_size: ScreenSize,
333    clip_rect: Option<PxRect>,
334) -> Vec<(Command, TypeId, PxSize, PxPosition)> {
335    let mut local_commands = Vec::new();
336
337    // Get metadata and calculate absolute position. This MUST happen for all nodes.
338    let Some(mut metadata) = metadatas.get_mut(&node_id) else {
339        warn!("Missing metadata for node {node_id:?}; skipping draw computation");
340        return local_commands;
341    };
342    let rel_pos = match metadata.rel_position {
343        Some(pos) => pos,
344        None if is_root => PxPosition::ZERO,
345        _ => return local_commands, // Skip nodes that were not placed at all.
346    };
347    let self_pos = start_pos + rel_pos;
348    metadata.abs_position = Some(self_pos);
349
350    let size = metadata
351        .computed_data
352        .map(|d| PxSize {
353            width: d.width,
354            height: d.height,
355        })
356        .unwrap_or_default();
357
358    let node_rect = PxRect {
359        x: self_pos.x,
360        y: self_pos.y,
361        width: size.width,
362        height: size.height,
363    };
364
365    let mut clip_rect = clip_rect;
366    if let Some(clip_rect) = clip_rect {
367        metadata.event_clip_rect = Some(clip_rect);
368    }
369
370    let clips_children = metadata.clips_children;
371    // Add Clip command if the node clips its children
372    if clips_children {
373        let new_clip_rect = if let Some(existing_clip) = clip_rect {
374            existing_clip
375                .intersection(&node_rect)
376                .unwrap_or(PxRect::ZERO)
377        } else {
378            node_rect
379        };
380
381        clip_rect = Some(new_clip_rect);
382
383        local_commands.push((
384            Command::ClipPush(new_clip_rect),
385            TypeId::of::<Command>(),
386            size,
387            self_pos,
388        ));
389    }
390
391    // Viewport culling check
392    let screen_rect = PxRect {
393        x: Px(0),
394        y: Px(0),
395        width: Px(screen_size.width),
396        height: Px(screen_size.height),
397    };
398
399    // Only drain commands if the node is visible.
400    if size.width.0 > 0 && size.height.0 > 0 && !node_rect.is_orthogonal(&screen_rect) {
401        for (cmd, type_id) in metadata.commands.drain(..) {
402            local_commands.push((cmd, type_id, size, self_pos));
403        }
404    }
405
406    drop(metadata); // Release lock before recursing
407
408    // ALWAYS recurse to children to ensure their abs_position is calculated.
409    let children: Vec<_> = node_id.children(tree).collect();
410    let child_results: Vec<Vec<_>> = children
411        .into_par_iter()
412        .filter_map(|child| {
413            // Grab the parent's absolute position without holding the DashMap guard across recursion.
414            let parent_abs_pos = {
415                let Some(parent_meta) = metadatas.get(&node_id) else {
416                    warn!(
417                        "Missing parent metadata for node {node_id:?}; skipping child {child:?}"
418                    );
419                    return None;
420                };
421                let Some(pos) = parent_meta.abs_position else {
422                    warn!(
423                        "Missing parent absolute position for node {node_id:?}; skipping child {child:?}"
424                    );
425                    return None;
426                };
427                pos
428            };
429
430            Some(compute_draw_commands_inner_parallel(
431                parent_abs_pos, // Pass the calculated absolute position
432                false,
433                child,
434                tree,
435                metadatas,
436                screen_size,
437                clip_rect,
438            ))
439        })
440        .collect();
441
442    for child_cmds in child_results {
443        local_commands.extend(child_cmds);
444    }
445
446    // If the node clips its children, we need to pop the clip command
447    if clips_children {
448        local_commands.push((Command::ClipPop, TypeId::of::<Command>(), size, self_pos));
449    }
450
451    local_commands
452}