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",
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}