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
45pub 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}