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}