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 { self.name }
50 fn bounds(&self) -> Rect { self.bounds }
51 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
52 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
53 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
54 fn margin(&self) -> Insets { self.base.margin }
55 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
56 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
57 fn min_size(&self) -> Size { self.base.min_size }
58 fn max_size(&self) -> Size { self.base.max_size }
59 fn layout(&mut self, _: Size) -> Size { Size::new(self.bounds.width, self.bounds.height) }
60 fn paint(&mut self, _: &mut dyn DrawCtx) {}
61 fn hit_test(&self, _: Point) -> bool { false }
62 fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
63 fn contributes_children_to_inspector(&self) -> bool { false }
64}
65
66const DEFAULT_PROPS_H: f64 = 180.0;
68const FONT_SIZE: f64 = 12.0;
69const HEADER_H: f64 = 30.0;
70const SPLIT_HIT: f64 = 5.0;
71const MIN_PROPS_H: f64 = 60.0;
72const MIN_TREE_H: f64 = 60.0;
73
74fn c_panel_bg (v: &crate::theme::Visuals) -> Color { v.panel_fill }
78fn c_header_bg(v: &crate::theme::Visuals) -> Color {
79 let f = if is_dark(v) { 0.80 } else { 0.94 };
81 Color::rgba(v.panel_fill.r * f, v.panel_fill.g * f, v.panel_fill.b * f, 1.0)
82}
83fn c_props_bg (v: &crate::theme::Visuals) -> Color { v.window_fill }
84fn c_split_bg (v: &crate::theme::Visuals) -> Color {
85 let t = if is_dark(v) { 1.0 } else { 0.0 };
86 Color::rgba(t, t, t, 0.10)
87}
88fn c_border (v: &crate::theme::Visuals) -> Color { v.separator }
89fn c_text (v: &crate::theme::Visuals) -> Color { v.text_color }
90fn c_dim_text (v: &crate::theme::Visuals) -> Color { v.text_dim }
91
92fn is_dark(v: &crate::theme::Visuals) -> bool {
93 let lum = 0.299 * v.panel_fill.r + 0.587 * v.panel_fill.g + 0.114 * v.panel_fill.b;
95 lum < 0.5
96}
97
98fn translate_event(event: &Event, offset_y: f64) -> Event {
103 match event {
104 Event::MouseDown { pos, button, modifiers } => Event::MouseDown {
105 pos: Point::new(pos.x, pos.y - offset_y),
106 button: *button,
107 modifiers: *modifiers,
108 },
109 Event::MouseMove { pos } => Event::MouseMove {
110 pos: Point::new(pos.x, pos.y - offset_y),
111 },
112 Event::MouseUp { pos, button, modifiers } => Event::MouseUp {
113 pos: Point::new(pos.x, pos.y - offset_y),
114 button: *button,
115 modifiers: *modifiers,
116 },
117 Event::MouseWheel { pos, delta_y, delta_x } => Event::MouseWheel {
118 pos: Point::new(pos.x, pos.y - offset_y),
119 delta_y: *delta_y,
120 delta_x: *delta_x,
121 },
122 other => other.clone(),
123 }
124}
125
126pub struct InspectorPanel {
129 bounds: Rect,
130 _children: Vec<Box<dyn Widget>>,
134 base: WidgetBase,
135 font: Arc<Font>,
136 nodes: Rc<RefCell<Vec<InspectorNode>>>,
137 selected: Option<usize>,
139 props_h: f64,
140 split_dragging: bool,
141 pub hovered_bounds: Rc<RefCell<Option<Rect>>>,
143 pub(crate) tree_view: TreeView,
145 pending_expanded: Option<Vec<bool>>,
149 pending_selected: Option<Option<usize>>,
150 snapshot_out: Option<Rc<RefCell<Option<InspectorSavedState>>>>,
154}
155
156#[derive(Clone, Debug, Default)]
158pub struct InspectorSavedState {
159 pub expanded: Vec<bool>,
160 pub selected: Option<usize>,
161 pub props_h: f64,
162}
163
164impl InspectorPanel {
165 pub fn new(
166 font: Arc<Font>,
167 nodes: Rc<RefCell<Vec<InspectorNode>>>,
168 hovered_bounds: Rc<RefCell<Option<Rect>>>,
169 ) -> Self {
170 let tree_view = TreeView::new(Arc::clone(&font))
171 .with_row_height(20.0)
172 .with_font_size(12.0)
173 .with_indent_width(14.0);
174 Self {
175 bounds: Rect::default(),
176 _children: vec![Box::new(InternalPresenceNode {
177 bounds: Rect::default(),
178 children: Vec::new(),
179 base: WidgetBase::new(),
180 name: "TreeView",
181 })],
182 base: WidgetBase::new(),
183 font,
184 nodes,
185 selected: None,
186 props_h: DEFAULT_PROPS_H,
187 split_dragging: false,
188 hovered_bounds,
189 tree_view,
190 pending_expanded: None,
191 pending_selected: None,
192 snapshot_out: None,
193 }
194 }
195
196 pub fn with_snapshot_cell(
200 mut self,
201 cell: Rc<RefCell<Option<InspectorSavedState>>>,
202 ) -> Self {
203 self.snapshot_out = Some(cell);
204 self
205 }
206
207 pub fn saved_state(&self) -> InspectorSavedState {
218 InspectorSavedState {
219 expanded: self.tree_view.nodes.iter().map(|n| n.is_expanded).collect(),
220 selected: self.tree_view.nodes.iter().position(|n| n.is_selected),
221 props_h: self.props_h,
222 }
223 }
224
225 pub fn apply_saved_state(&mut self, s: InspectorSavedState) {
230 self.pending_expanded = Some(s.expanded);
231 self.pending_selected = Some(s.selected);
232 self.props_h = s.props_h.clamp(MIN_PROPS_H, 1024.0);
233 }
234
235 fn list_area_h(&self) -> f64 { (self.bounds.height - HEADER_H).max(0.0) }
239
240 fn split_y(&self) -> f64 {
242 self.props_h.clamp(
243 MIN_PROPS_H,
244 (self.list_area_h() - MIN_TREE_H).max(MIN_PROPS_H),
245 )
246 }
247
248 fn tree_origin_y(&self) -> f64 { self.split_y() + 4.0 }
250
251 fn on_split_handle(&self, pos: Point) -> bool {
252 let sy = self.split_y();
253 pos.y >= sy - SPLIT_HIT && pos.y <= sy + SPLIT_HIT
254 }
255
256 fn pos_in_tree_area(&self, pos: Point) -> bool {
257 let tree_bot = self.tree_origin_y();
258 let tree_top = self.list_area_h();
259 pos.y >= tree_bot && pos.y <= tree_top
260 }
261
262 fn forward_to_tree(&mut self, event: &Event) -> EventResult {
264 let offset_y = self.tree_view.bounds().y;
268 let translated = translate_event(event, offset_y);
269 self.tree_view.on_event(&translated)
270 }
271}
272
273impl InspectorPanel {
276 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
277 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
278 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
279 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
280 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
281}
282
283impl Widget for InspectorPanel {
284 fn type_name(&self) -> &'static str { "InspectorPanel" }
285 fn bounds(&self) -> Rect { self.bounds }
286 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
287 fn children(&self) -> &[Box<dyn Widget>] { &self._children }
288 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self._children }
289
290 fn margin(&self) -> Insets { self.base.margin }
291 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
292 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
293 fn min_size(&self) -> Size { self.base.min_size }
294 fn max_size(&self) -> Size { self.base.max_size }
295
296 fn layout(&mut self, available: Size) -> Size {
297 self.bounds.width = available.width;
298 self.bounds.height = available.height;
299
300 let nodes = self.nodes.borrow();
301
302 let mut old_expanded: Vec<bool> = self.tree_view.nodes.iter()
307 .map(|n| n.is_expanded).collect();
308 let mut old_selected: Vec<bool> = self.tree_view.nodes.iter()
309 .map(|n| n.is_selected).collect();
310 if let Some(pe) = self.pending_expanded.take() {
311 old_expanded = pe;
312 }
313 if let Some(ps) = self.pending_selected.take() {
314 old_selected = vec![false; old_expanded.len().max(ps.map(|i| i + 1).unwrap_or(0))];
315 if let Some(i) = ps {
316 if i < old_selected.len() { old_selected[i] = true; }
317 }
318 }
319
320 self.tree_view.nodes.clear();
321
322 let mut depth_stack: Vec<usize> = Vec::new();
326 let mut per_parent_counts: std::collections::HashMap<Option<usize>, u32> =
327 std::collections::HashMap::new();
328
329 for (orig_idx, node) in nodes.iter().enumerate() {
330 let parent = if node.depth == 0 {
331 None
332 } else {
333 depth_stack.get(node.depth.saturating_sub(1)).copied()
334 };
335
336 let order = {
337 let cnt = per_parent_counts.entry(parent).or_insert(0);
338 let o = *cnt;
339 *cnt += 1;
340 o
341 };
342
343 let b = &node.screen_bounds;
345 let label = format!("{} {:.0}×{:.0}", node.type_name, b.width, b.height);
346
347 let tv_idx = self.tree_view.nodes.len();
348 self.tree_view.nodes.push(TreeNode::new(label, NodeIcon::Package, parent, order));
349
350 self.tree_view.nodes[tv_idx].is_expanded =
352 old_expanded.get(orig_idx).copied().unwrap_or(true);
353 self.tree_view.nodes[tv_idx].is_selected =
354 old_selected.get(orig_idx).copied().unwrap_or(false);
355
356 if depth_stack.len() <= node.depth {
358 depth_stack.resize(node.depth + 1, 0);
359 }
360 depth_stack[node.depth] = tv_idx;
361 }
362
363 self.selected = self.tree_view.nodes.iter().position(|n| n.is_selected);
365
366 *self.hovered_bounds.borrow_mut() = self.tree_view.hovered_node_idx()
368 .and_then(|i| nodes.get(i))
369 .map(|n| n.screen_bounds);
370
371 let tree_w = available.width;
373 let tree_bot = self.tree_origin_y();
374 let tree_top = self.list_area_h();
375 let tree_h = (tree_top - tree_bot).max(0.0);
376 self.tree_view.set_bounds(Rect::new(0.0, tree_bot, tree_w, tree_h));
377 self.tree_view.layout(Size::new(tree_w, tree_h));
378
379 self._children[0].set_bounds(self.tree_view.bounds());
382
383 if let Some(cell) = &self.snapshot_out {
385 *cell.borrow_mut() = Some(self.saved_state());
386 }
387
388 available
389 }
390
391 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
392 let w = self.bounds.width;
393 let h = self.bounds.height;
394 let sy = self.split_y();
395 let hdr_y = h - HEADER_H;
396 let v = ctx.visuals().clone();
397
398 ctx.set_fill_color(c_panel_bg(&v));
400 ctx.begin_path();
401 ctx.rect(0.0, 0.0, w, h);
402 ctx.fill();
403
404 ctx.set_stroke_color(c_border(&v));
406 ctx.set_line_width(1.0);
407 ctx.begin_path();
408 ctx.move_to(0.0, 0.0);
409 ctx.line_to(0.0, h);
410 ctx.stroke();
411
412 ctx.set_fill_color(c_header_bg(&v));
414 ctx.begin_path();
415 ctx.rect(0.0, hdr_y, w, HEADER_H);
416 ctx.fill();
417
418 ctx.set_stroke_color(c_border(&v));
419 ctx.set_line_width(1.0);
420 ctx.begin_path();
421 ctx.move_to(0.0, hdr_y);
422 ctx.line_to(w, hdr_y);
423 ctx.stroke();
424
425 ctx.set_font(Arc::clone(&self.font));
426 ctx.set_font_size(13.0);
427 ctx.set_fill_color(c_text(&v));
428 let title = "Widget Inspector";
429 if let Some(m) = ctx.measure_text(title) {
430 ctx.fill_text(
431 title,
432 12.0,
433 hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
434 );
435 }
436
437 let count_txt = format!("{} widgets", self.nodes.borrow().len());
438 ctx.set_font_size(11.0);
439 ctx.set_fill_color(c_dim_text(&v));
440 if let Some(m) = ctx.measure_text(&count_txt) {
441 ctx.fill_text(
442 &count_txt,
443 w - m.width - 10.0,
444 hdr_y + (HEADER_H - m.ascent - m.descent) * 0.5 + m.descent,
445 );
446 }
447
448 ctx.set_fill_color(c_props_bg(&v));
450 ctx.begin_path();
451 ctx.rect(0.0, 0.0, w, sy - 2.0);
452 ctx.fill();
453 self.paint_properties(ctx, sy - 2.0);
454
455 ctx.set_fill_color(c_split_bg(&v));
457 ctx.begin_path();
458 ctx.rect(0.0, sy - 2.0, w, 4.0);
459 ctx.fill();
460 ctx.set_stroke_color(c_border(&v));
461 ctx.set_line_width(1.0);
462 ctx.begin_path();
463 ctx.move_to(0.0, sy);
464 ctx.line_to(w, sy);
465 ctx.stroke();
466
467 let tree_bot = self.tree_origin_y();
469 let tree_top = self.list_area_h();
470 let tree_h = (tree_top - tree_bot).max(0.0);
471 if tree_h > 0.0 {
472 ctx.save();
473 ctx.translate(0.0, tree_bot);
474 ctx.clip_rect(0.0, 0.0, w, tree_h);
478 crate::widget::paint_subtree(&mut self.tree_view, ctx);
480 ctx.restore();
481 }
482 }
483
484 fn on_event(&mut self, event: &Event) -> EventResult {
485 match event {
486 Event::MouseDown { pos, button: MouseButton::Left, .. } => {
487 if self.on_split_handle(*pos) {
488 self.split_dragging = true;
489 return EventResult::Consumed;
493 }
494 if self.pos_in_tree_area(*pos) {
495 return self.forward_to_tree(event);
496 }
497 EventResult::Ignored
498 }
499 Event::MouseMove { pos } => {
500 if self.split_dragging {
501 self.props_h = pos.y.clamp(
502 MIN_PROPS_H,
503 (self.list_area_h() - MIN_TREE_H).max(MIN_PROPS_H),
504 );
505 crate::animation::request_tick();
506 return EventResult::Consumed;
507 }
508 if self.pos_in_tree_area(*pos) {
509 let _ = self.forward_to_tree(event);
510 }
511 EventResult::Ignored
512 }
513 Event::MouseUp { button: MouseButton::Left, pos, .. } => {
514 if self.split_dragging {
515 self.split_dragging = false;
516 crate::animation::request_tick();
517 return EventResult::Consumed;
518 }
519 if self.pos_in_tree_area(*pos) {
520 return self.forward_to_tree(event);
521 }
522 EventResult::Ignored
523 }
524 Event::MouseWheel { pos, .. } if self.pos_in_tree_area(*pos) => {
525 self.forward_to_tree(event)
526 }
527 _ => EventResult::Ignored,
528 }
529 }
530}
531
532impl InspectorPanel {
535 fn paint_properties(&self, ctx: &mut dyn DrawCtx, available_h: f64) {
536 if available_h < 4.0 { return; }
537 let w = self.bounds.width;
538 let v = ctx.visuals().clone();
539
540 ctx.set_font(Arc::clone(&self.font));
541 ctx.set_font_size(10.0);
542 ctx.set_fill_color(c_dim_text(&v));
543 ctx.fill_text("PROPERTIES", 10.0, available_h - 14.0);
544
545 ctx.set_stroke_color(c_border(&v));
546 ctx.set_line_width(1.0);
547 ctx.begin_path();
548 ctx.move_to(10.0 + 70.0, available_h - 10.0);
549 ctx.line_to(w - 8.0, available_h - 10.0);
550 ctx.stroke();
551
552 let Some(sel_idx) = self.selected else {
553 ctx.set_font_size(FONT_SIZE);
554 ctx.set_fill_color(c_dim_text(&v));
555 ctx.fill_text("(select a widget)", 10.0, available_h - 36.0);
556 return;
557 };
558
559 let nodes = self.nodes.borrow();
560 let Some(node) = nodes.get(sel_idx) else { return; };
561
562 ctx.set_font_size(14.0);
563 ctx.set_fill_color(c_text(&v));
564 ctx.fill_text(node.type_name, 10.0, available_h - 36.0);
565
566 let b = &node.screen_bounds;
567 let rows: &[(&str, String)] = &[
568 ("x", format!("{:.1}", b.x)),
569 ("y", format!("{:.1}", b.y)),
570 ("width", format!("{:.1}", b.width)),
571 ("height", format!("{:.1}", b.height)),
572 ("depth", format!("{}", node.depth)),
573 ];
574
575 ctx.set_font_size(FONT_SIZE);
576 let row_start_y = available_h - 56.0;
577 for (i, (label, value)) in rows.iter().enumerate() {
578 let ry = row_start_y - i as f64 * 18.0;
579 if ry < 4.0 { break; }
580 ctx.set_fill_color(c_dim_text(&v));
581 ctx.fill_text(label, 12.0, ry);
582 ctx.set_fill_color(c_text(&v));
583 if let Some(m) = ctx.measure_text(value) {
584 ctx.fill_text(value, w - m.width - 10.0, ry);
585 }
586 ctx.set_stroke_color(c_border(&v));
587 ctx.set_line_width(0.5);
588 ctx.begin_path();
589 ctx.move_to(8.0, ry - 4.0);
590 ctx.line_to(w - 8.0, ry - 4.0);
591 ctx.stroke();
592 }
593
594 let prop_start_y = row_start_y - rows.len() as f64 * 18.0 - 4.0;
596 for (j, (prop_label, prop_value)) in node.properties.iter().enumerate() {
597 let ry = prop_start_y - j as f64 * 18.0;
598 if ry < 4.0 { break; }
599 ctx.set_fill_color(c_dim_text(&v));
600 ctx.fill_text(prop_label, 12.0, ry);
601 let is_bool = prop_value == "true" || prop_value == "false";
603 if is_bool {
604 let bool_color = if prop_value == "true" {
605 Color::rgb(0.10, 0.52, 0.10)
606 } else {
607 Color::rgb(0.65, 0.18, 0.18)
608 };
609 ctx.set_fill_color(bool_color);
610 } else {
611 ctx.set_fill_color(c_text(&v));
612 }
613 if let Some(m) = ctx.measure_text(prop_value) {
614 ctx.fill_text(prop_value, w - m.width - 10.0, ry);
615 }
616 ctx.set_stroke_color(c_border(&v));
617 ctx.set_line_width(0.5);
618 ctx.begin_path();
619 ctx.move_to(8.0, ry - 4.0);
620 ctx.line_to(w - 8.0, ry - 4.0);
621 ctx.stroke();
622 }
623
624 let total_rows = rows.len() + node.properties.len();
626 let diag_h = (row_start_y - total_rows as f64 * 18.0 - 12.0).min(80.0);
627 if diag_h > 30.0 {
628 let diag_y_top = diag_h - 4.0;
629 let diag_w = w - 20.0;
630 let aspect = if b.height > 0.0 { b.width / b.height } else { 1.0 };
631 let box_h = (diag_h * 0.6).min(50.0);
632 let box_w = (box_h * aspect).min(diag_w * 0.8);
633 let box_x = 10.0 + (diag_w - box_w) * 0.5;
634 let box_y = diag_y_top - (diag_h + box_h) * 0.5;
635
636 ctx.set_fill_color(Color::rgba(0.10, 0.50, 1.0, 0.10));
637 ctx.begin_path();
638 ctx.rect(box_x, box_y, box_w, box_h);
639 ctx.fill();
640 ctx.set_stroke_color(Color::rgba(0.10, 0.50, 1.0, 0.50));
641 ctx.set_line_width(1.0);
642 ctx.begin_path();
643 ctx.rect(box_x, box_y, box_w, box_h);
644 ctx.stroke();
645
646 let dim = format!("{:.0} × {:.0}", b.width, b.height);
647 ctx.set_font_size(10.0);
648 ctx.set_fill_color(Color::rgba(0.10, 0.40, 0.90, 0.80));
649 if let Some(m) = ctx.measure_text(&dim) {
650 if m.width < box_w - 4.0 {
651 ctx.fill_text(
652 &dim,
653 box_x + (box_w - m.width) * 0.5,
654 box_y + (box_h - m.ascent - m.descent) * 0.5 + m.descent,
655 );
656 }
657 }
658 }
659 }
660}