1use std::collections::HashSet;
16
17use gpui::prelude::*;
18use gpui::{div, px, AnyElement, Context, Hsla, ScrollHandle, SharedString, Window};
19
20use super::probe::{ProbeNode, ProbeTree};
21use super::shell::{
22 cell, disclosure, elide, empty_state, filter_pill, glyph, hairline, hairline_v, kv_row,
23 section_header, Ink, LABEL_SIZE, MONO_SIZE, ROW_HEIGHT, SIDEBAR_WIDTH,
24};
25use super::styles::{box_model, declarations, BoxModel, Declaration};
26use super::DevTools;
27use crate::icon::IconName;
28use crate::style::MONO_FAMILY;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum ElementsSidebar {
33 #[default]
34 Styles,
35 Computed,
36 Node,
37}
38
39impl ElementsSidebar {
40 fn label(self) -> &'static str {
41 match self {
42 ElementsSidebar::Styles => "Styles",
43 ElementsSidebar::Computed => "Computed",
44 ElementsSidebar::Node => "Node",
45 }
46 }
47
48 const ALL: [ElementsSidebar; 3] = [
49 ElementsSidebar::Styles,
50 ElementsSidebar::Computed,
51 ElementsSidebar::Node,
52 ];
53}
54
55#[derive(Default)]
57pub struct ElementsPanel {
58 pub selected: Option<SharedString>,
60 collapsed: HashSet<SharedString>,
63 pub(crate) sidebar: ElementsSidebar,
64 scroll: ScrollHandle,
68}
69
70impl ElementsPanel {
71 pub fn select(&mut self, key: impl Into<SharedString>) {
72 self.selected = Some(key.into());
73 }
74
75 pub fn toggle(&mut self, key: &SharedString) {
76 if !self.collapsed.remove(key) {
77 self.collapsed.insert(key.clone());
78 }
79 }
80
81 fn is_collapsed(&self, key: &SharedString) -> bool {
82 self.collapsed.contains(key)
83 }
84
85 pub fn reveal(&mut self, tree: &ProbeTree, key: &SharedString) {
88 if let Some(index) = tree.find(key) {
89 for ancestor in tree.ancestry(index) {
90 self.collapsed.remove(&tree.nodes[ancestor].key);
91 }
92 }
93 self.selected = Some(key.clone());
94 if let Some(row) = self
97 .rows(tree)
98 .iter()
99 .position(|index| tree.get(*index).map(|node| &node.key) == Some(key))
100 {
101 self.scroll.scroll_to_top_of_item(row);
102 }
103 }
104
105 pub fn selected_node<'a>(&self, tree: &'a ProbeTree) -> Option<&'a ProbeNode> {
109 let key = self.selected.as_ref()?;
110 tree.find(key).and_then(|index| tree.get(index))
111 }
112
113 fn rows(&self, tree: &ProbeTree) -> Vec<usize> {
118 let mut rows = Vec::with_capacity(tree.len());
119 for root in &tree.roots {
120 self.push_rows(tree, *root, &mut rows);
121 }
122 rows
123 }
124
125 fn push_rows(&self, tree: &ProbeTree, index: usize, rows: &mut Vec<usize>) {
126 let Some(node) = tree.get(index) else {
127 return;
128 };
129 rows.push(index);
130 if node.is_leaf() || self.is_collapsed(&node.key) {
131 return;
132 }
133 for child in &node.children {
134 self.push_rows(tree, *child, rows);
135 }
136 }
137
138 pub fn render(
139 &self,
140 tree: &ProbeTree,
141 window: &mut Window,
142 cx: &mut Context<DevTools>,
143 ) -> AnyElement {
144 let ink = Ink::read(cx);
145
146 if tree.is_empty() {
147 return div()
148 .flex()
149 .flex_1()
150 .min_h(px(0.0))
151 .child(empty_state(
152 "No elements recorded. Components report themselves while the inspector is open.",
153 &ink,
154 ))
155 .into_any_element();
156 }
157
158 div()
159 .flex()
160 .flex_1()
161 .min_h(px(0.0))
162 .w_full()
163 .child(self.tree_column(tree, &ink, cx))
164 .child(hairline_v(&ink))
165 .child(self.sidebar_column(tree, &ink, window, cx))
166 .into_any_element()
167 }
168
169 fn tree_column(&self, tree: &ProbeTree, ink: &Ink, cx: &mut Context<DevTools>) -> AnyElement {
171 let rows = self.rows(tree);
172 let mut list = div()
173 .id("devtools-elements-tree")
174 .track_scroll(&self.scroll)
175 .flex()
176 .flex_col()
177 .flex_1()
178 .min_h(px(0.0))
179 .w_full()
180 .overflow_scroll()
181 .bg(ink.content)
182 .font_family(MONO_FAMILY)
183 .text_size(px(MONO_SIZE));
184
185 for index in rows {
186 let Some(node) = tree.get(index) else {
187 continue;
188 };
189 list = list.child(self.row(node, index, ink, cx));
190 }
191
192 div()
193 .flex()
194 .flex_col()
195 .flex_1()
196 .min_w(px(0.0))
197 .h_full()
198 .child(list)
199 .child(hairline(ink))
200 .child(self.breadcrumbs(tree, ink, cx))
201 .into_any_element()
202 }
203
204 fn row(
206 &self,
207 node: &ProbeNode,
208 index: usize,
209 ink: &Ink,
210 cx: &mut Context<DevTools>,
211 ) -> AnyElement {
212 let selected = self.selected.as_ref() == Some(&node.key);
213 let expandable = !node.is_leaf();
214 let expanded = !self.is_collapsed(&node.key);
215 let indent = node.depth as f32 * 12.0 + 6.0;
216 let text_color = if selected {
217 ink.selected_text
218 } else {
219 ink.text
220 };
221 let punct = if selected {
222 ink.selected_text
223 } else {
224 ink.punct
225 };
226 let tag_color = if selected { ink.selected_text } else { ink.tag };
227 let hover_bg = ink.hover;
228
229 let key_for_click = node.key.clone();
230 let key_for_toggle = node.key.clone();
231
232 let mut markup = div()
236 .flex()
237 .items_center()
238 .gap(px(0.0))
239 .flex_1()
240 .min_w(px(0.0))
241 .overflow_hidden()
242 .whitespace_nowrap()
243 .child(
244 div()
245 .flex_none()
246 .text_color(tag_color)
247 .child(node.name.clone()),
248 );
249
250 for (position, (name, value)) in node.attrs.iter().enumerate() {
251 markup = markup.child(
252 div()
253 .flex()
254 .flex_none()
255 .child(
256 div()
257 .text_color(punct)
258 .child(SharedString::new_static(if position == 0 {
259 " "
260 } else {
261 ", "
262 })),
263 )
264 .child(
265 div()
266 .text_color(if selected {
267 ink.selected_text
268 } else {
269 ink.attr
270 })
271 .child(name.clone()),
272 )
273 .when(!value.is_empty(), |el| {
274 el.child(
275 div()
276 .text_color(punct)
277 .child(SharedString::new_static(": ")),
278 )
279 .child(
280 div()
281 .text_color(if selected {
282 ink.selected_text
283 } else {
284 ink.value
285 })
286 .child(value.clone()),
287 )
288 }),
289 );
290 }
291
292 div()
293 .id(("devtools-element-row", index))
294 .flex()
295 .items_center()
296 .flex_none()
297 .h(px(ROW_HEIGHT))
298 .w_full()
299 .pl(px(indent))
300 .pr(px(6.0))
301 .text_color(text_color)
302 .when(selected, |el| el.bg(ink.selected))
303 .when(!selected, |el| el.hover(move |st| st.bg(hover_bg)))
304 .child(
305 div()
306 .id(("devtools-element-twisty", index))
307 .flex()
308 .flex_none()
309 .items_center()
310 .justify_center()
311 .w(px(14.0))
312 .h(px(ROW_HEIGHT))
313 .child(if expandable {
314 disclosure(Some(expanded), ink, cx)
315 } else {
316 disclosure(None, ink, cx)
317 })
318 .on_click(
319 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
320 this.elements.toggle(&key_for_toggle);
321 cx.notify();
322 }),
323 ),
324 )
325 .child(markup)
326 .on_click(
327 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
328 this.elements.select(key_for_click.clone());
329 cx.notify();
330 }),
331 )
332 .into_any_element()
333 }
334
335 fn breadcrumbs(&self, tree: &ProbeTree, ink: &Ink, cx: &mut Context<DevTools>) -> AnyElement {
337 let mut bar = div()
338 .flex()
339 .flex_none()
340 .items_center()
341 .h(px(22.0))
342 .w_full()
343 .px(px(6.0))
344 .gap(px(2.0))
345 .bg(ink.chrome)
346 .text_size(px(LABEL_SIZE))
347 .overflow_hidden();
348
349 let Some(index) = self.selected.as_ref().and_then(|key| tree.find(key)) else {
350 return bar
351 .child(
352 div()
353 .text_color(ink.dim)
354 .child(SharedString::new_static("Select an element")),
355 )
356 .into_any_element();
357 };
358
359 let chain = tree.ancestry(index);
360 let last = chain.len().saturating_sub(1);
361 for (position, node_index) in chain.into_iter().enumerate() {
362 let Some(node) = tree.get(node_index) else {
363 continue;
364 };
365 if position > 0 {
366 bar = bar.child(
367 div()
368 .flex_none()
369 .text_color(ink.dim)
370 .child(SharedString::new_static("›")),
371 );
372 }
373 let key = node.key.clone();
374 let is_last = position == last;
375 let hover_bg = ink.hover;
376 bar = bar.child(
377 div()
378 .id(("devtools-crumb", node_index))
379 .flex()
380 .flex_none()
381 .items_center()
382 .h(px(17.0))
383 .px(px(5.0))
384 .rounded(px(4.0))
385 .text_color(if is_last { ink.text } else { ink.dim })
386 .hover(move |st| st.bg(hover_bg))
387 .child(node.name.clone())
388 .on_click(
389 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
390 this.elements.select(key.clone());
391 cx.notify();
392 }),
393 ),
394 );
395 }
396
397 bar.into_any_element()
398 }
399
400 fn sidebar_column(
401 &self,
402 tree: &ProbeTree,
403 ink: &Ink,
404 window: &mut Window,
405 cx: &mut Context<DevTools>,
406 ) -> AnyElement {
407 let mut tabs = div()
408 .flex()
409 .flex_none()
410 .items_center()
411 .gap(px(4.0))
412 .h(px(26.0))
413 .px(px(8.0))
414 .w_full()
415 .bg(ink.chrome)
416 .border_b_1()
417 .border_color(ink.border);
418
419 for tab in ElementsSidebar::ALL {
420 tabs = tabs.child(
421 filter_pill(
422 ("devtools-elements-sidebar", tab as usize),
423 tab.label(),
424 self.sidebar == tab,
425 ink,
426 )
427 .on_click(
428 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
429 this.elements.sidebar = tab;
430 cx.notify();
431 }),
432 ),
433 );
434 }
435
436 let body = match self.selected_node(tree) {
437 None => empty_state("No element selected", ink).into_any_element(),
438 Some(node) => match self.sidebar {
439 ElementsSidebar::Styles => styles_view(node, ink, cx),
440 ElementsSidebar::Computed => computed_view(node, ink, window, cx),
441 ElementsSidebar::Node => node_view(node, ink, cx),
442 },
443 };
444
445 div()
446 .flex()
447 .flex_col()
448 .flex_none()
449 .w(px(SIDEBAR_WIDTH))
450 .h_full()
451 .bg(ink.content)
452 .child(tabs)
453 .child(
454 div()
455 .id("devtools-elements-sidebar-body")
456 .flex()
457 .flex_col()
458 .flex_1()
459 .min_h(px(0.0))
460 .w_full()
461 .overflow_scroll()
462 .child(body),
463 )
464 .into_any_element()
465 }
466}
467
468fn declaration_row(declaration: &Declaration, ink: &Ink) -> AnyElement {
470 div()
471 .flex()
472 .items_start()
473 .w_full()
474 .pl(px(20.0))
475 .pr(px(8.0))
476 .py(px(1.0))
477 .font_family(MONO_FAMILY)
478 .text_size(px(MONO_SIZE))
479 .child(
480 div()
481 .flex_none()
482 .text_color(ink.property)
483 .child(declaration.property.clone()),
484 )
485 .child(
486 div()
487 .flex_none()
488 .text_color(ink.punct)
489 .child(SharedString::new_static(": ")),
490 )
491 .when_some(declaration.color, |el, color| {
492 el.child(
493 div()
494 .flex_none()
495 .w(px(9.0))
496 .h(px(9.0))
497 .mt(px(3.0))
498 .mr(px(4.0))
499 .rounded(px(2.0))
500 .border_1()
501 .border_color(ink.border)
502 .bg(color),
503 )
504 })
505 .child(
506 div()
507 .flex_1()
508 .text_color(ink.value)
509 .child(declaration.value.clone()),
510 )
511 .child(
512 div()
513 .flex_none()
514 .text_color(ink.punct)
515 .child(SharedString::new_static(";")),
516 )
517 .into_any_element()
518}
519
520fn styles_view(node: &ProbeNode, ink: &Ink, _cx: &mut Context<DevTools>) -> AnyElement {
522 let Some(style) = node.style.as_ref() else {
523 return empty_state("This element reported no style", ink).into_any_element();
524 };
525 let declarations = declarations(style);
526
527 let mut block = div()
528 .flex()
529 .flex_col()
530 .w_full()
531 .py(px(4.0))
532 .font_family(MONO_FAMILY)
533 .text_size(px(MONO_SIZE))
534 .child(
535 div()
536 .flex()
537 .items_center()
538 .justify_between()
539 .w_full()
540 .px(px(8.0))
541 .child(
542 div()
543 .flex()
544 .child(
545 div()
546 .text_color(ink.tag)
547 .child(SharedString::from(node.name.to_string())),
548 )
549 .child(
550 div()
551 .text_color(ink.punct)
552 .child(SharedString::new_static(" {")),
553 ),
554 )
555 .when_some(node.source.as_ref(), |el, source| {
556 el.child(
557 div()
558 .text_size(px(LABEL_SIZE))
559 .text_color(ink.dim)
560 .child(SharedString::from(source.short())),
561 )
562 }),
563 );
564
565 if declarations.is_empty() {
566 block = block.child(
567 div()
568 .pl(px(20.0))
569 .text_color(ink.dim)
570 .child(SharedString::new_static("/* no declarations */")),
571 );
572 }
573 for declaration in &declarations {
574 block = block.child(declaration_row(declaration, ink));
575 }
576
577 block = block.child(
578 div()
579 .px(px(8.0))
580 .text_color(ink.punct)
581 .child(SharedString::new_static("}")),
582 );
583
584 div()
585 .flex()
586 .flex_col()
587 .w_full()
588 .child(section_header(
589 SharedString::from(format!("{} — {} rules", node.name, 1)),
590 ink,
591 ))
592 .child(block)
593 .into_any_element()
594}
595
596fn computed_view(
599 node: &ProbeNode,
600 ink: &Ink,
601 window: &mut Window,
602 _cx: &mut Context<DevTools>,
603) -> AnyElement {
604 let rem_size = window.rem_size();
605 let model = node
606 .style
607 .as_ref()
608 .map(|style| box_model(style, node.bounds.size, rem_size))
609 .unwrap_or(BoxModel {
610 width: f32::from(node.bounds.size.width),
611 height: f32::from(node.bounds.size.height),
612 ..BoxModel::default()
613 });
614
615 let mut sorted = node
616 .style
617 .as_ref()
618 .map(|style| declarations(style))
619 .unwrap_or_default();
620 sorted.sort_by(|a, b| a.property.cmp(&b.property));
621
622 let mut properties = div().flex().flex_col().w_full().pb(px(6.0));
623 for declaration in &sorted {
624 properties = properties.child(declaration_row(declaration, ink));
625 }
626
627 div()
628 .flex()
629 .flex_col()
630 .w_full()
631 .child(section_header("Box Model", ink))
632 .child(box_model_view(&model, ink))
633 .child(section_header("Properties", ink))
634 .child(properties)
635 .into_any_element()
636}
637
638fn box_model_view(model: &BoxModel, ink: &Ink) -> AnyElement {
641 let (content_width, content_height) = model.content();
642
643 let band = |label: &'static str,
644 color: Hsla,
645 top: f32,
646 right: f32,
647 bottom: f32,
648 left: f32,
649 inner: AnyElement| {
650 div()
651 .flex()
652 .flex_col()
653 .items_center()
654 .w_full()
655 .bg(color)
656 .border_1()
657 .border_color(ink.border)
658 .child(
659 div()
660 .flex()
661 .items_center()
662 .justify_between()
663 .w_full()
664 .px(px(4.0))
665 .child(
666 div()
669 .text_size(px(9.0))
670 .text_color(ink.text)
671 .child(SharedString::new_static(label)),
672 )
673 .child(
674 div()
675 .text_size(px(9.0))
676 .text_color(ink.text)
677 .child(SharedString::from(number(top))),
678 ),
679 )
680 .child(
681 div()
682 .flex()
683 .items_center()
684 .justify_between()
685 .w_full()
686 .gap(px(4.0))
687 .px(px(4.0))
688 .child(
689 div()
690 .flex_none()
691 .text_size(px(9.0))
692 .text_color(ink.text)
693 .child(SharedString::from(number(left))),
694 )
695 .child(inner)
696 .child(
697 div()
698 .flex_none()
699 .text_size(px(9.0))
700 .text_color(ink.text)
701 .child(SharedString::from(number(right))),
702 ),
703 )
704 .child(
705 div().flex().justify_center().w_full().child(
706 div()
707 .text_size(px(9.0))
708 .text_color(ink.text)
709 .child(SharedString::from(number(bottom))),
710 ),
711 )
712 };
713
714 let content = div()
715 .flex()
716 .flex_1()
717 .items_center()
718 .justify_center()
719 .h(px(30.0))
720 .bg(ink.box_content)
721 .border_1()
722 .border_color(ink.border)
723 .text_size(px(10.0))
724 .text_color(ink.text)
725 .child(SharedString::from(format!(
726 "{} × {}",
727 number(content_width),
728 number(content_height)
729 )))
730 .into_any_element();
731
732 let padding = band(
733 "padding",
734 ink.box_padding,
735 model.padding.top,
736 model.padding.right,
737 model.padding.bottom,
738 model.padding.left,
739 content,
740 )
741 .into_any_element();
742
743 let border = band(
744 "border",
745 ink.box_border,
746 model.border.top,
747 model.border.right,
748 model.border.bottom,
749 model.border.left,
750 padding,
751 )
752 .into_any_element();
753
754 let margin = band(
755 "margin",
756 ink.box_margin,
757 model.margin.top,
758 model.margin.right,
759 model.margin.bottom,
760 model.margin.left,
761 border,
762 );
763
764 div()
765 .flex()
766 .flex_col()
767 .w_full()
768 .p(px(10.0))
769 .font_family(MONO_FAMILY)
770 .child(margin)
771 .into_any_element()
772}
773
774fn number(value: f32) -> String {
776 if (value - value.round()).abs() < 0.01 {
777 format!("{}", value.round() as i64)
778 } else {
779 format!("{value:.2}")
780 }
781}
782
783fn node_view(node: &ProbeNode, ink: &Ink, cx: &mut Context<DevTools>) -> AnyElement {
785 let mut identity = div().flex().flex_col().w_full().py(px(4.0));
786 identity = identity.child(kv_row("Component", node.name.clone(), ink));
787 identity = identity.child(kv_row("Path", node.key.clone(), ink));
788 if let Some(id) = &node.element_id {
789 identity = identity.child(kv_row("Element ID", id.clone(), ink));
790 }
791 if let Some(source) = &node.source {
792 let target = source.clone();
793 identity = identity.child(
794 div()
795 .id("devtools-node-source")
796 .child(kv_row(
797 "Source",
798 SharedString::from(elide(source.file.as_ref(), 34) + &format!(":{}", source.line)),
799 ink,
800 ))
801 .on_click(
802 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
803 this.reveal_source(target.clone(), cx);
804 }),
805 ),
806 );
807 }
808
809 let bounds = node.bounds;
810 let mut geometry = div().flex().flex_col().w_full().py(px(4.0));
811 geometry = geometry.child(kv_row(
812 "Position",
813 SharedString::from(format!(
814 "{}, {}",
815 number(f32::from(bounds.origin.x)),
816 number(f32::from(bounds.origin.y))
817 )),
818 ink,
819 ));
820 geometry = geometry.child(kv_row(
821 "Size",
822 SharedString::from(format!(
823 "{} × {}",
824 number(f32::from(bounds.size.width)),
825 number(f32::from(bounds.size.height))
826 )),
827 ink,
828 ));
829 geometry = geometry.child(kv_row(
830 "Depth",
831 SharedString::from(node.depth.to_string()),
832 ink,
833 ));
834
835 let mut attributes = div().flex().flex_col().w_full().py(px(4.0));
836 if node.attrs.is_empty() {
837 attributes = attributes.child(
838 div()
839 .px(px(8.0))
840 .py(px(2.0))
841 .text_size(px(LABEL_SIZE))
842 .text_color(ink.dim)
843 .child(SharedString::new_static("None reported")),
844 );
845 }
846 for (name, value) in &node.attrs {
847 attributes = attributes.child(kv_row(
848 name.clone(),
849 if value.is_empty() {
850 SharedString::new_static("true")
851 } else {
852 value.clone()
853 },
854 ink,
855 ));
856 }
857
858 div()
859 .flex()
860 .flex_col()
861 .w_full()
862 .child(section_header("Identity", ink))
863 .child(identity)
864 .child(section_header("Geometry", ink))
865 .child(geometry)
866 .child(section_header("Attributes", ink))
867 .child(attributes)
868 .into_any_element()
869}
870
871pub fn layers_view(
875 tree: &ProbeTree,
876 selected: Option<&SharedString>,
877 ink: &Ink,
878 cx: &mut Context<DevTools>,
879) -> AnyElement {
880 if tree.is_empty() {
881 return empty_state("No elements recorded", ink).into_any_element();
882 }
883
884 let mut rows: Vec<&ProbeNode> = tree.nodes.iter().collect();
885 rows.sort_by(|a, b| {
886 let area_a = f32::from(a.bounds.size.width) * f32::from(a.bounds.size.height);
887 let area_b = f32::from(b.bounds.size.width) * f32::from(b.bounds.size.height);
888 area_b
889 .partial_cmp(&area_a)
890 .unwrap_or(std::cmp::Ordering::Equal)
891 });
892
893 let mut list = div()
894 .id("devtools-layers")
895 .flex()
896 .flex_col()
897 .flex_1()
898 .min_h(px(0.0))
899 .w_full()
900 .overflow_scroll()
901 .bg(ink.content)
902 .font_family(MONO_FAMILY)
903 .text_size(px(MONO_SIZE));
904
905 list = list.child(
906 div()
907 .flex()
908 .flex_none()
909 .items_center()
910 .h(px(20.0))
911 .w_full()
912 .bg(ink.chrome)
913 .border_b_1()
914 .border_color(ink.border)
915 .child(cell("Layer", None, ink.dim))
916 .child(cell("Depth", Some(52.0), ink.dim))
917 .child(cell("Position", Some(90.0), ink.dim))
918 .child(cell("Size", Some(90.0), ink.dim))
919 .child(cell("Area", Some(78.0), ink.dim)),
920 );
921
922 for (position, node) in rows.iter().enumerate() {
923 let is_selected = selected == Some(&node.key);
924 let key = node.key.clone();
925 let hover_bg = ink.hover;
926 let area = f32::from(node.bounds.size.width) * f32::from(node.bounds.size.height);
927 let text = if is_selected {
928 ink.selected_text
929 } else {
930 ink.text
931 };
932 let dim = if is_selected {
933 ink.selected_text
934 } else {
935 ink.dim
936 };
937
938 list = list.child(
939 div()
940 .id(("devtools-layer-row", position))
941 .flex()
942 .items_center()
943 .flex_none()
944 .h(px(ROW_HEIGHT))
945 .w_full()
946 .when(is_selected, |el| el.bg(ink.selected))
947 .when(!is_selected && position % 2 == 1, |el| el.bg(ink.stripe))
948 .when(!is_selected, |el| el.hover(move |st| st.bg(hover_bg)))
949 .child(
950 div()
951 .flex()
952 .items_center()
953 .flex_1()
954 .min_w(px(0.0))
955 .h_full()
956 .px(px(6.0))
957 .gap(px(5.0))
958 .child(glyph(IconName::Layers, 11.0, dim, cx))
959 .child(
960 div()
961 .overflow_hidden()
962 .whitespace_nowrap()
963 .text_color(text)
964 .child(node.name.clone()),
965 ),
966 )
967 .child(cell(node.depth.to_string(), Some(52.0), dim))
968 .child(cell(
969 format!(
970 "{}, {}",
971 number(f32::from(node.bounds.origin.x)),
972 number(f32::from(node.bounds.origin.y))
973 ),
974 Some(90.0),
975 dim,
976 ))
977 .child(cell(
978 format!(
979 "{} × {}",
980 number(f32::from(node.bounds.size.width)),
981 number(f32::from(node.bounds.size.height))
982 ),
983 Some(90.0),
984 dim,
985 ))
986 .child(cell(
988 format!("{} px²", area.round() as i64),
989 Some(84.0),
990 dim,
991 ))
992 .on_click(
993 cx.listener(move |this: &mut DevTools, _event, _window, cx| {
994 this.elements.select(key.clone());
995 cx.notify();
996 }),
997 ),
998 );
999 }
1000
1001 list.into_any_element()
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007 use crate::devtools::probe;
1008
1009 fn tree_of(build: impl FnOnce()) -> ProbeTree {
1010 probe::set_enabled(false);
1011 probe::set_enabled(true);
1012 build();
1013 probe::begin_frame_unclaimed();
1014 probe::tree()
1015 }
1016
1017 fn node(name: &'static str, children: impl FnOnce()) {
1019 probe::test_record(name, children);
1020 }
1021
1022 #[test]
1025 fn every_node_produces_exactly_one_row() {
1026 let panel = ElementsPanel::default();
1027 let tree = tree_of(|| {
1028 node("Stack", || {
1029 node("Button", || {});
1030 });
1031 });
1032
1033 assert_eq!(panel.rows(&tree), vec![0, 1]);
1034 }
1035
1036 #[test]
1037 fn collapsing_hides_children() {
1038 let mut panel = ElementsPanel::default();
1039 let tree = tree_of(|| {
1040 node("Stack", || {
1041 node("Button", || {});
1042 node("Badge", || {});
1043 });
1044 });
1045
1046 panel.toggle(&tree.nodes[0].key.clone());
1047 assert_eq!(panel.rows(&tree), vec![0]);
1048 }
1049
1050 #[test]
1051 fn toggling_twice_restores_the_children() {
1052 let mut panel = ElementsPanel::default();
1053 let tree = tree_of(|| node("Stack", || node("Button", || {})));
1054 let key = tree.nodes[0].key.clone();
1055
1056 panel.toggle(&key);
1057 panel.toggle(&key);
1058 assert_eq!(panel.rows(&tree), vec![0, 1]);
1059 }
1060
1061 #[test]
1062 fn revealing_expands_every_ancestor() {
1063 let mut panel = ElementsPanel::default();
1064 let tree = tree_of(|| node("AppShell", || node("Stack", || node("Button", || {}))));
1065 let leaf = tree.nodes[2].key.clone();
1066
1067 panel.toggle(&tree.nodes[0].key.clone());
1068 panel.toggle(&tree.nodes[1].key.clone());
1069 panel.reveal(&tree, &leaf);
1070
1071 assert_eq!(panel.selected.as_ref(), Some(&leaf));
1072 assert!(panel.rows(&tree).contains(&2));
1073 }
1074
1075 #[test]
1076 fn a_selection_that_left_the_tree_resolves_to_nothing() {
1077 let mut panel = ElementsPanel::default();
1078 let tree = tree_of(|| node("Stack", || node("Menu", || {})));
1079 panel.select(tree.nodes[1].key.clone());
1080 assert!(panel.selected_node(&tree).is_some());
1081
1082 let without_menu = tree_of(|| node("Stack", || {}));
1083 assert!(panel.selected_node(&without_menu).is_none());
1084 }
1085}