freya_devtools/
lib.rs

1use std::collections::HashSet;
2
3use dioxus::prelude::*;
4use dioxus_radio::prelude::*;
5use dioxus_router::{
6    hooks::use_navigator,
7    prelude::{
8        use_route,
9        Outlet,
10        Routable,
11        Router,
12    },
13};
14use freya_components::*;
15use freya_core::event_loop_messages::EventLoopMessage;
16use freya_elements as dioxus_elements;
17use freya_hooks::{
18    use_applied_theme,
19    use_init_theme,
20    use_platform,
21    DARK_THEME,
22};
23use freya_native_core::NodeId;
24use freya_winit::devtools::{
25    DevtoolsReceiver,
26    HoveredNode,
27};
28use state::{
29    DevtoolsChannel,
30    DevtoolsState,
31};
32
33mod hooks;
34mod node;
35mod property;
36mod state;
37mod tabs;
38
39use tabs::{
40    layout::*,
41    style::*,
42    tree::*,
43};
44
45/// Run the [`VirtualDom`] with a sidepanel where the devtools are located.
46pub fn with_devtools(
47    root: fn() -> Element,
48    devtools_receiver: DevtoolsReceiver,
49    hovered_node: HoveredNode,
50) -> VirtualDom {
51    VirtualDom::new_with_props(
52        AppWithDevtools,
53        AppWithDevtoolsProps {
54            root,
55            devtools_receiver,
56            hovered_node,
57        },
58    )
59}
60
61#[derive(Props, Clone)]
62struct AppWithDevtoolsProps {
63    root: fn() -> Element,
64    devtools_receiver: DevtoolsReceiver,
65    hovered_node: HoveredNode,
66}
67
68impl PartialEq for AppWithDevtoolsProps {
69    fn eq(&self, _other: &Self) -> bool {
70        true
71    }
72}
73
74#[allow(non_snake_case)]
75fn AppWithDevtools(props: AppWithDevtoolsProps) -> Element {
76    #[allow(non_snake_case)]
77    let Root = props.root;
78    let devtools_receiver = props.devtools_receiver;
79    let hovered_node = props.hovered_node;
80
81    rsx!(
82        NativeContainer {
83            ResizableContainer {
84                direction: "horizontal",
85                ResizablePanel {
86                    initial_size: 75.,
87                    Root { }
88                }
89                ResizableHandle { }
90                ResizablePanel {
91                    initial_size: 25.,
92                    min_size: 10.,
93                    rect {
94                        background: "rgb(40, 40, 40)",
95                        height: "fill",
96                        width: "fill",
97                        ThemeProvider {
98                            DevTools {
99                                devtools_receiver,
100                                hovered_node
101                            }
102                        }
103                    }
104                }
105            }
106        }
107    )
108}
109
110#[derive(Props, Clone)]
111pub struct DevToolsProps {
112    devtools_receiver: DevtoolsReceiver,
113    hovered_node: HoveredNode,
114}
115
116impl PartialEq for DevToolsProps {
117    fn eq(&self, _: &Self) -> bool {
118        true
119    }
120}
121
122#[allow(non_snake_case)]
123pub fn DevTools(props: DevToolsProps) -> Element {
124    use_init_theme(|| DARK_THEME);
125    use_init_radio_station::<DevtoolsState, DevtoolsChannel>(|| DevtoolsState {
126        hovered_node: props.hovered_node.clone(),
127        devtools_receiver: props.devtools_receiver.clone(),
128        devtools_tree: HashSet::default(),
129    });
130
131    let theme = use_applied_theme!(None, body);
132    let color = &theme.color;
133
134    rsx!(
135        rect {
136            width: "fill",
137            height: "fill",
138            color: "{color}",
139            Router::<Route> { }
140        }
141    )
142}
143
144#[component]
145#[allow(non_snake_case)]
146pub fn DevtoolsBar() -> Element {
147    rsx!(
148        Tabsbar {
149            Link {
150                to: Route::DOMInspector { },
151                ActivableRoute {
152                    route: Route::DOMInspector { },
153                    Tab {
154                        label {
155                            "Elements"
156                        }
157                    }
158                }
159            }
160        }
161
162        NativeRouter {
163            Outlet::<Route> {}
164        }
165    )
166}
167
168#[derive(Routable, Clone, PartialEq, Debug)]
169#[rustfmt::skip]
170pub enum Route {
171    #[layout(DevtoolsBar)]
172        #[layout(LayoutForDOMInspector)]
173            #[route("/")]
174            DOMInspector  {},
175            #[nest("/node/:node_id")]
176                #[layout(LayoutForNodeInspector)]
177                    #[route("/style")]
178                    NodeInspectorStyle { node_id: String },
179                    #[route("/layout")]
180                    NodeInspectorLayout { node_id: String },
181                #[end_layout]
182            #[end_nest]
183        #[end_layout]
184    #[end_layout]
185    #[route("/..route")]
186    PageNotFound { },
187}
188
189impl Route {
190    pub fn get_node_id(&self) -> Option<NodeId> {
191        match self {
192            Self::NodeInspectorStyle { node_id } | Self::NodeInspectorLayout { node_id } => {
193                Some(NodeId::deserialize(node_id))
194            }
195            _ => None,
196        }
197    }
198}
199
200#[allow(non_snake_case)]
201#[component]
202fn PageNotFound() -> Element {
203    rsx!(
204        label {
205            "Page not found."
206        }
207    )
208}
209
210#[allow(non_snake_case)]
211#[component]
212fn LayoutForNodeInspector(node_id: String) -> Element {
213    let navigator = use_navigator();
214
215    rsx!(
216        rect {
217            overflow: "clip",
218            width: "fill",
219            height: "fill",
220            background: "rgb(30, 30, 30)",
221            margin: "10",
222            corner_radius: "16",
223            cross_align: "center",
224            padding: "6 0 0 0",
225            spacing: "6",
226            rect {
227                direction: "horizontal",
228                width: "fill",
229                main_align: "space-between",
230                padding: "0 2",
231                rect {
232                    direction: "horizontal",
233                    Link {
234                        to: Route::NodeInspectorStyle { node_id: node_id.clone() },
235                        ActivableRoute {
236                            route: Route::NodeInspectorStyle { node_id: node_id.clone() },
237                            BottomTab {
238                                label {
239                                    "Style"
240                                }
241                            }
242                        }
243                    }
244                    Link {
245                        to: Route::NodeInspectorLayout { node_id: node_id.clone() },
246                        ActivableRoute {
247                            route: Route::NodeInspectorLayout { node_id },
248                            BottomTab {
249                                label {
250                                    "Layout"
251                                }
252                            }
253                        }
254                    }
255                }
256                BottomTab {
257                    onpress: move |_| {navigator.replace(Route::DOMInspector {});},
258                    label {
259                        "Close"
260                    }
261                }
262            }
263            Outlet::<Route> {}
264        }
265    )
266}
267
268#[allow(non_snake_case)]
269#[component]
270fn LayoutForDOMInspector() -> Element {
271    let route = use_route::<Route>();
272    let platform = use_platform();
273    let mut radio = use_radio(DevtoolsChannel::Global);
274    use_hook(move || {
275        spawn(async move {
276            let mut devtools_receiver = radio.read().devtools_receiver.clone();
277            loop {
278                devtools_receiver
279                    .changed()
280                    .await
281                    .expect("Failed while waiting for DOM changes.");
282
283                radio.write_channel(DevtoolsChannel::UpdatedDOM);
284            }
285        });
286    });
287
288    let selected_node_id = route.get_node_id();
289
290    let is_expanded_vertical = selected_node_id.is_some();
291
292    rsx!(
293        rect {
294            height: "fill",
295            ResizableContainer {
296                direction: "vertical",
297                ResizablePanel {
298                    initial_size: 50.,
299                    NodesTree {
300                        height: "fill",
301                        selected_node_id,
302                        onselected: move |node_id: NodeId| {
303                            if let Some(hovered_node) = &radio.read().hovered_node.as_ref() {
304                                hovered_node.lock().unwrap().replace(node_id);
305                                platform.send(EventLoopMessage::RequestFullRerender).ok();
306                            }
307                        }
308                    }
309                }
310                ResizableHandle { }
311                ResizablePanel {
312                    initial_size: 50.,
313                    if is_expanded_vertical {
314
315                        Outlet::<Route> {}
316                    } else {
317                        rect {
318                            main_align: "center",
319                            cross_align: "center",
320                            width: "fill",
321                            height: "fill",
322                            label {
323                                "Select an element to inspect."
324                            }
325                        }
326                    }
327                }
328            }
329        }
330    )
331}
332
333#[allow(non_snake_case)]
334#[component]
335fn DOMInspector() -> Element {
336    Ok(VNode::placeholder())
337}
338
339pub trait NodeIdSerializer {
340    fn serialize(&self) -> String;
341
342    fn deserialize(node_id: &str) -> Self;
343}
344
345impl NodeIdSerializer for NodeId {
346    fn serialize(&self) -> String {
347        format!("{}-{}", self.index(), self.gen())
348    }
349
350    fn deserialize(node_id: &str) -> Self {
351        let (index, gen) = node_id.split_once('-').unwrap();
352        NodeId::new_from_index_and_gen(index.parse().unwrap(), gen.parse().unwrap())
353    }
354}