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",
225            spacing: "6",
226            rect {
227                direction: "horizontal",
228                width: "fill",
229                main_align: "space-between",
230                rect {
231                    direction: "horizontal",
232                    Link {
233                        to: Route::NodeInspectorStyle { node_id: node_id.clone() },
234                        ActivableRoute {
235                            route: Route::NodeInspectorStyle { node_id: node_id.clone() },
236                            BottomTab {
237                                label {
238                                    "Style"
239                                }
240                            }
241                        }
242                    }
243                    Link {
244                        to: Route::NodeInspectorLayout { node_id: node_id.clone() },
245                        ActivableRoute {
246                            route: Route::NodeInspectorLayout { node_id },
247                            BottomTab {
248                                label {
249                                    "Layout"
250                                }
251                            }
252                        }
253                    }
254                }
255                BottomTab {
256                    onpress: move |_| {navigator.replace(Route::DOMInspector {});},
257                    label {
258                        "Close"
259                    }
260                }
261            }
262            Outlet::<Route> {}
263        }
264    )
265}
266
267#[allow(non_snake_case)]
268#[component]
269fn LayoutForDOMInspector() -> Element {
270    let route = use_route::<Route>();
271    let platform = use_platform();
272    let mut radio = use_radio(DevtoolsChannel::Global);
273    use_hook(move || {
274        spawn(async move {
275            let mut devtools_receiver = radio.read().devtools_receiver.clone();
276            loop {
277                devtools_receiver
278                    .changed()
279                    .await
280                    .expect("Failed while waiting for DOM changes.");
281
282                radio.write_channel(DevtoolsChannel::UpdatedDOM);
283            }
284        });
285    });
286
287    let selected_node_id = route.get_node_id();
288
289    let is_expanded_vertical = selected_node_id.is_some();
290
291    rsx!(
292        rect {
293            height: "fill",
294            ResizableContainer {
295                direction: "vertical",
296                ResizablePanel {
297                    initial_size: 50.,
298                    NodesTree {
299                        height: "fill",
300                        selected_node_id,
301                        onselected: move |node_id: NodeId| {
302                            if let Some(hovered_node) = &radio.read().hovered_node.as_ref() {
303                                hovered_node.lock().unwrap().replace(node_id);
304                                platform.send(EventLoopMessage::RequestFullRerender).ok();
305                            }
306                        }
307                    }
308                }
309                ResizableHandle { }
310                ResizablePanel {
311                    initial_size: 50.,
312                    if is_expanded_vertical {
313
314                        Outlet::<Route> {}
315                    } else {
316                        rect {
317                            main_align: "center",
318                            cross_align: "center",
319                            width: "fill",
320                            height: "fill",
321                            label {
322                                "Select an element to inspect."
323                            }
324                        }
325                    }
326                }
327            }
328        }
329    )
330}
331
332#[allow(non_snake_case)]
333#[component]
334fn DOMInspector() -> Element {
335    Ok(VNode::placeholder())
336}
337
338pub trait NodeIdSerializer {
339    fn serialize(&self) -> String;
340
341    fn deserialize(node_id: &str) -> Self;
342}
343
344impl NodeIdSerializer for NodeId {
345    fn serialize(&self) -> String {
346        format!("{}-{}", self.index(), self.gen())
347    }
348
349    fn deserialize(node_id: &str) -> Self {
350        let (index, gen) = node_id.split_once('-').unwrap();
351        NodeId::new_from_index_and_gen(index.parse().unwrap(), gen.parse().unwrap())
352    }
353}