Skip to main content

freya_devtools_app/tabs/
tree.rs

1use std::collections::HashSet;
2
3use freya::prelude::*;
4use freya_core::integration::NodeId;
5use freya_devtools::NodeInfo;
6use freya_radio::prelude::{
7    Radio,
8    use_radio,
9};
10use freya_router::prelude::RouterContext;
11
12use crate::{
13    Route,
14    node::NodeElement,
15    state::{
16        DevtoolsChannel,
17        DevtoolsState,
18    },
19};
20
21#[derive(Clone, PartialEq)]
22struct NodeTreeItem {
23    is_open: Option<bool>,
24    window_id: u64,
25    node_id: NodeId,
26}
27
28#[derive(PartialEq)]
29pub struct NodesTree {
30    pub selected_node_id: Option<NodeId>,
31    pub selected_window_id: Option<u64>,
32    pub on_selected: EventHandler<(u64, NodeId)>,
33    pub on_hover: EventHandler<(u64, Option<NodeId>)>,
34}
35
36impl NodesTree {
37    /// Collect all descendant node IDs starting from a given node
38    fn collect_descendants(window_nodes: &[NodeInfo], node_id: NodeId) -> Vec<NodeId> {
39        let mut result = Vec::new();
40        let mut stack = vec![node_id];
41
42        while let Some(current_id) = stack.pop() {
43            result.push(current_id);
44
45            // Find children of current node and push to stack
46            for node in window_nodes.iter() {
47                if node.parent_id == Some(current_id) {
48                    stack.push(node.node_id);
49                }
50            }
51        }
52
53        result
54    }
55
56    /// Expands or collapses a node and all of its descendants.
57    fn set_subtree_expanded(
58        mut radio: Radio<DevtoolsState, DevtoolsChannel>,
59        window_id: u64,
60        node_id: NodeId,
61        expanded: bool,
62    ) {
63        let mut state = radio.write();
64        let Some(window_nodes) = state.nodes.get(&window_id) else {
65            return;
66        };
67        for descendant in Self::collect_descendants(window_nodes, node_id) {
68            if expanded {
69                state.expanded_nodes.insert((window_id, descendant));
70            } else {
71                state.expanded_nodes.remove(&(window_id, descendant));
72            }
73        }
74    }
75}
76
77impl Component for NodesTree {
78    fn render(&self) -> impl IntoElement {
79        let mut radio = use_radio(DevtoolsChannel::UpdatedTree);
80
81        let items = {
82            let radio = radio.read();
83            radio
84                .nodes
85                .iter()
86                .flat_map(|(window_id, nodes)| {
87                    let mut allowed_nodes = HashSet::new();
88                    nodes
89                        .iter()
90                        .filter_map(|node| {
91                            let parent_is_open = node
92                                .parent_id
93                                .map(|node_id| {
94                                    allowed_nodes.contains(&node_id)
95                                        && radio.expanded_nodes.contains(&(*window_id, node_id))
96                                })
97                                .unwrap_or(false);
98                            let is_top_height = node.height == 1;
99                            if parent_is_open || is_top_height {
100                                allowed_nodes.insert(node.node_id);
101                                let is_open = (node.children_len != 0).then_some(
102                                    radio.expanded_nodes.contains(&(*window_id, node.node_id)),
103                                );
104                                Some(NodeTreeItem {
105                                    is_open,
106                                    node_id: node.node_id,
107                                    window_id: *window_id,
108                                })
109                            } else {
110                                None
111                            }
112                        })
113                        .collect::<Vec<_>>()
114                })
115                .collect::<Vec<_>>()
116        };
117
118        if items.is_empty() {
119            return rect()
120                .center()
121                .expanded()
122                .child("Waiting for an app to connect...")
123                .into_element();
124        }
125
126        let items_len = items.len();
127
128        VirtualScrollView::new_with_data(
129            (
130                self.selected_node_id,
131                self.selected_window_id,
132                self.on_selected.clone(),
133                self.on_hover.clone(),
134            ),
135            move |i, (selected_node_id, selected_window_id, on_selected, on_hover)| {
136                let NodeTreeItem {
137                    window_id,
138                    node_id,
139                    is_open,
140                } = items[i];
141                let on_selected = on_selected.clone();
142                let on_hover = on_hover.clone();
143                NodeElement {
144                    is_selected: Some(node_id) == *selected_node_id
145                        && Some(window_id) == *selected_window_id,
146                    is_open,
147                    on_toggle: EventHandler::new(move |_| {
148                        let mut radio = radio.write();
149                        if radio.expanded_nodes.contains(&(window_id, node_id)) {
150                            radio.expanded_nodes.remove(&(window_id, node_id));
151                        } else {
152                            radio.expanded_nodes.insert((window_id, node_id));
153                        }
154                    }),
155                    on_expand_all: EventHandler::new(move |_| {
156                        NodesTree::set_subtree_expanded(radio, window_id, node_id, true);
157                    }),
158                    on_collapse_all: EventHandler::new(move |_| {
159                        NodesTree::set_subtree_expanded(radio, window_id, node_id, false);
160                    }),
161                    on_selected: EventHandler::new(move |_| {
162                        on_selected.call((window_id, node_id));
163                        let route = match RouterContext::get().current::<Route>() {
164                            Route::NodeInspectorTextStyle { .. } => {
165                                Route::NodeInspectorTextStyle { node_id, window_id }
166                            }
167                            Route::NodeInspectorLayout { .. } => {
168                                Route::NodeInspectorLayout { node_id, window_id }
169                            }
170                            _ => Route::NodeInspectorStyle { node_id, window_id },
171                        };
172                        let _ = RouterContext::get().push(route);
173                    }),
174                    on_hover: EventHandler::new(move |node_id| {
175                        on_hover.call((window_id, node_id));
176                    }),
177                    node_id,
178                    window_id,
179                }
180                .into()
181            },
182        )
183        .length(items_len)
184        .item_size(27.)
185        .into()
186    }
187}