freya_devtools_app/
main.rs

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