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}