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