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
324pub fn capture_metrics() -> PerfMetrics {
340 PerfMetrics::default()
343}
344
345pub fn format_log_entry(entry: &LogEntry) -> String {
366 let level_str = match entry.level {
367 LogLevel::Debug => "DEBUG",
368 LogLevel::Info => "INFO",
369 LogLevel::Warn => "WARN",
370 LogLevel::Error => "ERROR",
371 };
372 format!("[{}] {}: {}", entry.timestamp, level_str, entry.message)
373}
374
375pub fn current_timestamp() -> String {
379 match SystemTime::now().duration_since(UNIX_EPOCH) {
380 Ok(d) => format!("{}", d.as_secs()),
381 Err(_) => "0".to_string(),
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_dashboard_new() {
391 let dashboard = DevToolsDashboard::new();
392 assert!(!dashboard.visible);
393 assert_eq!(dashboard.panels.len(), 2);
394 assert_eq!(dashboard.active_panel, 0);
395 assert_eq!(dashboard.panels[0].title, "Performance");
396 assert_eq!(dashboard.panels[1].title, "Logs");
397 }
398
399 #[test]
400 fn test_dashboard_toggle() {
401 let mut dashboard = DevToolsDashboard::new();
402 assert!(!dashboard.visible);
403 dashboard.toggle();
404 assert!(dashboard.visible);
405 dashboard.toggle();
406 assert!(!dashboard.visible);
407 }
408
409 #[test]
410 fn test_dashboard_add_panel() {
411 let mut dashboard = DevToolsDashboard::new();
412 dashboard.add_panel(Panel::new("Inspector", PanelContent::NodeInspector));
413 assert_eq!(dashboard.panels.len(), 3);
414 assert_eq!(dashboard.panels[2].title, "Inspector");
415 assert_eq!(dashboard.panels[2].content, PanelContent::NodeInspector);
416 }
417
418 #[test]
419 fn test_dashboard_remove_panel() {
420 let mut dashboard = DevToolsDashboard::new();
421 dashboard.add_panel(Panel::new("Extra", PanelContent::GraphView));
422 assert_eq!(dashboard.panels.len(), 3);
423 dashboard.remove_panel(2);
424 assert_eq!(dashboard.panels.len(), 2);
425 }
426
427 #[test]
428 fn test_dashboard_remove_panel_out_of_bounds() {
429 let mut dashboard = DevToolsDashboard::new();
430 dashboard.remove_panel(99);
431 assert_eq!(dashboard.panels.len(), 2);
432 }
433
434 #[test]
435 fn test_dashboard_remove_active_panel_clamps() {
436 let mut dashboard = DevToolsDashboard::new();
437 dashboard.set_active(1);
438 assert_eq!(dashboard.active_panel, 1);
439 dashboard.remove_panel(1);
440 assert_eq!(dashboard.active_panel, 0);
442 }
443
444 #[test]
445 fn test_dashboard_set_active() {
446 let mut dashboard = DevToolsDashboard::new();
447 dashboard.add_panel(Panel::new("Third", PanelContent::ThemeEditor));
448 dashboard.set_active(2);
449 assert_eq!(dashboard.active_panel, 2);
450 }
451
452 #[test]
453 fn test_dashboard_set_active_out_of_bounds() {
454 let mut dashboard = DevToolsDashboard::new();
455 dashboard.set_active(99);
456 assert_eq!(dashboard.active_panel, 0);
457 }
458
459 #[test]
460 fn test_dashboard_render_hidden() {
461 let dashboard = DevToolsDashboard::new();
462 let widgets = dashboard.render();
463 assert!(widgets.is_empty());
464 }
465
466 #[test]
467 fn test_dashboard_render_visible() {
468 let mut dashboard = DevToolsDashboard::new();
469 dashboard.toggle();
470 let widgets = dashboard.render();
471 assert!(!widgets.is_empty());
473 }
474
475 #[test]
476 fn test_panel_new() {
477 let panel = Panel::new("Test", PanelContent::GraphView);
478 assert_eq!(panel.title, "Test");
479 assert_eq!(panel.content, PanelContent::GraphView);
480 assert_eq!(panel.width, 1.0);
481 assert_eq!(panel.height, 1.0);
482 }
483
484 #[test]
485 fn test_panel_with_size() {
486 let panel = Panel::new("Sized", PanelContent::LogView).with_size(0.5, 0.75);
487 assert_eq!(panel.width, 0.5);
488 assert_eq!(panel.height, 0.75);
489 }
490
491 #[test]
492 fn test_panel_with_size_clamped() {
493 let panel = Panel::new("Clamped", PanelContent::LogView).with_size(1.5, -0.5);
494 assert_eq!(panel.width, 1.0);
495 assert_eq!(panel.height, 0.0);
496 }
497
498 #[test]
499 fn test_capture_metrics() {
500 let metrics = capture_metrics();
501 assert_eq!(metrics.frame_time_ms, 0.0);
502 assert_eq!(metrics.fps, 0.0);
503 assert_eq!(metrics.node_count, 0);
504 assert_eq!(metrics.edge_count, 0);
505 assert_eq!(metrics.gpu_memory_mb, 0.0);
506 }
507
508 #[test]
509 fn test_format_log_entry_info() {
510 let entry = LogEntry {
511 timestamp: "2025-01-01T00:00:00Z".to_string(),
512 level: LogLevel::Info,
513 message: "Application started".to_string(),
514 };
515 assert_eq!(
516 format_log_entry(&entry),
517 "[2025-01-01T00:00:00Z] INFO: Application started"
518 );
519 }
520
521 #[test]
522 fn test_format_log_entry_debug() {
523 let entry = LogEntry {
524 timestamp: "T1".to_string(),
525 level: LogLevel::Debug,
526 message: "debug msg".to_string(),
527 };
528 assert_eq!(format_log_entry(&entry), "[T1] DEBUG: debug msg");
529 }
530
531 #[test]
532 fn test_format_log_entry_warn() {
533 let entry = LogEntry {
534 timestamp: "T2".to_string(),
535 level: LogLevel::Warn,
536 message: "watch out".to_string(),
537 };
538 assert_eq!(format_log_entry(&entry), "[T2] WARN: watch out");
539 }
540
541 #[test]
542 fn test_format_log_entry_error() {
543 let entry = LogEntry {
544 timestamp: "T3".to_string(),
545 level: LogLevel::Error,
546 message: "something broke".to_string(),
547 };
548 assert_eq!(format_log_entry(&entry), "[T3] ERROR: something broke");
549 }
550
551 #[test]
552 fn test_log_level_ordering() {
553 assert!(LogLevel::Debug < LogLevel::Info);
554 assert!(LogLevel::Info < LogLevel::Warn);
555 assert!(LogLevel::Warn < LogLevel::Error);
556 }
557
558 #[test]
559 fn test_perf_metrics_default() {
560 let m = PerfMetrics::default();
561 assert_eq!(m.frame_time_ms, 0.0);
562 assert_eq!(m.fps, 0.0);
563 assert_eq!(m.node_count, 0);
564 assert_eq!(m.edge_count, 0);
565 assert_eq!(m.gpu_memory_mb, 0.0);
566 }
567
568 #[test]
569 fn test_dev_tool_widget_variants() {
570 let text = DevToolWidget::Text {
571 content: "hello".to_string(),
572 color: [1.0, 1.0, 1.0, 1.0],
573 };
574 let graph = DevToolWidget::Graph {
575 data: vec![1.0, 2.0],
576 label: "test".to_string(),
577 };
578 let inspector = DevToolWidget::Inspector {
579 properties: vec![("k".to_string(), "v".to_string())],
580 };
581 let button = DevToolWidget::Button {
582 label: "click".to_string(),
583 clicked: false,
584 };
585 drop(text);
587 drop(graph);
588 drop(inspector);
589 drop(button);
590 }
591
592 #[test]
593 fn test_current_timestamp_nonzero() {
594 let ts = current_timestamp();
595 assert!(!ts.is_empty());
596 assert!(ts.parse::<u64>().is_ok());
597 }
598}