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