freya_devtools_app/
main.rs

1use std::{
2    collections::{
3        HashMap,
4        HashSet,
5    },
6    sync::Arc,
7    time::Duration,
8};
9
10use freya::{
11    prelude::*,
12    radio::*,
13};
14use freya_core::integration::NodeId;
15use freya_devtools::{
16    IncomingMessage,
17    IncomingMessageAction,
18    OutgoingMessage,
19    OutgoingMessageAction,
20};
21use freya_router::prelude::*;
22use futures_util::StreamExt;
23use smol::{
24    Timer,
25    net::TcpStream,
26};
27use state::{
28    DevtoolsChannel,
29    DevtoolsState,
30};
31
32mod components;
33mod hooks;
34mod node;
35mod property;
36mod state;
37mod tabs;
38
39use async_tungstenite::tungstenite::protocol::Message;
40use hooks::use_node_info;
41use tabs::{
42    computed_layout::computed_layout,
43    layout::*,
44    misc::*,
45    style::*,
46    text_style::*,
47    tree::*,
48};
49
50fn main() {
51    launch(
52        LaunchConfig::new().with_window(
53            WindowConfig::new(app)
54                .with_title("Freya Devtools")
55                .with_size(1200., 700.),
56        ),
57    )
58}
59
60pub fn app() -> impl IntoElement {
61    use_init_root_theme(|| DARK_THEME);
62    use_init_radio_station::<DevtoolsState, DevtoolsChannel>(|| DevtoolsState {
63        nodes: HashMap::new(),
64        expanded_nodes: HashSet::default(),
65        client: Arc::default(),
66        animation_speed: AnimationClock::DEFAULT_SPEED / AnimationClock::MAX_SPEED * 100.,
67    });
68    let mut radio = use_radio(DevtoolsChannel::Global);
69
70    use_hook(move || {
71        spawn(async move {
72            async fn connect(
73                mut radio: Radio<DevtoolsState, DevtoolsChannel>,
74            ) -> Result<(), tungstenite::Error> {
75                let tcp_stream = TcpStream::connect("[::1]:7354").await?;
76                let (ws_stream, _response) =
77                    async_tungstenite::client_async("ws://[::1]:7354", tcp_stream).await?;
78
79                let (write, read) = ws_stream.split();
80
81                radio.write_silently().client.lock().await.replace(write);
82
83                read.for_each(move |message| async move {
84                    if let Ok(message) = message
85                        && let Ok(text) = message.into_text()
86                        && let Ok(outgoing) = serde_json::from_str::<OutgoingMessage>(&text)
87                    {
88                        match outgoing.action {
89                            OutgoingMessageAction::Update { window_id, nodes } => {
90                                radio
91                                    .write_channel(DevtoolsChannel::UpdatedTree)
92                                    .nodes
93                                    .insert(window_id, nodes);
94                            }
95                        }
96                    }
97                })
98                .await;
99
100                Ok(())
101            }
102
103            loop {
104                println!("Connecting to server...");
105                connect(radio).await.ok();
106                radio
107                    .write_channel(DevtoolsChannel::UpdatedTree)
108                    .nodes
109                    .clear();
110                Timer::after(Duration::from_secs(2)).await;
111            }
112        })
113    });
114
115    rect()
116        .width(Size::fill())
117        .height(Size::fill())
118        .color(Color::WHITE)
119        .background((15, 15, 15))
120        .child(Router::new(|| {
121            RouterConfig::<Route>::default().with_initial_path(Route::TreeInspector {})
122        }))
123}
124
125#[derive(PartialEq)]
126struct NavBar;
127impl Component for NavBar {
128    fn render(&self) -> impl IntoElement {
129        rect()
130            .horizontal()
131            .child(
132                rect()
133                    .theme_background()
134                    .height(Size::fill())
135                    .width(Size::px(100.))
136                    .padding(8.)
137                    .child(ActivableRoute::new(
138                        Route::TreeInspector {},
139                        Link::new(Route::TreeInspector {}).child(SideBarItem::new().child("Tree")),
140                    ))
141                    .child(ActivableRoute::new(
142                        Route::Misc {},
143                        Link::new(Route::Misc {}).child(SideBarItem::new().child("Misc")),
144                    )),
145            )
146            .child(
147                rect()
148                    .padding(Gaps::new_all(8.))
149                    .child(Outlet::<Route>::new()),
150            )
151    }
152}
153#[derive(Routable, Clone, PartialEq, Debug)]
154#[rustfmt::skip]
155pub enum Route {
156    #[layout(NavBar)]
157        #[route("/misc")]
158        Misc {},
159        #[layout(LayoutForTreeInspector)]
160            #[nest("/inspector")]
161                #[route("/")]
162                TreeInspector {},
163                #[nest("/node/:node_id/:window_id")]
164                    #[layout(LayoutForNodeInspector)]
165                        #[route("/style")]
166                        NodeInspectorStyle { node_id: NodeId, window_id: u64 },
167                        #[route("/layout")]
168                        NodeInspectorLayout { node_id: NodeId, window_id: u64 },
169                        #[route("/text-style")]
170                        NodeInspectorTextStyle { node_id: NodeId, window_id: u64 },
171}
172
173impl Route {
174    pub fn node_id(&self) -> Option<NodeId> {
175        match self {
176            Self::NodeInspectorStyle { node_id, .. }
177            | Self::NodeInspectorLayout { node_id, .. }
178            | Self::NodeInspectorTextStyle { node_id, .. } => Some(*node_id),
179            _ => None,
180        }
181    }
182
183    pub fn window_id(&self) -> Option<u64> {
184        match self {
185            Self::NodeInspectorStyle { window_id, .. }
186            | Self::NodeInspectorLayout { window_id, .. }
187            | Self::NodeInspectorTextStyle { window_id, .. } => Some(*window_id),
188            _ => None,
189        }
190    }
191}
192
193#[derive(PartialEq, Clone, Copy)]
194struct LayoutForNodeInspector {
195    window_id: u64,
196    node_id: NodeId,
197}
198
199impl Component for LayoutForNodeInspector {
200    fn render(&self) -> impl IntoElement {
201        let LayoutForNodeInspector { window_id, node_id } = *self;
202
203        let Some(node_info) = use_node_info(node_id, window_id) else {
204            return rect();
205        };
206
207        let inner_area = format!(
208            "{}x{}",
209            node_info.inner_area.width().round(),
210            node_info.inner_area.height().round()
211        );
212        let area = format!(
213            "{}x{}",
214            node_info.area.width().round(),
215            node_info.area.height().round()
216        );
217        let padding = node_info.state.layout.padding;
218        let margin = node_info.state.layout.margin;
219
220        rect()
221            .expanded()
222            .child(
223                ScrollView::new()
224                    .show_scrollbar(false)
225                    .height(Size::px(280.))
226                    .child(
227                        rect()
228                            .padding(16.)
229                            .width(Size::fill())
230                            .cross_align(Alignment::Center)
231                            .child(
232                                rect()
233                                    .width(Size::fill())
234                                    .max_width(Size::px(300.))
235                                    .spacing(6.)
236                                    .child(
237                                        rect()
238                                            .horizontal()
239                                            .spacing(6.)
240                                            .child(
241                                                paragraph()
242                                                    .max_lines(1)
243                                                    .height(Size::px(20.))
244                                                    .span(Span::new(area))
245                                                    .span(
246                                                        Span::new(" area").color((200, 200, 200)),
247                                                    ),
248                                            )
249                                            .child(
250                                                paragraph()
251                                                    .max_lines(1)
252                                                    .height(Size::px(20.))
253                                                    .span(Span::new(
254                                                        node_info.children_len.to_string(),
255                                                    ))
256                                                    .span(
257                                                        Span::new(" children")
258                                                            .color((200, 200, 200)),
259                                                    ),
260                                            )
261                                            .child(
262                                                paragraph()
263                                                    .max_lines(1)
264                                                    .height(Size::px(20.))
265                                                    .span(Span::new(node_info.layer.to_string()))
266                                                    .span(
267                                                        Span::new(" layer").color((200, 200, 200)),
268                                                    ),
269                                            ),
270                                    )
271                                    .child(computed_layout(inner_area, padding, margin)),
272                            ),
273                    ),
274            )
275            .child(
276                ScrollView::new()
277                    .show_scrollbar(false)
278                    .height(Size::auto())
279                    .child(
280                        rect()
281                            .direction(Direction::Horizontal)
282                            .padding((0., 4.))
283                            .child(ActivableRoute::new(
284                                Route::NodeInspectorStyle { node_id, window_id },
285                                Link::new(Route::NodeInspectorStyle { node_id, window_id }).child(
286                                    FloatingTab::new().child(label().text("Style").max_lines(1)),
287                                ),
288                            ))
289                            .child(ActivableRoute::new(
290                                Route::NodeInspectorLayout { node_id, window_id },
291                                Link::new(Route::NodeInspectorLayout { node_id, window_id }).child(
292                                    FloatingTab::new().child(label().text("Layout").max_lines(1)),
293                                ),
294                            ))
295                            .child(ActivableRoute::new(
296                                Route::NodeInspectorTextStyle { node_id, window_id },
297                                Link::new(Route::NodeInspectorTextStyle { node_id, window_id })
298                                    .child(
299                                        FloatingTab::new()
300                                            .child(label().text("Text Style").max_lines(1)),
301                                    ),
302                            )),
303                    ),
304            )
305            .child(rect().padding((6., 0.)).child(Outlet::<Route>::new()))
306    }
307}
308
309#[derive(PartialEq)]
310struct LayoutForTreeInspector;
311
312impl Component for LayoutForTreeInspector {
313    fn render(&self) -> impl IntoElement {
314        let route = use_route::<Route>();
315        let radio = use_radio(DevtoolsChannel::Global);
316
317        let selected_node_id = route.node_id();
318        let selected_window_id = route.window_id();
319
320        let is_expanded_vertical = selected_node_id.is_some();
321
322        ResizableContainer::new()
323            .direction(Direction::Horizontal)
324            .panel(
325                ResizablePanel::new(60.).child(rect().padding(10.).child(NodesTree {
326                    selected_node_id,
327                    selected_window_id,
328                    on_selected: EventHandler::new(move |(window_id, node_id)| {
329                        let message = Message::Text(
330                            serde_json::to_string(&IncomingMessage {
331                                action: IncomingMessageAction::HighlightNode { window_id, node_id },
332                            })
333                            .unwrap()
334                            .into(),
335                        );
336                        let client = radio.read().client.clone();
337                        spawn(async move {
338                            client
339                                .lock()
340                                .await
341                                .as_mut()
342                                .unwrap()
343                                .send(message)
344                                .await
345                                .ok();
346                        });
347                    }),
348                    on_hover: EventHandler::new(move |(window_id, node_id)| {
349                        let message = Message::Text(
350                            serde_json::to_string(&IncomingMessage {
351                                action: IncomingMessageAction::HoverNode { window_id, node_id },
352                            })
353                            .unwrap()
354                            .into(),
355                        );
356                        let client = radio.read().client.clone();
357                        spawn(async move {
358                            client
359                                .lock()
360                                .await
361                                .as_mut()
362                                .unwrap()
363                                .send(message)
364                                .await
365                                .ok();
366                        });
367                    }),
368                })),
369            )
370            .panel(
371                is_expanded_vertical
372                    .then(|| ResizablePanel::new(40.).child(Outlet::<Route>::new())),
373            )
374    }
375}
376
377#[derive(PartialEq)]
378struct TreeInspector;
379
380impl Component for TreeInspector {
381    fn render(&self) -> impl IntoElement {
382        rect()
383    }
384}