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