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 SideBar::new()
130 .width(Size::px(100.))
131 .bar(
132 rect()
133 .child(ActivableRoute::new(
134 Route::TreeInspector {},
135 Link::new(Route::TreeInspector {}).child(SideBarItem::new().child("Tree")),
136 ))
137 .child(ActivableRoute::new(
138 Route::Misc {},
139 Link::new(Route::Misc {}).child(SideBarItem::new().child("Misc")),
140 )),
141 )
142 .content(
143 rect()
144 .padding(Gaps::new_all(8.))
145 .child(Outlet::<Route>::new()),
146 )
147 }
148}
149#[derive(Routable, Clone, PartialEq, Debug)]
150#[rustfmt::skip]
151pub enum Route {
152 #[layout(NavBar)]
153 #[route("/misc")]
154 Misc {},
155 #[layout(LayoutForTreeInspector)]
156 #[nest("/inspector")]
157 #[route("/")]
158 TreeInspector {},
159 #[nest("/node/:node_id/:window_id")]
160 #[layout(LayoutForNodeInspector)]
161 #[route("/style")]
162 NodeInspectorStyle { node_id: NodeId, window_id: u64 },
163 #[route("/layout")]
164 NodeInspectorLayout { node_id: NodeId, window_id: u64 },
165 #[route("/text-style")]
166 NodeInspectorTextStyle { node_id: NodeId, window_id: u64 },
167}
168
169impl Route {
170 pub fn node_id(&self) -> Option<NodeId> {
171 match self {
172 Self::NodeInspectorStyle { node_id, .. }
173 | Self::NodeInspectorLayout { node_id, .. }
174 | Self::NodeInspectorTextStyle { node_id, .. } => Some(*node_id),
175 _ => None,
176 }
177 }
178
179 pub fn window_id(&self) -> Option<u64> {
180 match self {
181 Self::NodeInspectorStyle { window_id, .. }
182 | Self::NodeInspectorLayout { window_id, .. }
183 | Self::NodeInspectorTextStyle { window_id, .. } => Some(*window_id),
184 _ => None,
185 }
186 }
187}
188
189#[derive(PartialEq, Clone, Copy)]
190struct LayoutForNodeInspector {
191 window_id: u64,
192 node_id: NodeId,
193}
194
195impl Component for LayoutForNodeInspector {
196 fn render(&self) -> impl IntoElement {
197 let LayoutForNodeInspector { window_id, node_id } = *self;
198
199 let Some(node_info) = use_node_info(node_id, window_id) else {
200 return rect();
201 };
202
203 let inner_area = format!(
204 "{}x{}",
205 node_info.inner_area.width().round(),
206 node_info.inner_area.height().round()
207 );
208 let area = format!(
209 "{}x{}",
210 node_info.area.width().round(),
211 node_info.area.height().round()
212 );
213 let padding = node_info.state.layout.padding;
214 let margin = node_info.state.layout.margin;
215
216 rect()
217 .expanded()
218 .child(
219 ScrollView::new()
220 .show_scrollbar(false)
221 .height(Size::px(280.))
222 .child(
223 rect()
224 .padding(16.)
225 .width(Size::fill())
226 .cross_align(Alignment::Center)
227 .child(
228 rect()
229 .width(Size::fill())
230 .max_width(Size::px(300.))
231 .spacing(6.)
232 .child(
233 rect()
234 .horizontal()
235 .spacing(6.)
236 .child(
237 paragraph()
238 .max_lines(1)
239 .height(Size::px(20.))
240 .span(Span::new(area))
241 .span(
242 Span::new(" area").color((200, 200, 200)),
243 ),
244 )
245 .child(
246 paragraph()
247 .max_lines(1)
248 .height(Size::px(20.))
249 .span(Span::new(
250 node_info.children_len.to_string(),
251 ))
252 .span(
253 Span::new(" children")
254 .color((200, 200, 200)),
255 ),
256 )
257 .child(
258 paragraph()
259 .max_lines(1)
260 .height(Size::px(20.))
261 .span(Span::new(node_info.layer.to_string()))
262 .span(
263 Span::new(" layer").color((200, 200, 200)),
264 ),
265 ),
266 )
267 .child(computed_layout(inner_area, padding, margin)),
268 ),
269 ),
270 )
271 .child(
272 ScrollView::new()
273 .show_scrollbar(false)
274 .height(Size::auto())
275 .child(
276 rect()
277 .direction(Direction::Horizontal)
278 .padding((0., 4.))
279 .child(ActivableRoute::new(
280 Route::NodeInspectorStyle { node_id, window_id },
281 Link::new(Route::NodeInspectorStyle { node_id, window_id }).child(
282 FloatingTab::new().child(label().text("Style").max_lines(1)),
283 ),
284 ))
285 .child(ActivableRoute::new(
286 Route::NodeInspectorLayout { node_id, window_id },
287 Link::new(Route::NodeInspectorLayout { node_id, window_id }).child(
288 FloatingTab::new().child(label().text("Layout").max_lines(1)),
289 ),
290 ))
291 .child(ActivableRoute::new(
292 Route::NodeInspectorTextStyle { node_id, window_id },
293 Link::new(Route::NodeInspectorTextStyle { node_id, window_id })
294 .child(
295 FloatingTab::new()
296 .child(label().text("Text Style").max_lines(1)),
297 ),
298 )),
299 ),
300 )
301 .child(rect().padding((6., 0.)).child(Outlet::<Route>::new()))
302 }
303}
304
305#[derive(PartialEq)]
306struct LayoutForTreeInspector;
307
308impl Component for LayoutForTreeInspector {
309 fn render(&self) -> impl IntoElement {
310 let route = use_route::<Route>();
311 let radio = use_radio(DevtoolsChannel::Global);
312
313 let selected_node_id = route.node_id();
314 let selected_window_id = route.window_id();
315
316 let is_expanded_vertical = selected_node_id.is_some();
317
318 ResizableContainer::new()
319 .direction(Direction::Horizontal)
320 .panel(
321 ResizablePanel::new(60.).child(rect().padding(10.).child(NodesTree {
322 selected_node_id,
323 selected_window_id,
324 on_selected: EventHandler::new(move |(window_id, node_id)| {
325 let message = Message::Text(
326 serde_json::to_string(&IncomingMessage {
327 action: IncomingMessageAction::HighlightNode { window_id, node_id },
328 })
329 .unwrap()
330 .into(),
331 );
332 let client = radio.read().client.clone();
333 spawn(async move {
334 client
335 .lock()
336 .await
337 .as_mut()
338 .unwrap()
339 .send(message)
340 .await
341 .ok();
342 });
343 }),
344 on_hover: EventHandler::new(move |(window_id, node_id)| {
345 let message = Message::Text(
346 serde_json::to_string(&IncomingMessage {
347 action: IncomingMessageAction::HoverNode { window_id, node_id },
348 })
349 .unwrap()
350 .into(),
351 );
352 let client = radio.read().client.clone();
353 spawn(async move {
354 client
355 .lock()
356 .await
357 .as_mut()
358 .unwrap()
359 .send(message)
360 .await
361 .ok();
362 });
363 }),
364 })),
365 )
366 .panel(
367 is_expanded_vertical
368 .then(|| ResizablePanel::new(40.).child(Outlet::<Route>::new())),
369 )
370 }
371}
372
373#[derive(PartialEq)]
374struct TreeInspector;
375
376impl Component for TreeInspector {
377 fn render(&self) -> impl IntoElement {
378 rect()
379 }
380}