Skip to main content

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    IncomingMessageAction,
17    OutgoingMessage,
18    OutgoingMessageAction,
19};
20use freya_router::prelude::*;
21use futures_util::StreamExt;
22use smol::{
23    Timer,
24    net::TcpStream,
25};
26use state::{
27    DevtoolsChannel,
28    DevtoolsState,
29};
30
31mod components;
32mod hooks;
33mod node;
34mod property;
35mod state;
36mod tabs;
37
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_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(ContextMenuViewer::new())
119        .child(Router::new(|| {
120            RouterConfig::<Route>::default().with_initial_path(Route::TreeInspector {})
121        }))
122}
123
124#[derive(PartialEq)]
125struct NavBar;
126impl Component for NavBar {
127    fn render(&self) -> impl IntoElement {
128        rect()
129            .horizontal()
130            .child(
131                rect()
132                    .theme_background()
133                    .height(Size::fill())
134                    .width(Size::px(100.))
135                    .padding(8.)
136                    .child(ActivableRoute::new(
137                        Route::TreeInspector {},
138                        Link::new(Route::TreeInspector {}).child(SideBarItem::new().child("Tree")),
139                    ))
140                    .child(ActivableRoute::new(
141                        Route::Misc {},
142                        Link::new(Route::Misc {}).child(SideBarItem::new().child("Misc")),
143                    )),
144            )
145            .child(
146                rect()
147                    .padding(Gaps::new_all(8.))
148                    .overflow(Overflow::Clip)
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
193fn info_label(value: impl Into<String>, suffix: &str) -> impl IntoElement {
194    paragraph()
195        .max_lines(1)
196        .height(Size::px(20.))
197        .span(Span::new(value.into()))
198        .span(Span::new(format!(" {suffix}")).color((200, 200, 200)))
199}
200
201fn inspector_tab(route: Route, text: &'static str) -> impl IntoElement {
202    ActivableRoute::new(
203        route.clone(),
204        Link::new(route).child(
205            FloatingTab::new()
206                .corner_radius(CornerRadius::new_all(8.))
207                .padding(Gaps::new_all(8.))
208                .child(label().text(text).max_lines(1)),
209        ),
210    )
211}
212
213#[derive(PartialEq, Clone, Copy)]
214struct LayoutForNodeInspector {
215    window_id: u64,
216    node_id: NodeId,
217}
218
219impl Component for LayoutForNodeInspector {
220    fn render(&self) -> impl IntoElement {
221        let LayoutForNodeInspector { window_id, node_id } = *self;
222
223        let Some(node_info) = use_node_info(node_id, window_id) else {
224            return rect();
225        };
226
227        let inner_area = format!(
228            "{}x{}",
229            node_info.inner_area.width().round(),
230            node_info.inner_area.height().round()
231        );
232        let area = format!(
233            "{}x{}",
234            node_info.area.width().round(),
235            node_info.area.height().round()
236        );
237        let padding = node_info.state.layout.padding;
238        let margin = node_info.state.layout.margin;
239
240        rect()
241            .expanded()
242            .child(
243                ScrollView::new()
244                    .show_scrollbar(false)
245                    .height(Size::px(280.))
246                    .child(
247                        rect()
248                            .padding(16.)
249                            .width(Size::fill())
250                            .cross_align(Alignment::Center)
251                            .child(
252                                rect()
253                                    .width(Size::fill())
254                                    .max_width(Size::px(300.))
255                                    .spacing(6.)
256                                    .child(
257                                        rect()
258                                            .horizontal()
259                                            .spacing(6.)
260                                            .child(info_label(area, "area"))
261                                            .child(info_label(
262                                                node_info.children_len.to_string(),
263                                                "children",
264                                            ))
265                                            .child(info_label(
266                                                node_info.layer.to_string(),
267                                                "layer",
268                                            )),
269                                    )
270                                    .child(computed_layout(inner_area, padding, margin)),
271                            ),
272                    ),
273            )
274            .child(
275                ScrollView::new()
276                    .show_scrollbar(false)
277                    .height(Size::auto())
278                    .child(
279                        rect()
280                            .direction(Direction::Horizontal)
281                            .padding((0., 4.))
282                            .child(inspector_tab(
283                                Route::NodeInspectorStyle { node_id, window_id },
284                                "Style",
285                            ))
286                            .child(inspector_tab(
287                                Route::NodeInspectorLayout { node_id, window_id },
288                                "Layout",
289                            ))
290                            .child(inspector_tab(
291                                Route::NodeInspectorTextStyle { node_id, window_id },
292                                "Text Style",
293                            )),
294                    ),
295            )
296            .child(rect().padding((6., 0.)).child(Outlet::<Route>::new()))
297    }
298}
299
300#[derive(PartialEq)]
301struct LayoutForTreeInspector;
302
303impl Component for LayoutForTreeInspector {
304    fn render(&self) -> impl IntoElement {
305        let route = use_route::<Route>();
306        let radio = use_radio(DevtoolsChannel::Global);
307
308        let selected_node_id = route.node_id();
309        let selected_window_id = route.window_id();
310
311        let is_expanded_vertical = selected_node_id.is_some();
312
313        ResizableContainer::new()
314            .direction(Direction::Horizontal)
315            .panel(
316                ResizablePanel::new(PanelSize::percent(60.)).child(rect().padding(10.).child(
317                    NodesTree {
318                        selected_node_id,
319                        selected_window_id,
320                        on_selected: EventHandler::new(move |(window_id, node_id)| {
321                            radio
322                                .read()
323                                .send_action(IncomingMessageAction::HighlightNode {
324                                    window_id,
325                                    node_id,
326                                });
327                        }),
328                        on_hover: EventHandler::new(move |(window_id, node_id)| {
329                            radio.read().send_action(IncomingMessageAction::HoverNode {
330                                window_id,
331                                node_id,
332                            });
333                        }),
334                    },
335                )),
336            )
337            .panel(is_expanded_vertical.then(|| {
338                ResizablePanel::new(PanelSize::px(400.))
339                    .min_size(300.)
340                    .child(Outlet::<Route>::new())
341            }))
342    }
343}
344
345#[derive(PartialEq)]
346struct TreeInspector;
347
348impl Component for TreeInspector {
349    fn render(&self) -> impl IntoElement {
350        rect()
351    }
352}