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 tabs::{
39    computed_layout::*,
40    layout::*,
41    misc::*,
42    style::*,
43    text_style::*,
44    tree::*,
45};
46
47fn main() {
48    launch(
49        LaunchConfig::new().with_window(
50            WindowConfig::new(app)
51                .with_title("Freya Devtools")
52                .with_size(1200., 700.),
53        ),
54    )
55}
56
57pub fn app() -> impl IntoElement {
58    use_init_root_theme(|| DARK_THEME);
59    use_init_radio_station::<DevtoolsState, DevtoolsChannel>(|| DevtoolsState {
60        nodes: HashMap::new(),
61        expanded_nodes: HashSet::default(),
62        client: Arc::default(),
63        animation_speed: AnimationClock::DEFAULT_SPEED / AnimationClock::MAX_SPEED * 100.,
64    });
65    let mut radio = use_radio(DevtoolsChannel::Global);
66
67    use_hook(move || {
68        spawn(async move {
69            async fn connect(
70                mut radio: Radio<DevtoolsState, DevtoolsChannel>,
71            ) -> Result<(), tungstenite::Error> {
72                let tcp_stream = TcpStream::connect("[::1]:7354").await?;
73                let (ws_stream, _response) =
74                    async_tungstenite::client_async("ws://[::1]:7354", tcp_stream).await?;
75
76                let (write, read) = ws_stream.split();
77
78                radio.write_silently().client.lock().await.replace(write);
79
80                read.for_each(move |message| async move {
81                    if let Ok(message) = message
82                        && let Ok(text) = message.into_text()
83                        && let Ok(outgoing) = serde_json::from_str::<OutgoingMessage>(&text)
84                    {
85                        match outgoing.action {
86                            OutgoingMessageAction::Update { window_id, nodes } => {
87                                radio
88                                    .write_channel(DevtoolsChannel::UpdatedTree)
89                                    .nodes
90                                    .insert(window_id, nodes);
91                            }
92                        }
93                    }
94                })
95                .await;
96
97                Ok(())
98            }
99
100            loop {
101                println!("Connecting to server...");
102                connect(radio).await.ok();
103                radio
104                    .write_channel(DevtoolsChannel::UpdatedTree)
105                    .nodes
106                    .clear();
107                Timer::after(Duration::from_secs(2)).await;
108            }
109        })
110    });
111
112    rect()
113        .width(Size::fill())
114        .height(Size::fill())
115        .color(Color::WHITE)
116        .background((15, 15, 15))
117        .child(router(|| {
118            RouterConfig::<Route>::default().with_initial_path(Route::TreeInspector {})
119        }))
120}
121
122#[derive(PartialEq)]
123struct NavBar;
124impl Render for NavBar {
125    fn render(&self) -> impl IntoElement {
126        SideBar::new()
127            .width(Size::px(100.))
128            .bar(
129                rect()
130                    .child(ActivableRoute::new(
131                        Route::TreeInspector {},
132                        Link::new(Route::TreeInspector {}).child(SideBarItem::new().child("Tree")),
133                    ))
134                    .child(ActivableRoute::new(
135                        Route::Misc {},
136                        Link::new(Route::Misc {}).child(SideBarItem::new().child("Misc")),
137                    )),
138            )
139            .content(rect().padding(Gaps::new_all(8.)).child(outlet::<Route>()))
140    }
141}
142#[derive(Routable, Clone, PartialEq, Debug)]
143#[rustfmt::skip]
144pub enum Route {
145    #[layout(NavBar)]
146        #[route("/misc")]
147        Misc {},
148        #[layout(LayoutForTreeInspector)]
149            #[nest("/inspector")]
150                #[route("/")]
151                TreeInspector {},
152                #[nest("/node/:node_id/:window_id")]
153                    #[layout(LayoutForNodeInspector)]
154                        #[route("/style")]
155                        NodeInspectorStyle { node_id: NodeId, window_id: u64 },
156                        #[route("/layout")]
157                        NodeInspectorLayout { node_id: NodeId, window_id: u64 },
158                        #[route("/computed-layout")]
159                        NodeInspectorComputedLayout { node_id: NodeId, window_id: u64 },
160                        #[route("/text-style")]
161                        NodeInspectorTextStyle { node_id: NodeId, window_id: u64 },
162}
163
164impl Route {
165    pub fn node_id(&self) -> Option<NodeId> {
166        match self {
167            Self::NodeInspectorStyle { node_id, .. }
168            | Self::NodeInspectorComputedLayout { node_id, .. }
169            | Self::NodeInspectorLayout { node_id, .. }
170            | Self::NodeInspectorTextStyle { node_id, .. } => Some(*node_id),
171            _ => None,
172        }
173    }
174
175    pub fn window_id(&self) -> Option<u64> {
176        match self {
177            Self::NodeInspectorStyle { window_id, .. }
178            | Self::NodeInspectorComputedLayout { window_id, .. }
179            | Self::NodeInspectorLayout { window_id, .. }
180            | Self::NodeInspectorTextStyle { window_id, .. } => Some(*window_id),
181            _ => None,
182        }
183    }
184}
185
186#[derive(PartialEq, Clone, Copy)]
187struct LayoutForNodeInspector {
188    window_id: u64,
189    node_id: NodeId,
190}
191
192impl Render for LayoutForNodeInspector {
193    fn render(&self) -> impl IntoElement {
194        let LayoutForNodeInspector { window_id, node_id } = *self;
195
196        rect()
197            .expanded()
198            .child(
199                ScrollView::new()
200                    .show_scrollbar(false)
201                    .height(Size::auto())
202                    .child(
203                        rect()
204                            .direction(Direction::Horizontal)
205                            .padding((0., 4.))
206                            .child(ActivableRoute::new(
207                                Route::NodeInspectorStyle { node_id, window_id },
208                                Link::new(Route::NodeInspectorStyle { node_id, window_id }).child(
209                                    FloatingTab::new().child(label().text("Style").max_lines(1)),
210                                ),
211                            ))
212                            .child(ActivableRoute::new(
213                                Route::NodeInspectorLayout { node_id, window_id },
214                                Link::new(Route::NodeInspectorLayout { node_id, window_id }).child(
215                                    FloatingTab::new().child(label().text("Layout").max_lines(1)),
216                                ),
217                            ))
218                            .child(ActivableRoute::new(
219                                Route::NodeInspectorTextStyle { node_id, window_id },
220                                Link::new(Route::NodeInspectorTextStyle { node_id, window_id })
221                                    .child(
222                                        FloatingTab::new()
223                                            .child(label().text("Text Style").max_lines(1)),
224                                    ),
225                            ))
226                            .child(ActivableRoute::new(
227                                Route::NodeInspectorComputedLayout { node_id, window_id },
228                                Link::new(Route::NodeInspectorComputedLayout {
229                                    node_id,
230                                    window_id,
231                                })
232                                .child(
233                                    FloatingTab::new()
234                                        .child(label().text("Computed Layout").max_lines(1)),
235                                ),
236                            )),
237                    ),
238            )
239            .child(rect().padding((6., 0.)).child(outlet::<Route>()))
240    }
241}
242
243#[derive(PartialEq)]
244struct LayoutForTreeInspector;
245
246impl Render for LayoutForTreeInspector {
247    fn render(&self) -> impl IntoElement {
248        let route = use_route::<Route>();
249        let radio = use_radio(DevtoolsChannel::Global);
250
251        let selected_node_id = route.node_id();
252        let selected_window_id = route.window_id();
253
254        let is_expanded_vertical = selected_node_id.is_some();
255
256        ResizableContainer::new()
257            .direction(Direction::Horizontal)
258            .panel(
259                ResizablePanel::new(60.).child(rect().padding(10.).child(NodesTree {
260                    selected_node_id,
261                    selected_window_id,
262                    on_selected: EventHandler::new(move |(window_id, node_id)| {
263                        let message = Message::Text(
264                            serde_json::to_string(&IncomingMessage {
265                                action: IncomingMessageAction::HighlightNode { window_id, node_id },
266                            })
267                            .unwrap()
268                            .into(),
269                        );
270                        let client = radio.read().client.clone();
271                        spawn(async move {
272                            client
273                                .lock()
274                                .await
275                                .as_mut()
276                                .unwrap()
277                                .send(message)
278                                .await
279                                .ok();
280                        });
281                    }),
282                })),
283            )
284            .panel(is_expanded_vertical.then(|| ResizablePanel::new(40.).child(outlet::<Route>())))
285    }
286}
287
288#[derive(PartialEq)]
289struct TreeInspector;
290
291impl Render for TreeInspector {
292    fn render(&self) -> impl IntoElement {
293        rect()
294    }
295}