1use std::cell::RefCell;
14use std::rc::Rc;
15use std::sync::Arc;
16
17use crate::color::Color;
18use crate::draw_ctx::DrawCtx;
19use crate::event::{Event, EventResult, MouseButton};
20use crate::geometry::{Point, Rect, Size};
21use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
22use crate::text::Font;
23use crate::widget::{InspectorNode, Widget};
24use crate::widgets::tree_view::{NodeIcon, TreeNode, TreeView};
25
26struct InternalPresenceNode {
42 bounds: Rect,
43 children: Vec<Box<dyn Widget>>,
44 base: WidgetBase,
45 name: &'static str,
46}
47
48impl Widget for InternalPresenceNode {
49 fn type_name(&self) -> &'static str {
50 self.name
51 }
52 fn bounds(&self) -> Rect {
53 self.bounds
54 }
55 fn set_bounds(&mut self, b: Rect) {
56 self.bounds = b;
57 }
58 fn children(&self) -> &[Box<dyn Widget>] {
59 &self.children
60 }
61 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
62 &mut self.children
63 }
64 fn margin(&self) -> Insets {
65 self.base.margin
66 }
67 fn h_anchor(&self) -> HAnchor {
68 self.base.h_anchor
69 }
70 fn v_anchor(&self) -> VAnchor {
71 self.base.v_anchor
72 }
73 fn min_size(&self) -> Size {
74 self.base.min_size
75 }
76 fn max_size(&self) -> Size {
77 self.base.max_size
78 }
79 fn layout(&mut self, _: Size) -> Size {
80 Size::new(self.bounds.width, self.bounds.height)
81 }
82 fn paint(&mut self, _: &mut dyn DrawCtx) {}
83 fn hit_test(&self, _: Point) -> bool {
84 false
85 }
86 fn on_event(&mut self, _: &Event) -> EventResult {
87 EventResult::Ignored
88 }
89 fn contributes_children_to_inspector(&self) -> bool {
90 false
91 }
92}
93
94const DEFAULT_PROPS_H: f64 = 180.0;
96const FONT_SIZE: f64 = 12.0;
97const HEADER_H: f64 = 30.0;
98const SPLIT_HIT: f64 = 5.0;
99const MIN_PROPS_H: f64 = 60.0;
100const MIN_TREE_H: f64 = 60.0;
101
102fn c_panel_bg(v: &crate::theme::Visuals) -> Color {
106 v.panel_fill
107}
108fn c_header_bg(v: &crate::theme::Visuals) -> Color {
109 let f = if is_dark(v) { 0.80 } else { 0.94 };
111 Color::rgba(
112 v.panel_fill.r * f,
113 v.panel_fill.g * f,
114 v.panel_fill.b * f,
115 1.0,
116 )
117}
118fn c_props_bg(v: &crate::theme::Visuals) -> Color {
119 v.window_fill
120}
121fn c_split_bg(v: &crate::theme::Visuals) -> Color {
122 let t = if is_dark(v) { 1.0 } else { 0.0 };
123 Color::rgba(t, t, t, 0.10)
124}
125fn c_border(v: &crate::theme::Visuals) -> Color {
126 v.separator
127}
128fn c_text(v: &crate::theme::Visuals) -> Color {
129 v.text_color
130}
131fn c_dim_text(v: &crate::theme::Visuals) -> Color {
132 v.text_dim
133}
134
135fn is_dark(v: &crate::theme::Visuals) -> bool {
136 let lum = 0.299 * v.panel_fill.r + 0.587 * v.panel_fill.g + 0.114 * v.panel_fill.b;
138 lum < 0.5
139}
140
141fn translate_event(event: &Event, offset_y: f64) -> Event {
146 match event {
147 Event::MouseDown {
148 pos,
149 button,
150 modifiers,
151 } => Event::MouseDown {
152 pos: Point::new(pos.x, pos.y - offset_y),
153 button: *button,
154 modifiers: *modifiers,
155 },
156 Event::MouseMove { pos } => Event::MouseMove {
157 pos: Point::new(pos.x, pos.y - offset_y),
158 },
159 Event::MouseUp {
160 pos,
161 button,
162 modifiers,
163 } => Event::MouseUp {
164 pos: Point::new(pos.x, pos.y - offset_y),
165 button: *button,
166 modifiers: *modifiers,
167 },
168 Event::MouseWheel {
169 pos,
170 delta_y,
171 delta_x,
172 modifiers,
173 } => Event::MouseWheel {
174 pos: Point::new(pos.x, pos.y - offset_y),
175 delta_y: *delta_y,
176 delta_x: *delta_x,
177 modifiers: *modifiers,
178 },
179 other => other.clone(),
180 }
181}
182
183pub struct InspectorPanel {
186 bounds: Rect,
187 _children: Vec<Box<dyn Widget>>,
191 base: WidgetBase,
192 font: Arc<Font>,
193 nodes: Rc<RefCell<Vec<InspectorNode>>>,
194 selected: Option<usize>,
196 props_h: f64,
197 split_dragging: bool,
198 pub hovered_bounds: Rc<RefCell<Option<Rect>>>,
200 pub(crate) tree_view: TreeView,
202 pending_expanded: Option<Vec<bool>>,
206 pending_selected: Option<Option<usize>>,
207 snapshot_out: Option<Rc<RefCell<Option<InspectorSavedState>>>>,
211}
212
213#[derive(Clone, Debug, Default)]
215pub struct InspectorSavedState {
216 pub expanded: Vec<bool>,
217 pub selected: Option<usize>,
218 pub props_h: f64,
219}
220
221impl InspectorPanel {
222 pub fn new(
223 font: Arc<Font>,
224 nodes: Rc<RefCell<Vec<InspectorNode>>>,
225 hovered_bounds: Rc<RefCell<Option<Rect>>>,
226 ) -> Self {
227 let tree_view = TreeView::new(Arc::clone(&font))
228 .with_row_height(20.0)
229 .with_font_size(12.0)
230 .with_indent_width(14.0)
231 .with_hover_repaint(false);
232 Self {
233 bounds: Rect::default(),
234 _children: vec![Box::new(InternalPresenceNode {
235 bounds: Rect::default(),
236 children: Vec::new(),
237 base: WidgetBase::new(),
238 name: "TreeView",
239 })],
240 base: WidgetBase::new(),
241 font,
242 nodes,
243 selected: None,
244 props_h: DEFAULT_PROPS_H,
245 split_dragging: false,
246 hovered_bounds,
247 tree_view,
248 pending_expanded: None,
249 pending_selected: None,
250 snapshot_out: None,
251 }
252 }
253
254 pub fn with_snapshot_cell(mut self, cell: Rc<RefCell<Option<InspectorSavedState>>>) -> Self {
258 self.snapshot_out = Some(cell);
259 self
260 }
261
262 pub fn saved_state(&self) -> InspectorSavedState {
273 InspectorSavedState {
274 expanded: self.tree_view.nodes.iter().map(|n| n.is_expanded).collect(),
275 selected: self.tree_view.nodes.iter().position(|n| n.is_selected),
276 props_h: self.props_h,
277 }
278 }
279
280 pub fn apply_saved_state(&mut self, s: InspectorSavedState) {
285 self.pending_expanded = Some(s.expanded);
286 self.pending_selected = Some(s.selected);
287 self.props_h = s.props_h.clamp(MIN_PROPS_H, 1024.0);
288 }
289
290 fn list_area_h(&self) -> f64 {
294 (self.bounds.height - HEADER_H).max(0.0)
295 }
296
297 fn split_y(&self) -> f64 {
299 self.props_h.clamp(
300 MIN_PROPS_H,
301 (self.list_area_h() - MIN_TREE_H).max(MIN_PROPS_H),
302 )
303 }
304
305 fn tree_origin_y(&self) -> f64 {
307 self.split_y() + 4.0
308 }
309
310 fn on_split_handle(&self, pos: Point) -> bool {
311 let sy = self.split_y();
312 pos.y >= sy - SPLIT_HIT && pos.y <= sy + SPLIT_HIT
313 }
314
315 fn pos_in_tree_area(&self, pos: Point) -> bool {
316 let tree_bot = self.tree_origin_y();
317 let tree_top = self.list_area_h();
318 pos.y >= tree_bot && pos.y <= tree_top
319 }
320
321 fn forward_to_tree(&mut self, event: &Event) -> EventResult {
323 let offset_y = self.tree_view.bounds().y;
327 let translated = translate_event(event, offset_y);
328 self.tree_view.on_event(&translated)
329 }
330
331 fn update_hovered_bounds_from_tree(&self) {
332 let nodes = self.nodes.borrow();
333 let next = self
334 .tree_view
335 .hovered_node_idx()
336 .and_then(|i| nodes.get(i))
337 .map(|n| n.screen_bounds);
338 let mut hovered = self.hovered_bounds.borrow_mut();
339 if *hovered != next {
340 *hovered = next;
341 crate::animation::request_draw_without_invalidation();
342 }
343 }
344}
345
346impl InspectorPanel {
349 pub fn with_margin(mut self, m: Insets) -> Self {
350 self.base.margin = m;
351 self
352 }
353 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
354 self.base.h_anchor = h;
355 self
356 }
357 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
358 self.base.v_anchor = v;
359 self
360 }
361 pub fn with_min_size(mut self, s: Size) -> Self {
362 self.base.min_size = s;
363 self
364 }
365 pub fn with_max_size(mut self, s: Size) -> Self {
366 self.base.max_size = s;
367 self
368 }
369}
370
371impl Widget for InspectorPanel {
372 fn type_name(&self) -> &'static str {
373 "InspectorPanel"
374 }
375 fn bounds(&self) -> Rect {
376 self.bounds
377 }
378 fn set_bounds(&mut self, b: Rect) {
379 self.bounds = b;
380 }
381 fn children(&self) -> &[Box<dyn Widget>] {
382 &self._children
383 }
384 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
385 &mut self._children
386 }
387
388 fn margin(&self) -> Insets {
389 self.base.margin
390 }
391 fn h_anchor(&self) -> HAnchor {
392 self.base.h_anchor
393 }
394 fn v_anchor(&self) -> VAnchor {
395 self.base.v_anchor
396 }
397 fn min_size(&self) -> Size {
398 self.base.min_size
399 }
400 fn max_size(&self) -> Size {
401 self.base.max_size
402 }
403
404 fn layout(&mut self, available: Size) -> Size {
405 self.bounds.width = available.width;
406 self.bounds.height = available.height;
407
408 let nodes = self.nodes.borrow();
409
410 let mut old_expanded: Vec<bool> =
415 self.tree_view.nodes.iter().map(|n| n.is_expanded).collect();
416 let mut old_selected: Vec<bool> =
417 self.tree_view.nodes.iter().map(|n| n.is_selected).collect();
418 if let Some(pe) = self.pending_expanded.take() {
419 old_expanded = pe;
420 }
421 if let Some(ps) = self.pending_selected.take() {
422 old_selected = vec![false; old_expanded.len().max(ps.map(|i| i + 1).unwrap_or(0))];
423 if let Some(i) = ps {
424 if i < old_selected.len() {
425 old_selected[i] = true;
426 }
427 }
428 }
429
430 self.tree_view.nodes.clear();
431
432 let mut depth_stack: Vec<usize> = Vec::new();
436 let mut per_parent_counts: std::collections::HashMap<Option<usize>, u32> =
437 std::collections::HashMap::new();
438
439 for (orig_idx, node) in nodes.iter().enumerate() {
440 let parent = if node.depth == 0 {
441 None
442 } else {
443 depth_stack.get(node.depth.saturating_sub(1)).copied()
444 };
445
446 let order = {
447 let cnt = per_parent_counts.entry(parent).or_insert(0);
448 let o = *cnt;
449 *cnt += 1;
450 o
451 };
452
453 let b = &node.screen_bounds;
455 let label = format!("{} {:.0}×{:.0}", node.type_name, b.width, b.height);
456
457 let tv_idx = self.tree_view.nodes.len();
458 self.tree_view
459 .nodes
460 .push(TreeNode::new(label, NodeIcon::Package, parent, order));
461
462 self.tree_view.nodes[tv_idx].is_expanded =
464 old_expanded.get(orig_idx).copied().unwrap_or(true);
465 self.tree_view.nodes[tv_idx].is_selected =
466 old_selected.get(orig_idx).copied().unwrap_or(false);
467
468 if depth_stack.len() <= node.depth {
470 depth_stack.resize(node.depth + 1, 0);
471 }
472 depth_stack[node.depth] = tv_idx;
473 }
474
475 self.selected = self.tree_view.nodes.iter().position(|n| n.is_selected);
477
478 *self.hovered_bounds.borrow_mut() = self
480 .tree_view
481 .hovered_node_idx()
482 .and_then(|i| nodes.get(i))
483 .map(|n| n.screen_bounds);
484
485 let tree_w = available.width;
487 let tree_bot = self.tree_origin_y();
488 let tree_top = self.list_area_h();
489 let tree_h = (tree_top - tree_bot).max(0.0);
490 self.tree_view
491 .set_bounds(Rect::new(0.0, tree_bot, tree_w, tree_h));
492 self.tree_view.layout(Size::new(tree_w, tree_h));
493
494 self._children[0].set_bounds(self.tree_view.bounds());
497
498 if let Some(cell) = &self.snapshot_out {
500 *cell.borrow_mut() = Some(self.saved_state());
501 }
502
503 available
504 }
505
506 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
507 let w = self.bounds.width;
508 let h = self.bounds.height;
509 let sy = self.split_y();
510 let hdr_y = h - HEADER_H;
511 let v = ctx.visuals().clone();
512
513 ctx.set_fill_color(c_panel_bg(&v));
515 ctx.begin_path();
516 ctx.rect(0.0, 0.0, w, h);
517 ctx.fill();
518
519 ctx.set_stroke_color(c_border(&v));
521 ctx.set_line_width(1.0);
522 ctx.begin_path();
523 ctx.move_to(0.0, 0.0);
524 ctx.line_to(0.0, h);
525 ctx.stroke();
526
527 ctx.set_fill_color(c_header_bg(&v));
529 ctx.begin_path();
530 ctx.rect(0.0, hdr_y, w, HEADER_H);
531 ctx.fill();
532
533 ctx.set_stroke_color(c_border(&v));
534 ctx.set_line_width(1.0);
535 ctx.begin_path();
536 ctx.move_to(0.0, hdr_y);
537 ctx.line_to(w, hdr_y);
538 ctx.stroke();
539
540 ctx.set_font(Arc::clone(&self.font));
541 ctx.set_font_size(13.0);
542 ctx.set_fill_color(c_text(&v));
543 let title = "Widget Inspector";
544 if let Some(m) = ctx.measure_text(title) {
545 ctx.fill_text(
546 title,
547 12.0,
548 hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
549 );
550 }
551
552 let count_txt = format!("{} widgets", self.nodes.borrow().len());
553 ctx.set_font_size(11.0);
554 ctx.set_fill_color(c_dim_text(&v));
555 if let Some(m) = ctx.measure_text(&count_txt) {
556 ctx.fill_text(
557 &count_txt,
558 w - m.width - 10.0,
559 hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
560 );
561 }
562
563 ctx.set_fill_color(c_props_bg(&v));
565 ctx.begin_path();
566 ctx.rect(0.0, 0.0, w, sy - 2.0);
567 ctx.fill();
568 self.paint_properties(ctx, sy - 2.0);
569
570 ctx.set_fill_color(c_split_bg(&v));
572 ctx.begin_path();
573 ctx.rect(0.0, sy - 2.0, w, 4.0);
574 ctx.fill();
575 ctx.set_stroke_color(c_border(&v));
576 ctx.set_line_width(1.0);
577 ctx.begin_path();
578 ctx.move_to(0.0, sy);
579 ctx.line_to(w, sy);
580 ctx.stroke();
581
582 let tree_bot = self.tree_origin_y();
584 let tree_top = self.list_area_h();
585 let tree_h = (tree_top - tree_bot).max(0.0);
586 if tree_h > 0.0 {
587 ctx.save();
588 ctx.translate(0.0, tree_bot);
589 ctx.clip_rect(0.0, 0.0, w, tree_h);
593 crate::widget::paint_subtree(&mut self.tree_view, ctx);
595 ctx.restore();
596 }
597 }
598
599 fn on_event(&mut self, event: &Event) -> EventResult {
600 match event {
601 Event::MouseDown {
602 pos,
603 button: MouseButton::Left,
604 ..
605 } => {
606 if self.on_split_handle(*pos) {
607 self.split_dragging = true;
608 return EventResult::Consumed;
612 }
613 if self.pos_in_tree_area(*pos) {
614 return self.forward_to_tree(event);
615 }
616 EventResult::Ignored
617 }
618 Event::MouseMove { pos } => {
619 if self.split_dragging {
620 self.props_h = pos.y.clamp(
621 MIN_PROPS_H,
622 (self.list_area_h() - MIN_TREE_H).max(MIN_PROPS_H),
623 );
624 crate::animation::request_draw();
625 return EventResult::Consumed;
626 }
627 if self.pos_in_tree_area(*pos) {
628 let _ = self.forward_to_tree(event);
629 self.update_hovered_bounds_from_tree();
630 } else if self.hovered_bounds.borrow().is_some() {
631 *self.hovered_bounds.borrow_mut() = None;
632 crate::animation::request_draw_without_invalidation();
633 }
634 EventResult::Ignored
635 }
636 Event::MouseUp {
637 button: MouseButton::Left,
638 pos,
639 ..
640 } => {
641 if self.split_dragging {
642 self.split_dragging = false;
643 crate::animation::request_draw();
644 return EventResult::Consumed;
645 }
646 if self.pos_in_tree_area(*pos) {
647 return self.forward_to_tree(event);
648 }
649 EventResult::Ignored
650 }
651 Event::MouseWheel { pos, .. } if self.pos_in_tree_area(*pos) => {
652 self.forward_to_tree(event)
653 }
654 _ => EventResult::Ignored,
655 }
656 }
657}
658
659impl InspectorPanel {
662 fn paint_properties(&self, ctx: &mut dyn DrawCtx, available_h: f64) {
663 if available_h < 4.0 {
664 return;
665 }
666 let w = self.bounds.width;
667 let v = ctx.visuals().clone();
668
669 ctx.set_font(Arc::clone(&self.font));
670 ctx.set_font_size(10.0);
671 ctx.set_fill_color(c_dim_text(&v));
672 ctx.fill_text("PROPERTIES", 10.0, available_h - 14.0);
673
674 ctx.set_stroke_color(c_border(&v));
675 ctx.set_line_width(1.0);
676 ctx.begin_path();
677 ctx.move_to(10.0 + 70.0, available_h - 10.0);
678 ctx.line_to(w - 8.0, available_h - 10.0);
679 ctx.stroke();
680
681 let Some(sel_idx) = self.selected else {
682 ctx.set_font_size(FONT_SIZE);
683 ctx.set_fill_color(c_dim_text(&v));
684 ctx.fill_text("(select a widget)", 10.0, available_h - 36.0);
685 return;
686 };
687
688 let nodes = self.nodes.borrow();
689 let Some(node) = nodes.get(sel_idx) else {
690 return;
691 };
692
693 ctx.set_font_size(14.0);
694 ctx.set_fill_color(c_text(&v));
695 ctx.fill_text(node.type_name, 10.0, available_h - 36.0);
696
697 let b = &node.screen_bounds;
698 let rows: &[(&str, String)] = &[
699 ("x", format!("{:.1}", b.x)),
700 ("y", format!("{:.1}", b.y)),
701 ("width", format!("{:.1}", b.width)),
702 ("height", format!("{:.1}", b.height)),
703 ("depth", format!("{}", node.depth)),
704 ];
705
706 ctx.set_font_size(FONT_SIZE);
707 let row_start_y = available_h - 56.0;
708 for (i, (label, value)) in rows.iter().enumerate() {
709 let ry = row_start_y - i as f64 * 18.0;
710 if ry < 4.0 {
711 break;
712 }
713 ctx.set_fill_color(c_dim_text(&v));
714 ctx.fill_text(label, 12.0, ry);
715 ctx.set_fill_color(c_text(&v));
716 if let Some(m) = ctx.measure_text(value) {
717 ctx.fill_text(value, w - m.width - 10.0, ry);
718 }
719 ctx.set_stroke_color(c_border(&v));
720 ctx.set_line_width(0.5);
721 ctx.begin_path();
722 ctx.move_to(8.0, ry - 4.0);
723 ctx.line_to(w - 8.0, ry - 4.0);
724 ctx.stroke();
725 }
726
727 let prop_start_y = row_start_y - rows.len() as f64 * 18.0 - 4.0;
729 for (j, (prop_label, prop_value)) in node.properties.iter().enumerate() {
730 let ry = prop_start_y - j as f64 * 18.0;
731 if ry < 4.0 {
732 break;
733 }
734 ctx.set_fill_color(c_dim_text(&v));
735 ctx.fill_text(prop_label, 12.0, ry);
736 let is_bool = prop_value == "true" || prop_value == "false";
738 if is_bool {
739 let bool_color = if prop_value == "true" {
740 Color::rgb(0.10, 0.52, 0.10)
741 } else {
742 Color::rgb(0.65, 0.18, 0.18)
743 };
744 ctx.set_fill_color(bool_color);
745 } else {
746 ctx.set_fill_color(c_text(&v));
747 }
748 if let Some(m) = ctx.measure_text(prop_value) {
749 ctx.fill_text(prop_value, w - m.width - 10.0, ry);
750 }
751 ctx.set_stroke_color(c_border(&v));
752 ctx.set_line_width(0.5);
753 ctx.begin_path();
754 ctx.move_to(8.0, ry - 4.0);
755 ctx.line_to(w - 8.0, ry - 4.0);
756 ctx.stroke();
757 }
758
759 let total_rows = rows.len() + node.properties.len();
761 let diag_h = (row_start_y - total_rows as f64 * 18.0 - 12.0).min(80.0);
762 if diag_h > 30.0 {
763 let diag_y_top = diag_h - 4.0;
764 let diag_w = w - 20.0;
765 let aspect = if b.height > 0.0 {
766 b.width / b.height
767 } else {
768 1.0
769 };
770 let box_h = (diag_h * 0.6).min(50.0);
771 let box_w = (box_h * aspect).min(diag_w * 0.8);
772 let box_x = 10.0 + (diag_w - box_w) * 0.5;
773 let box_y = diag_y_top - (diag_h + box_h) * 0.5;
774
775 ctx.set_fill_color(Color::rgba(0.10, 0.50, 1.0, 0.10));
776 ctx.begin_path();
777 ctx.rect(box_x, box_y, box_w, box_h);
778 ctx.fill();
779 ctx.set_stroke_color(Color::rgba(0.10, 0.50, 1.0, 0.50));
780 ctx.set_line_width(1.0);
781 ctx.begin_path();
782 ctx.rect(box_x, box_y, box_w, box_h);
783 ctx.stroke();
784
785 let dim = format!("{:.0} × {:.0}", b.width, b.height);
786 ctx.set_font_size(10.0);
787 ctx.set_fill_color(Color::rgba(0.10, 0.40, 0.90, 0.80));
788 if let Some(m) = ctx.measure_text(&dim) {
789 if m.width < box_w - 4.0 {
790 ctx.fill_text(
791 &dim,
792 box_x + (box_w - m.width) * 0.5,
793 box_y + (box_h - m.ascent - m.descent) * 0.5 + m.descent,
794 );
795 }
796 }
797 }
798 }
799}