1use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum PanelContent {
13 GraphView,
15 NodeInspector,
17 PerformanceMetrics,
19 LogView,
21 ThemeEditor,
23}
24
25#[derive(Debug, Clone)]
27pub struct Panel {
28 pub title: String,
30 pub content: PanelContent,
32 pub width: f32,
34 pub height: f32,
36}
37
38impl Panel {
39 pub fn new(title: &str, content: PanelContent) -> Self {
46 Self {
47 title: title.to_string(),
48 content,
49 width: 1.0,
50 height: 1.0,
51 }
52 }
53
54 pub fn with_size(mut self, width: f32, height: f32) -> Self {
56 self.width = width.clamp(0.0, 1.0);
57 self.height = height.clamp(0.0, 1.0);
58 self
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct PerfMetrics {
65 pub frame_time_ms: f32,
67 pub fps: f32,
69 pub node_count: usize,
71 pub edge_count: usize,
73 pub gpu_memory_mb: f32,
75}
76
77impl Default for PerfMetrics {
78 fn default() -> Self {
79 Self {
80 frame_time_ms: 0.0,
81 fps: 0.0,
82 node_count: 0,
83 edge_count: 0,
84 gpu_memory_mb: 0.0,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
91pub enum LogLevel {
92 Debug,
94 Info,
96 Warn,
98 Error,
100}
101
102#[derive(Debug, Clone)]
104pub struct LogEntry {
105 pub timestamp: String,
107 pub level: LogLevel,
109 pub message: String,
111}
112
113#[derive(Debug, Clone)]
118pub enum DevToolWidget {
119 Text {
121 content: String,
123 color: [f32; 4],
125 },
126 Graph {
128 data: Vec<f32>,
130 label: String,
132 },
133 Inspector {
135 properties: Vec<(String, String)>,
137 },
138 Button {
140 label: String,
142 clicked: bool,
144 },
145}
146
147#[derive(Debug, Clone)]
149pub struct DevToolsDashboard {
150 pub panels: Vec<Panel>,
152 pub active_panel: usize,
154 pub visible: bool,
156}
157
158impl Default for DevToolsDashboard {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164impl DevToolsDashboard {
165 pub fn new() -> Self {
179 Self {
180 panels: vec![
181 Panel::new("Performance", PanelContent::PerformanceMetrics),
182 Panel::new("Logs", PanelContent::LogView),
183 ],
184 active_panel: 0,
185 visible: false,
186 }
187 }
188
189 pub fn toggle(&mut self) {
191 self.visible = !self.visible;
192 }
193
194 pub fn add_panel(&mut self, panel: Panel) {
200 self.panels.push(panel);
201 }
202
203 pub fn remove_panel(&mut self, index: usize) {
212 if index < self.panels.len() {
213 self.panels.remove(index);
214 if self.active_panel >= self.panels.len() && !self.panels.is_empty() {
215 self.active_panel = self.panels.len() - 1;
216 }
217 }
218 }
219
220 pub fn set_active(&mut self, index: usize) {
228 if index < self.panels.len() {
229 self.active_panel = index;
230 }
231 }
232
233 pub fn render(&self) -> Vec<DevToolWidget> {
238 if !self.visible || self.panels.is_empty() {
239 return Vec::new();
240 }
241
242 let mut widgets = Vec::new();
243
244 for (i, panel) in self.panels.iter().enumerate() {
246 let color = if i == self.active_panel {
247 [0.0, 1.0, 1.0, 1.0]
248 } else {
249 [0.5, 0.5, 0.5, 1.0]
250 };
251 widgets.push(DevToolWidget::Button {
252 label: panel.title.clone(),
253 clicked: false,
254 });
255 widgets.push(DevToolWidget::Text {
256 content: panel.title.clone(),
257 color,
258 });
259 }
260
261 if let Some(panel) = self.panels.get(self.active_panel) {
263 match &panel.content {
264 PanelContent::PerformanceMetrics => {
265 let metrics = capture_metrics();
266 widgets.push(DevToolWidget::Text {
267 content: format!("FPS: {:.1}", metrics.fps),
268 color: [0.0, 1.0, 0.0, 1.0],
269 });
270 widgets.push(DevToolWidget::Text {
271 content: format!("Frame Time: {:.2} ms", metrics.frame_time_ms),
272 color: [1.0, 1.0, 0.0, 1.0],
273 });
274 widgets.push(DevToolWidget::Text {
275 content: format!("Nodes: {}", metrics.node_count),
276 color: [1.0, 1.0, 1.0, 1.0],
277 });
278 widgets.push(DevToolWidget::Text {
279 content: format!("Edges: {}", metrics.edge_count),
280 color: [1.0, 1.0, 1.0, 1.0],
281 });
282 widgets.push(DevToolWidget::Text {
283 content: format!("GPU Memory: {:.1} MB", metrics.gpu_memory_mb),
284 color: [1.0, 0.5, 0.0, 1.0],
285 });
286 widgets.push(DevToolWidget::Graph {
287 data: vec![metrics.frame_time_ms],
288 label: "Frame Time (ms)".to_string(),
289 });
290 }
291 PanelContent::NodeInspector => {
292 widgets.push(DevToolWidget::Inspector {
293 properties: vec![
294 ("type".to_string(), "Node".to_string()),
295 ("id".to_string(), "0".to_string()),
296 ],
297 });
298 }
299 PanelContent::GraphView => {
300 widgets.push(DevToolWidget::Text {
301 content: "Graph View".to_string(),
302 color: [0.0, 1.0, 1.0, 1.0],
303 });
304 }
305 PanelContent::LogView => {
306 widgets.push(DevToolWidget::Text {
307 content: "Log View — No entries".to_string(),
308 color: [0.7, 0.7, 0.7, 1.0],
309 });
310 }
311 PanelContent::ThemeEditor => {
312 widgets.push(DevToolWidget::Text {
313 content: "Theme Editor".to_string(),
314 color: [1.0, 0.0, 1.0, 1.0],
315 });
316 }
317 }
318 }
319
320 widgets
321 }
322}
323
324use std::sync::RwLock;
325
326static METRICS: RwLock<PerfMetrics> = RwLock::new(PerfMetrics {
328 frame_time_ms: 0.0,
329 fps: 0.0,
330 node_count: 0,
331 edge_count: 0,
332 gpu_memory_mb: 0.0,
333});
334
335pub fn capture_metrics() -> PerfMetrics {
350 METRICS.read().map(|m| m.clone()).unwrap_or_default()
351}
352
353pub fn update_metrics(metrics: PerfMetrics) {
357 if let Ok(mut m) = METRICS.write() {
358 *m = metrics;
359 }
360}
361
362pub fn format_log_entry(entry: &LogEntry) -> String {
383 let level_str = match entry.level {
384 LogLevel::Debug => "DEBUG",
385 LogLevel::Info => "INFO",
386 LogLevel::Warn => "WARN",
387 LogLevel::Error => "ERROR",
388 };
389 format!("[{}] {}: {}", entry.timestamp, level_str, entry.message)
390}
391
392pub fn current_timestamp() -> String {
396 match SystemTime::now().duration_since(UNIX_EPOCH) {
397 Ok(d) => format!("{}", d.as_secs()),
398 Err(_) => "0".to_string(),
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_dashboard_new() {
408 let dashboard = DevToolsDashboard::new();
409 assert!(!dashboard.visible);
410 assert_eq!(dashboard.panels.len(), 2);
411 assert_eq!(dashboard.active_panel, 0);
412 assert_eq!(dashboard.panels[0].title, "Performance");
413 assert_eq!(dashboard.panels[1].title, "Logs");
414 }
415
416 #[test]
417 fn test_dashboard_toggle() {
418 let mut dashboard = DevToolsDashboard::new();
419 assert!(!dashboard.visible);
420 dashboard.toggle();
421 assert!(dashboard.visible);
422 dashboard.toggle();
423 assert!(!dashboard.visible);
424 }
425
426 #[test]
427 fn test_dashboard_add_panel() {
428 let mut dashboard = DevToolsDashboard::new();
429 dashboard.add_panel(Panel::new("Inspector", PanelContent::NodeInspector));
430 assert_eq!(dashboard.panels.len(), 3);
431 assert_eq!(dashboard.panels[2].title, "Inspector");
432 assert_eq!(dashboard.panels[2].content, PanelContent::NodeInspector);
433 }
434
435 #[test]
436 fn test_dashboard_remove_panel() {
437 let mut dashboard = DevToolsDashboard::new();
438 dashboard.add_panel(Panel::new("Extra", PanelContent::GraphView));
439 assert_eq!(dashboard.panels.len(), 3);
440 dashboard.remove_panel(2);
441 assert_eq!(dashboard.panels.len(), 2);
442 }
443
444 #[test]
445 fn test_dashboard_remove_panel_out_of_bounds() {
446 let mut dashboard = DevToolsDashboard::new();
447 dashboard.remove_panel(99);
448 assert_eq!(dashboard.panels.len(), 2);
449 }
450
451 #[test]
452 fn test_dashboard_remove_active_panel_clamps() {
453 let mut dashboard = DevToolsDashboard::new();
454 dashboard.set_active(1);
455 assert_eq!(dashboard.active_panel, 1);
456 dashboard.remove_panel(1);
457 assert_eq!(dashboard.active_panel, 0);
459 }
460
461 #[test]
462 fn test_dashboard_set_active() {
463 let mut dashboard = DevToolsDashboard::new();
464 dashboard.add_panel(Panel::new("Third", PanelContent::ThemeEditor));
465 dashboard.set_active(2);
466 assert_eq!(dashboard.active_panel, 2);
467 }
468
469 #[test]
470 fn test_dashboard_set_active_out_of_bounds() {
471 let mut dashboard = DevToolsDashboard::new();
472 dashboard.set_active(99);
473 assert_eq!(dashboard.active_panel, 0);
474 }
475
476 #[test]
477 fn test_dashboard_render_hidden() {
478 let dashboard = DevToolsDashboard::new();
479 let widgets = dashboard.render();
480 assert!(widgets.is_empty());
481 }
482
483 #[test]
484 fn test_dashboard_render_visible() {
485 let mut dashboard = DevToolsDashboard::new();
486 dashboard.toggle();
487 let widgets = dashboard.render();
488 assert!(!widgets.is_empty());
490 }
491
492 #[test]
493 fn test_panel_new() {
494 let panel = Panel::new("Test", PanelContent::GraphView);
495 assert_eq!(panel.title, "Test");
496 assert_eq!(panel.content, PanelContent::GraphView);
497 assert_eq!(panel.width, 1.0);
498 assert_eq!(panel.height, 1.0);
499 }
500
501 #[test]
502 fn test_panel_with_size() {
503 let panel = Panel::new("Sized", PanelContent::LogView).with_size(0.5, 0.75);
504 assert_eq!(panel.width, 0.5);
505 assert_eq!(panel.height, 0.75);
506 }
507
508 #[test]
509 fn test_panel_with_size_clamped() {
510 let panel = Panel::new("Clamped", PanelContent::LogView).with_size(1.5, -0.5);
511 assert_eq!(panel.width, 1.0);
512 assert_eq!(panel.height, 0.0);
513 }
514
515 #[test]
516 fn test_capture_metrics() {
517 let metrics = capture_metrics();
518 assert_eq!(metrics.frame_time_ms, 0.0);
519 assert_eq!(metrics.fps, 0.0);
520 assert_eq!(metrics.node_count, 0);
521 assert_eq!(metrics.edge_count, 0);
522 assert_eq!(metrics.gpu_memory_mb, 0.0);
523 }
524
525 #[test]
526 fn test_format_log_entry_info() {
527 let entry = LogEntry {
528 timestamp: "2025-01-01T00:00:00Z".to_string(),
529 level: LogLevel::Info,
530 message: "Application started".to_string(),
531 };
532 assert_eq!(
533 format_log_entry(&entry),
534 "[2025-01-01T00:00:00Z] INFO: Application started"
535 );
536 }
537
538 #[test]
539 fn test_format_log_entry_debug() {
540 let entry = LogEntry {
541 timestamp: "T1".to_string(),
542 level: LogLevel::Debug,
543 message: "debug msg".to_string(),
544 };
545 assert_eq!(format_log_entry(&entry), "[T1] DEBUG: debug msg");
546 }
547
548 #[test]
549 fn test_format_log_entry_warn() {
550 let entry = LogEntry {
551 timestamp: "T2".to_string(),
552 level: LogLevel::Warn,
553 message: "watch out".to_string(),
554 };
555 assert_eq!(format_log_entry(&entry), "[T2] WARN: watch out");
556 }
557
558 #[test]
559 fn test_format_log_entry_error() {
560 let entry = LogEntry {
561 timestamp: "T3".to_string(),
562 level: LogLevel::Error,
563 message: "something broke".to_string(),
564 };
565 assert_eq!(format_log_entry(&entry), "[T3] ERROR: something broke");
566 }
567
568 #[test]
569 fn test_log_level_ordering() {
570 assert!(LogLevel::Debug < LogLevel::Info);
571 assert!(LogLevel::Info < LogLevel::Warn);
572 assert!(LogLevel::Warn < LogLevel::Error);
573 }
574
575 #[test]
576 fn test_perf_metrics_default() {
577 let m = PerfMetrics::default();
578 assert_eq!(m.frame_time_ms, 0.0);
579 assert_eq!(m.fps, 0.0);
580 assert_eq!(m.node_count, 0);
581 assert_eq!(m.edge_count, 0);
582 assert_eq!(m.gpu_memory_mb, 0.0);
583 }
584
585 #[test]
586 fn test_dev_tool_widget_variants() {
587 let text = DevToolWidget::Text {
588 content: "hello".to_string(),
589 color: [1.0, 1.0, 1.0, 1.0],
590 };
591 let graph = DevToolWidget::Graph {
592 data: vec![1.0, 2.0],
593 label: "test".to_string(),
594 };
595 let inspector = DevToolWidget::Inspector {
596 properties: vec![("k".to_string(), "v".to_string())],
597 };
598 let button = DevToolWidget::Button {
599 label: "click".to_string(),
600 clicked: false,
601 };
602 drop(text);
604 drop(graph);
605 drop(inspector);
606 drop(button);
607 }
608
609 #[test]
610 fn test_current_timestamp_nonzero() {
611 let ts = current_timestamp();
612 assert!(!ts.is_empty());
613 assert!(ts.parse::<u64>().is_ok());
614 }
615}