1use super::*;
2use crate::layout_engine::{DebugDumpNodeInfo, TaffyLayoutEngine};
3
4#[cfg(not(target_arch = "wasm32"))]
5#[derive(Debug, Clone, Copy)]
6struct LayoutSidecarRootRecord {
7 capture_index: usize,
8 kind: &'static str,
9 root: NodeId,
10 root_bounds: Rect,
11 blocks_underlay_input: bool,
12 blocks_underlay_focus: bool,
13 hit_testable: bool,
14}
15
16fn layout_debug_node_info<H: UiHost>(
17 app: &mut H,
18 window: AppWindowId,
19 node: NodeId,
20) -> DebugDumpNodeInfo {
21 let Some(record) = crate::declarative::frame::element_record_for_node(app, window, node) else {
22 return DebugDumpNodeInfo::default();
23 };
24
25 let mut label = format!("{:?}", record.instance);
26 let mut debug = serde_json::Map::new();
27 debug.insert(
28 "element_id".to_string(),
29 serde_json::json!(record.element.0),
30 );
31 debug.insert(
32 "instance_kind".to_string(),
33 serde_json::json!(record.instance.kind_name()),
34 );
35
36 let mut effective_test_id: Option<String> = None;
37 let mut effective_role: Option<String> = None;
38 let mut effective_label: Option<String> = None;
39
40 match &record.instance {
41 crate::declarative::frame::ElementInstance::Semantics(props) => {
42 effective_role = Some(format!("{:?}", props.role));
43 effective_test_id = props.test_id.as_ref().map(ToString::to_string);
44 effective_label = props.label.as_ref().map(ToString::to_string);
45 }
46 crate::declarative::frame::ElementInstance::SemanticFlex(props) => {
47 effective_role = Some(format!("{:?}", props.role));
48 }
49 _ => {}
50 }
51
52 if let Some(decoration) = record.semantics_decoration.as_ref() {
53 let mut decoration_debug = serde_json::Map::new();
54 if let Some(test_id) = decoration.test_id.as_ref() {
55 decoration_debug.insert("test_id".to_string(), serde_json::json!(test_id.as_ref()));
56 effective_test_id = Some(test_id.to_string());
57 }
58 if let Some(role) = decoration.role {
59 let role = format!("{role:?}");
60 decoration_debug.insert("role".to_string(), serde_json::json!(role));
61 effective_role = Some(role);
62 }
63 if let Some(label_text) = decoration.label.as_ref() {
64 decoration_debug.insert("label".to_string(), serde_json::json!(label_text.as_ref()));
65 effective_label = Some(label_text.to_string());
66 }
67 if !decoration_debug.is_empty() {
68 debug.insert(
69 "semantics_decoration".to_string(),
70 serde_json::Value::Object(decoration_debug),
71 );
72 }
73 }
74
75 if let Some(test_id) = effective_test_id.as_ref() {
76 debug.insert("test_id".to_string(), serde_json::json!(test_id));
77 label.push_str(&format!(" [test_id={test_id}]"));
78 }
79 if let Some(role) = effective_role.as_ref() {
80 debug.insert("semantics_role".to_string(), serde_json::json!(role));
81 label.push_str(&format!(" [semantics_role={role}]"));
82 }
83 if let Some(label_text) = effective_label.as_ref() {
84 debug.insert("semantics_label".to_string(), serde_json::json!(label_text));
85 label.push_str(&format!(" [semantics_label={label_text}]"));
86 }
87 if let Some(key_context) = record.key_context.as_ref() {
88 debug.insert(
89 "key_context".to_string(),
90 serde_json::json!(key_context.as_ref()),
91 );
92 }
93
94 DebugDumpNodeInfo {
95 label: Some(label),
96 debug: Some(serde_json::Value::Object(debug)),
97 }
98}
99
100fn layout_debug_search_label<H: UiHost>(app: &mut H, window: AppWindowId, node: NodeId) -> String {
101 layout_debug_node_info(app, window, node)
102 .label
103 .unwrap_or_default()
104}
105
106#[cfg(not(target_arch = "wasm32"))]
107fn find_layout_debug_match_in_subtree<H: UiHost>(
108 tree: &UiTree<H>,
109 app: &mut H,
110 window: AppWindowId,
111 root: NodeId,
112 filter: &str,
113) -> Option<NodeId> {
114 let root_label = layout_debug_search_label(app, window, root);
115 if root_label.contains(filter) {
116 return Some(root);
117 }
118
119 let mut stack: Vec<NodeId> = vec![root];
120 let mut visited: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
121 while let Some(node) = stack.pop() {
122 if !visited.insert(node) {
123 continue;
124 }
125
126 let label = layout_debug_search_label(app, window, node);
127 if label.contains(filter) {
128 return Some(node);
129 }
130
131 if let Some(node) = tree.nodes.get(node) {
132 stack.extend(node.children.iter().copied());
133 }
134 }
135
136 None
137}
138
139impl<H: UiHost> UiTree<H> {
140 #[cfg(not(target_arch = "wasm32"))]
141 fn layout_sidecar_roots(
142 &self,
143 fallback_root: NodeId,
144 fallback_bounds: Rect,
145 ) -> Vec<LayoutSidecarRootRecord> {
146 let layer_roots: Vec<NodeId> = self
147 .visible_layers_in_paint_order()
148 .filter_map(|layer_id| self.layers.get(layer_id).map(|layer| layer.root))
149 .collect();
150
151 let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
152 let mut roots: Vec<LayoutSidecarRootRecord> = self
153 .visible_layers_in_paint_order()
154 .enumerate()
155 .filter_map(|(capture_index, layer_id)| {
156 let layer = self.layers.get(layer_id)?;
157 if !seen.insert(layer.root) {
158 return None;
159 }
160 let root_bounds = self
161 .nodes
162 .get(layer.root)
163 .map(|node| node.bounds)
164 .unwrap_or(fallback_bounds);
165 Some(LayoutSidecarRootRecord {
166 capture_index,
167 kind: "layer",
168 root: layer.root,
169 root_bounds,
170 blocks_underlay_input: layer.blocks_underlay_input,
171 blocks_underlay_focus: layer.blocks_underlay_focus,
172 hit_testable: layer.hit_testable,
173 })
174 })
175 .collect();
176
177 let viewport_bounds: std::collections::HashMap<NodeId, Rect> =
178 self.viewport_roots().iter().copied().collect();
179 for root in self.layout_engine.debug_independent_root_nodes() {
180 if !seen.insert(root) {
181 continue;
182 }
183 if !layer_roots.is_empty()
184 && !self.is_reachable_from_any_root_via_children(root, &layer_roots)
185 {
186 continue;
187 }
188
189 let root_bounds = viewport_bounds
190 .get(&root)
191 .copied()
192 .or_else(|| self.nodes.get(root).map(|node| node.bounds))
193 .unwrap_or(fallback_bounds);
194 let kind = if viewport_bounds.contains_key(&root) {
195 "viewport"
196 } else {
197 "independent"
198 };
199 roots.push(LayoutSidecarRootRecord {
200 capture_index: roots.len(),
201 kind,
202 root,
203 root_bounds,
204 blocks_underlay_input: false,
205 blocks_underlay_focus: false,
206 hit_testable: true,
207 });
208 }
209
210 if roots.is_empty() {
211 roots.push(LayoutSidecarRootRecord {
212 capture_index: 0,
213 kind: "fallback",
214 root: fallback_root,
215 root_bounds: fallback_bounds,
216 blocks_underlay_input: false,
217 blocks_underlay_focus: false,
218 hit_testable: true,
219 });
220 }
221
222 roots
223 }
224
225 pub(super) fn maybe_dump_taffy_subtree(
226 &self,
227 app: &mut H,
228 window: AppWindowId,
229 engine: &TaffyLayoutEngine,
230 root: NodeId,
231 root_bounds: Rect,
232 scale_factor: f32,
233 ) {
234 use std::sync::atomic::{AtomicU32, Ordering};
235
236 let config = crate::runtime_config::ui_runtime_config();
237 let Some(taffy_dump) = config.taffy_dump.as_ref() else {
238 return;
239 };
240
241 static DUMP_COUNT: AtomicU32 = AtomicU32::new(0);
242 let dump_max: Option<u32> = if config.taffy_dump_once {
243 Some(1)
244 } else {
245 taffy_dump.max
246 };
247 if let Some(max) = dump_max {
248 let prev = DUMP_COUNT.fetch_add(1, Ordering::SeqCst);
249 if prev >= max {
250 return;
251 }
252 }
253
254 if let Some(filter) = taffy_dump.root_filter.as_ref()
255 && !format!("{root:?}").contains(filter)
256 {
257 return;
258 }
259
260 let dump_root = if let Some(filter) = taffy_dump.root_label_filter.as_ref() {
263 let root_label = layout_debug_search_label(app, window, root);
264 if root_label.contains(filter) {
265 root
266 } else {
267 let mut stack: Vec<NodeId> = vec![root];
268 let mut visited: std::collections::HashSet<NodeId> =
269 std::collections::HashSet::new();
270 let mut found: Option<NodeId> = None;
271 while let Some(node) = stack.pop() {
272 if !visited.insert(node) {
273 continue;
274 }
275
276 let label = layout_debug_search_label(app, window, node);
277 if label.contains(filter) {
278 found = Some(node);
279 break;
280 }
281
282 if let Some(node) = self.nodes.get(node) {
283 stack.extend(node.children.iter().copied());
284 }
285 }
286
287 let Some(found) = found else {
288 return;
289 };
290
291 found
292 }
293 } else {
294 root
295 };
296
297 let out_dir = taffy_dump.out_dir.clone();
298
299 let frame = app.frame_id().0;
300 let root_slug: String = format!("{dump_root:?}")
301 .chars()
302 .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
303 .collect();
304 let filename = format!("taffy_{frame}_{root_slug}.json");
305
306 let dump = engine.debug_dump_subtree_json_with_info(dump_root, |node| {
307 layout_debug_node_info(app, window, node)
308 });
309
310 let wrapped = serde_json::json!({
311 "meta": {
312 "window": format!("{window:?}"),
313 "root_bounds": {
314 "x": root_bounds.origin.x.0,
315 "y": root_bounds.origin.y.0,
316 "w": root_bounds.size.width.0,
317 "h": root_bounds.size.height.0,
318 },
319 "scale_factor": scale_factor,
320 },
321 "taffy": dump,
322 });
323
324 let result = std::fs::create_dir_all(&out_dir)
325 .and_then(|_| {
326 serde_json::to_vec_pretty(&wrapped)
327 .map_err(|e| std::io::Error::other(format!("serialize: {e}")))
328 })
329 .and_then(|bytes| {
330 std::fs::write(std::path::Path::new(&out_dir).join(&filename), bytes)
331 });
332
333 match result {
334 Ok(()) => tracing::info!(
335 out_dir = %out_dir,
336 filename = %filename,
337 "wrote taffy debug dump"
338 ),
339 Err(err) => tracing::warn!(
340 error = %err,
341 out_dir = %out_dir,
342 filename = %filename,
343 "failed to write taffy debug dump"
344 ),
345 }
346 }
347
348 #[cfg(not(target_arch = "wasm32"))]
358 #[allow(clippy::too_many_arguments)]
359 pub fn debug_write_taffy_subtree_json(
360 &self,
361 app: &mut H,
362 window: AppWindowId,
363 root: NodeId,
364 root_bounds: Rect,
365 scale_factor: f32,
366 root_label_filter: Option<&str>,
367 out_dir: impl AsRef<std::path::Path>,
368 filename_tag: &str,
369 ) -> std::io::Result<std::path::PathBuf> {
370 fn sanitize_for_filename(s: &str) -> String {
371 s.chars()
372 .map(|ch| match ch {
373 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
374 _ => '_',
375 })
376 .collect()
377 }
378
379 let dump_root = if let Some(filter) = root_label_filter {
380 let root_label = layout_debug_search_label(app, window, root);
381 if root_label.contains(filter) {
382 root
383 } else {
384 let mut stack: Vec<NodeId> = vec![root];
385 let mut visited: std::collections::HashSet<NodeId> =
386 std::collections::HashSet::new();
387 let mut found: Option<NodeId> = None;
388 while let Some(node) = stack.pop() {
389 if !visited.insert(node) {
390 continue;
391 }
392
393 let label = layout_debug_search_label(app, window, node);
394 if label.contains(filter) {
395 found = Some(node);
396 break;
397 }
398
399 if let Some(node) = self.nodes.get(node) {
400 stack.extend(node.children.iter().copied());
401 }
402 }
403
404 found.unwrap_or(root)
405 }
406 } else {
407 root
408 };
409
410 let tag = sanitize_for_filename(filename_tag);
411 let frame = app.frame_id().0;
412 let root_slug = sanitize_for_filename(&format!("{dump_root:?}"));
413 let filename = if tag.is_empty() {
414 format!("taffy_{frame}_{root_slug}.json")
415 } else {
416 format!("taffy_{frame}_{tag}_{root_slug}.json")
417 };
418
419 let dump = self
420 .layout_engine
421 .debug_dump_subtree_json_with_info(dump_root, |node| {
422 layout_debug_node_info(app, window, node)
423 });
424
425 let wrapped = serde_json::json!({
426 "meta": {
427 "window": format!("{window:?}"),
428 "root_bounds": {
429 "x": root_bounds.origin.x.0,
430 "y": root_bounds.origin.y.0,
431 "w": root_bounds.size.width.0,
432 "h": root_bounds.size.height.0,
433 },
434 "scale_factor": scale_factor,
435 },
436 "taffy": dump,
437 });
438
439 let out_dir = out_dir.as_ref();
440 std::fs::create_dir_all(out_dir)?;
441 let path = out_dir.join(filename);
442 let bytes = serde_json::to_vec_pretty(&wrapped)
443 .map_err(|e| std::io::Error::other(format!("serialize: {e}")))?;
444 std::fs::write(&path, bytes)?;
445 Ok(path)
446 }
447
448 #[cfg(not(target_arch = "wasm32"))]
455 #[allow(clippy::too_many_arguments)]
456 pub fn debug_write_layout_sidecar_taffy_v1_json(
457 &self,
458 app: &mut H,
459 window: AppWindowId,
460 root: NodeId,
461 root_bounds: Rect,
462 scale_factor: f32,
463 root_label_filter: Option<&str>,
464 out_dir: impl AsRef<std::path::Path>,
465 captured_at_unix_ms: u64,
466 ) -> std::io::Result<std::path::PathBuf> {
467 let sidecar_roots = self.layout_sidecar_roots(root, root_bounds);
468 let dump_root = if let Some(filter) = root_label_filter {
469 sidecar_roots
470 .iter()
471 .rev()
472 .find_map(|root_record| {
473 find_layout_debug_match_in_subtree(self, app, window, root_record.root, filter)
474 })
475 .unwrap_or(root)
476 } else {
477 root
478 };
479
480 let mut dump = self
481 .layout_engine
482 .debug_dump_subtree_json_with_info(dump_root, |node| {
483 layout_debug_node_info(app, window, node)
484 });
485 let root_dumps = sidecar_roots
486 .iter()
487 .map(|root_record| {
488 serde_json::json!({
489 "capture_index": root_record.capture_index,
490 "kind": root_record.kind,
491 "root": format!("{:?}", root_record.root),
492 "root_bounds": {
493 "x": root_record.root_bounds.origin.x.0,
494 "y": root_record.root_bounds.origin.y.0,
495 "w": root_record.root_bounds.size.width.0,
496 "h": root_record.root_bounds.size.height.0,
497 },
498 "blocks_underlay_input": root_record.blocks_underlay_input,
499 "blocks_underlay_focus": root_record.blocks_underlay_focus,
500 "hit_testable": root_record.hit_testable,
501 "dump": self.layout_engine.debug_dump_subtree_json_with_info(
502 root_record.root,
503 |node| layout_debug_node_info(app, window, node),
504 ),
505 })
506 })
507 .collect::<Vec<_>>();
508 if let Some(dump_obj) = dump.as_object_mut() {
509 dump_obj.insert("roots".to_string(), serde_json::Value::Array(root_dumps));
510 }
511
512 let wrapped = serde_json::json!({
513 "schema_version": "v1",
514 "engine": "taffy",
515 "captured_at_unix_ms": captured_at_unix_ms,
516 "clip": {
517 "max_nodes": 0u64,
518 "max_bytes": 0u64,
519 "clipped_nodes": 0u64,
520 "clipped_bytes": 0u64,
521 },
522 "meta": {
523 "window": format!("{window:?}"),
524 "root_bounds": {
525 "x": root_bounds.origin.x.0,
526 "y": root_bounds.origin.y.0,
527 "w": root_bounds.size.width.0,
528 "h": root_bounds.size.height.0,
529 },
530 "scale_factor": scale_factor,
531 "root_label_filter": root_label_filter,
532 "captured_root_count": sidecar_roots.len(),
533 "visible_layer_root_count": self.visible_layers_in_paint_order().count(),
534 },
535 "taffy": dump,
536 });
537
538 let out_dir = out_dir.as_ref();
539 std::fs::create_dir_all(out_dir)?;
540 let path = out_dir.join("layout.taffy.v1.json");
541 let bytes = serde_json::to_vec(&wrapped)
542 .map_err(|e| std::io::Error::other(format!("serialize: {e}")))?;
543 std::fs::write(&path, bytes)?;
544 Ok(path)
545 }
546}