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 tabs::{
39 computed_layout::*,
40 layout::*,
41 misc::*,
42 style::*,
43 text_style::*,
44 tree::*,
45};
46
47fn main() {
48 launch(
49 LaunchConfig::new().with_window(
50 WindowConfig::new(app)
51 .with_title("Freya Devtools")
52 .with_size(1200., 700.),
53 ),
54 )
55}
56
57pub fn app() -> impl IntoElement {
58 use_init_root_theme(|| DARK_THEME);
59 use_init_radio_station::<DevtoolsState, DevtoolsChannel>(|| DevtoolsState {
60 nodes: HashMap::new(),
61 expanded_nodes: HashSet::default(),
62 client: Arc::default(),
63 animation_speed: AnimationClock::DEFAULT_SPEED / AnimationClock::MAX_SPEED * 100.,
64 });
65 let mut radio = use_radio(DevtoolsChannel::Global);
66
67 use_hook(move || {
68 spawn(async move {
69 async fn connect(
70 mut radio: Radio<DevtoolsState, DevtoolsChannel>,
71 ) -> Result<(), tungstenite::Error> {
72 let tcp_stream = TcpStream::connect("[::1]:7354").await?;
73 let (ws_stream, _response) =
74 async_tungstenite::client_async("ws://[::1]:7354", tcp_stream).await?;
75
76 let (write, read) = ws_stream.split();
77
78 radio.write_silently().client.lock().await.replace(write);
79
80 read.for_each(move |message| async move {
81 if let Ok(message) = message
82 && let Ok(text) = message.into_text()
83 && let Ok(outgoing) = serde_json::from_str::<OutgoingMessage>(&text)
84 {
85 match outgoing.action {
86 OutgoingMessageAction::Update { window_id, nodes } => {
87 radio
88 .write_channel(DevtoolsChannel::UpdatedTree)
89 .nodes
90 .insert(window_id, nodes);
91 }
92 }
93 }
94 })
95 .await;
96
97 Ok(())
98 }
99
100 loop {
101 println!("Connecting to server...");
102 connect(radio).await.ok();
103 radio
104 .write_channel(DevtoolsChannel::UpdatedTree)
105 .nodes
106 .clear();
107 Timer::after(Duration::from_secs(2)).await;
108 }
109 })
110 });
111
112 rect()
113 .width(Size::fill())
114 .height(Size::fill())
115 .color(Color::WHITE)
116 .background((15, 15, 15))
117 .child(router(|| {
118 RouterConfig::<Route>::default().with_initial_path(Route::TreeInspector {})
119 }))
120}
121
122#[derive(PartialEq)]
123struct NavBar;
124impl Render for NavBar {
125 fn render(&self) -> impl IntoElement {
126 SideBar::new()
127 .width(Size::px(100.))
128 .bar(
129 rect()
130 .child(ActivableRoute::new(
131 Route::TreeInspector {},
132 Link::new(Route::TreeInspector {}).child(SideBarItem::new().child("Tree")),
133 ))
134 .child(ActivableRoute::new(
135 Route::Misc {},
136 Link::new(Route::Misc {}).child(SideBarItem::new().child("Misc")),
137 )),
138 )
139 .content(rect().padding(Gaps::new_all(8.)).child(outlet::<Route>()))
140 }
141}
142#[derive(Routable, Clone, PartialEq, Debug)]
143#[rustfmt::skip]
144pub enum Route {
145 #[layout(NavBar)]
146 #[route("/misc")]
147 Misc {},
148 #[layout(LayoutForTreeInspector)]
149 #[nest("/inspector")]
150 #[route("/")]
151 TreeInspector {},
152 #[nest("/node/:node_id/:window_id")]
153 #[layout(LayoutForNodeInspector)]
154 #[route("/style")]
155 NodeInspectorStyle { node_id: NodeId, window_id: u64 },
156 #[route("/layout")]
157 NodeInspectorLayout { node_id: NodeId, window_id: u64 },
158 #[route("/computed-layout")]
159 NodeInspectorComputedLayout { node_id: NodeId, window_id: u64 },
160 #[route("/text-style")]
161 NodeInspectorTextStyle { node_id: NodeId, window_id: u64 },
162}
163
164impl Route {
165 pub fn node_id(&self) -> Option<NodeId> {
166 match self {
167 Self::NodeInspectorStyle { node_id, .. }
168 | Self::NodeInspectorComputedLayout { node_id, .. }
169 | Self::NodeInspectorLayout { node_id, .. }
170 | Self::NodeInspectorTextStyle { node_id, .. } => Some(*node_id),
171 _ => None,
172 }
173 }
174
175 pub fn window_id(&self) -> Option<u64> {
176 match self {
177 Self::NodeInspectorStyle { window_id, .. }
178 | Self::NodeInspectorComputedLayout { window_id, .. }
179 | Self::NodeInspectorLayout { window_id, .. }
180 | Self::NodeInspectorTextStyle { window_id, .. } => Some(*window_id),
181 _ => None,
182 }
183 }
184}
185
186#[derive(PartialEq, Clone, Copy)]
187struct LayoutForNodeInspector {
188 window_id: u64,
189 node_id: NodeId,
190}
191
192impl Render for LayoutForNodeInspector {
193 fn render(&self) -> impl IntoElement {
194 let LayoutForNodeInspector { window_id, node_id } = *self;
195
196 rect()
197 .expanded()
198 .child(
199 ScrollView::new()
200 .show_scrollbar(false)
201 .height(Size::auto())
202 .child(
203 rect()
204 .direction(Direction::Horizontal)
205 .padding((0., 4.))
206 .child(ActivableRoute::new(
207 Route::NodeInspectorStyle { node_id, window_id },
208 Link::new(Route::NodeInspectorStyle { node_id, window_id }).child(
209 FloatingTab::new().child(label().text("Style").max_lines(1)),
210 ),
211 ))
212 .child(ActivableRoute::new(
213 Route::NodeInspectorLayout { node_id, window_id },
214 Link::new(Route::NodeInspectorLayout { node_id, window_id }).child(
215 FloatingTab::new().child(label().text("Layout").max_lines(1)),
216 ),
217 ))
218 .child(ActivableRoute::new(
219 Route::NodeInspectorTextStyle { node_id, window_id },
220 Link::new(Route::NodeInspectorTextStyle { node_id, window_id })
221 .child(
222 FloatingTab::new()
223 .child(label().text("Text Style").max_lines(1)),
224 ),
225 ))
226 .child(ActivableRoute::new(
227 Route::NodeInspectorComputedLayout { node_id, window_id },
228 Link::new(Route::NodeInspectorComputedLayout {
229 node_id,
230 window_id,
231 })
232 .child(
233 FloatingTab::new()
234 .child(label().text("Computed Layout").max_lines(1)),
235 ),
236 )),
237 ),
238 )
239 .child(rect().padding((6., 0.)).child(outlet::<Route>()))
240 }
241}
242
243#[derive(PartialEq)]
244struct LayoutForTreeInspector;
245
246impl Render for LayoutForTreeInspector {
247 fn render(&self) -> impl IntoElement {
248 let route = use_route::<Route>();
249 let radio = use_radio(DevtoolsChannel::Global);
250
251 let selected_node_id = route.node_id();
252 let selected_window_id = route.window_id();
253
254 let is_expanded_vertical = selected_node_id.is_some();
255
256 ResizableContainer::new()
257 .direction(Direction::Horizontal)
258 .panel(
259 ResizablePanel::new(60.).child(rect().padding(10.).child(NodesTree {
260 selected_node_id,
261 selected_window_id,
262 on_selected: EventHandler::new(move |(window_id, node_id)| {
263 let message = Message::Text(
264 serde_json::to_string(&IncomingMessage {
265 action: IncomingMessageAction::HighlightNode { window_id, node_id },
266 })
267 .unwrap()
268 .into(),
269 );
270 let client = radio.read().client.clone();
271 spawn(async move {
272 client
273 .lock()
274 .await
275 .as_mut()
276 .unwrap()
277 .send(message)
278 .await
279 .ok();
280 });
281 }),
282 })),
283 )
284 .panel(is_expanded_vertical.then(|| ResizablePanel::new(40.).child(outlet::<Route>())))
285 }
286}
287
288#[derive(PartialEq)]
289struct TreeInspector;
290
291impl Render for TreeInspector {
292 fn render(&self) -> impl IntoElement {
293 rect()
294 }
295}