runmat_plot/gui/
plot_overlay.rs

1//! GUI overlay system for interactive plot controls and annotations
2//!
3//! This module handles the egui-based UI that sits on top of the WGPU-rendered
4//! plot content, providing controls, axis labels, grid lines, and titles.
5
6use crate::core::{plot_utils, PlotRenderer};
7use crate::styling::{ModernDarkTheme, PlotThemeConfig};
8use egui::{Align2, Color32, Context, FontId, Pos2, Rect, Stroke};
9
10/// GUI overlay manager for plot annotations and controls
11pub struct PlotOverlay {
12    /// Current theme
13    #[allow(dead_code)] // TODO: Use for theme customization
14    theme: PlotThemeConfig,
15
16    /// Cached plot area from last frame
17    plot_area: Option<Rect>,
18
19    /// Show debug information
20    show_debug: bool,
21
22    /// Show Dystr information modal
23    show_dystr_modal: bool,
24}
25
26/// Configuration for the plot overlay
27#[derive(Debug, Clone)]
28pub struct OverlayConfig {
29    /// Whether to show the sidebar with controls
30    pub show_sidebar: bool,
31
32    /// Whether to show grid lines
33    pub show_grid: bool,
34
35    /// Whether to show axis labels
36    pub show_axes: bool,
37
38    /// Whether to show plot title
39    pub show_title: bool,
40
41    /// Custom plot title (if any)
42    pub title: Option<String>,
43
44    /// Custom axis labels
45    pub x_label: Option<String>,
46    pub y_label: Option<String>,
47
48    /// Sidebar width
49    pub sidebar_width: f32,
50
51    /// Margins around plot area
52    pub plot_margins: PlotMargins,
53}
54
55#[derive(Debug, Clone)]
56pub struct PlotMargins {
57    pub left: f32,
58    pub right: f32,
59    pub top: f32,
60    pub bottom: f32,
61}
62
63impl Default for OverlayConfig {
64    fn default() -> Self {
65        Self {
66            show_sidebar: true,
67            show_grid: true,
68            show_axes: true,
69            show_title: true,
70            title: Some("Plot".to_string()),
71            x_label: Some("X".to_string()),
72            y_label: Some("Y".to_string()),
73            sidebar_width: 280.0,
74            plot_margins: PlotMargins {
75                left: 60.0,
76                right: 20.0,
77                top: 40.0,
78                bottom: 60.0,
79            },
80        }
81    }
82}
83
84/// Information about the current frame's UI state
85#[derive(Debug)]
86pub struct FrameInfo {
87    /// The plot area where WGPU should render
88    pub plot_area: Option<Rect>,
89
90    /// Whether the UI consumed any input events
91    pub consumed_input: bool,
92
93    /// Performance metrics to display
94    pub metrics: OverlayMetrics,
95}
96
97#[derive(Debug, Default)]
98pub struct OverlayMetrics {
99    pub vertex_count: usize,
100    pub triangle_count: usize,
101    pub render_time_ms: f64,
102    pub fps: f32,
103}
104
105impl Default for PlotOverlay {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl PlotOverlay {
112    /// Create a new plot overlay
113    pub fn new() -> Self {
114        Self {
115            theme: PlotThemeConfig::default(),
116            plot_area: None,
117            show_debug: false,
118            show_dystr_modal: false,
119        }
120    }
121
122    /// Apply theme to egui context
123    pub fn apply_theme(&self, ctx: &Context) {
124        // Apply the modern dark theme (using stored config for future customization)
125        let theme = ModernDarkTheme::default();
126        theme.apply_to_egui(ctx);
127
128        // Make context transparent so WGPU content shows through
129        let mut visuals = ctx.style().visuals.clone();
130        visuals.window_fill = Color32::TRANSPARENT;
131        visuals.panel_fill = Color32::TRANSPARENT;
132        ctx.set_visuals(visuals);
133    }
134
135    /// Render the complete overlay UI
136    pub fn render(
137        &mut self,
138        ctx: &Context,
139        plot_renderer: &PlotRenderer,
140        config: &OverlayConfig,
141        metrics: OverlayMetrics,
142    ) -> FrameInfo {
143        let mut consumed_input = false;
144        let mut plot_area = None;
145
146        // Render sidebar if enabled
147        if config.show_sidebar {
148            consumed_input |= self.render_sidebar(ctx, plot_renderer, config, &metrics);
149        }
150
151        // Render main plot area
152        let central_response = egui::CentralPanel::default()
153            .frame(egui::Frame::none()) // Transparent frame
154            .show(ctx, |ui| {
155                plot_area = Some(self.render_plot_area(ui, plot_renderer, config));
156            });
157
158        consumed_input |= central_response.response.hovered();
159
160        // Render Dystr modal if needed
161        if self.show_dystr_modal {
162            consumed_input |= self.render_dystr_modal(ctx);
163        }
164
165        // Store plot area for next frame
166        self.plot_area = plot_area;
167
168        FrameInfo {
169            plot_area,
170            consumed_input,
171            metrics,
172        }
173    }
174
175    /// Render the sidebar with controls and information
176    fn render_sidebar(
177        &mut self,
178        ctx: &Context,
179        plot_renderer: &PlotRenderer,
180        config: &OverlayConfig,
181        metrics: &OverlayMetrics,
182    ) -> bool {
183        let mut consumed_input = false;
184
185        let sidebar_response = egui::SidePanel::left("plot_controls")
186            .resizable(true)
187            .default_width(config.sidebar_width)
188            .min_width(200.0)
189            .show(ctx, |ui| {
190                ui.style_mut().visuals.widgets.noninteractive.bg_fill = Color32::from_gray(25);
191                ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::from_gray(35);
192                ui.style_mut().visuals.widgets.hovered.bg_fill = Color32::from_gray(45);
193
194                // Header with Dystr branding
195                ui.horizontal(|ui| {
196                    // Placeholder for Dystr logo (32x32 square)
197                    let logo_size = egui::Vec2::splat(32.0);
198                    let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::click()).0;
199
200                    // Draw placeholder logo background
201                    ui.painter().rect_filled(
202                        logo_rect,
203                        4.0, // rounded corners
204                        Color32::from_rgb(100, 100, 100),
205                    );
206
207                    // Draw "D" placeholder text in the logo area
208                    ui.painter().text(
209                        logo_rect.center(),
210                        Align2::CENTER_CENTER,
211                        "D",
212                        FontId::proportional(20.0),
213                        Color32::WHITE,
214                    );
215
216                    ui.vertical(|ui| {
217                        ui.heading("RunMat");
218                        ui.horizontal(|ui| {
219                            ui.small("a community project by ");
220                            if ui.small_button("dystr.com").clicked() {
221                                self.show_dystr_modal = true;
222                            }
223                        });
224                    });
225                });
226                ui.separator();
227                ui.label("GC Stats: [not available]");
228
229                // Camera information
230                ui.collapsing("📷 Camera", |ui| {
231                    let camera = plot_renderer.camera();
232                    ui.label(format!(
233                        "Position: {:.2}, {:.2}, {:.2}",
234                        camera.position.x, camera.position.y, camera.position.z
235                    ));
236                    ui.label(format!(
237                        "Target: {:.2}, {:.2}, {:.2}",
238                        camera.target.x, camera.target.y, camera.target.z
239                    ));
240
241                    if let Some(bounds) = plot_renderer.data_bounds() {
242                        ui.label(format!("Data X: {:.2} to {:.2}", bounds.0, bounds.1));
243                        ui.label(format!("Data Y: {:.2} to {:.2}", bounds.2, bounds.3));
244                    }
245                });
246
247                // Scene information
248                ui.collapsing("🎬 Scene", |ui| {
249                    let stats = plot_renderer.scene_statistics();
250                    ui.label(format!("Nodes: {}", stats.total_nodes));
251                    ui.label(format!("Visible: {}", stats.visible_nodes));
252                    ui.label(format!("Vertices: {}", stats.total_vertices));
253                    ui.label(format!("Triangles: {}", stats.total_triangles));
254                });
255
256                // Performance metrics
257                ui.collapsing("⚡ Performance", |ui| {
258                    ui.label(format!("FPS: {:.1}", metrics.fps));
259                    ui.label(format!("Render: {:.2}ms", metrics.render_time_ms));
260                    ui.label(format!("Vertices: {}", metrics.vertex_count));
261                    ui.label(format!("Triangles: {}", metrics.triangle_count));
262                });
263
264                // Theme selection
265                ui.collapsing("🎨 Theme", |ui| {
266                    ui.label("Modern Dark (Active)");
267                    ui.checkbox(&mut self.show_debug, "Show Debug Info");
268                });
269
270                ui.separator();
271
272                // Plot controls
273                ui.collapsing("🔧 Controls", |ui| {
274                    ui.label("🖱️ Left drag: Rotate");
275                    ui.label("🖱️ Right drag: Pan");
276                    ui.label("🖱️ Scroll: Zoom");
277                    ui.label("📱 Touch: Pinch to zoom");
278                });
279            });
280
281        consumed_input |= sidebar_response.response.hovered();
282        consumed_input
283    }
284
285    /// Render the main plot area with grid, axes, and annotations
286    fn render_plot_area(
287        &mut self,
288        ui: &mut egui::Ui,
289        plot_renderer: &PlotRenderer,
290        config: &OverlayConfig,
291    ) -> Rect {
292        let available_rect = ui.available_rect_before_wrap();
293
294        // Calculate plot area with margins
295        let plot_rect = Rect::from_min_size(
296            available_rect.min + egui::Vec2::new(config.plot_margins.left, config.plot_margins.top),
297            available_rect.size()
298                - egui::Vec2::new(
299                    config.plot_margins.left + config.plot_margins.right,
300                    config.plot_margins.top + config.plot_margins.bottom,
301                ),
302        );
303
304        // Ensure square aspect ratio for better plots
305        let size = plot_rect.width().min(plot_rect.height());
306        let centered_plot_rect =
307            Rect::from_center_size(plot_rect.center(), egui::Vec2::splat(size));
308
309        // Draw plot frame
310        ui.painter().rect_stroke(
311            centered_plot_rect,
312            0.0,
313            Stroke::new(1.5, Color32::from_gray(180)),
314        );
315
316        // Draw grid if enabled
317        if config.show_grid {
318            self.draw_grid(ui, centered_plot_rect, plot_renderer);
319        }
320
321        // Draw axes if enabled
322        if config.show_axes {
323            self.draw_axes(ui, centered_plot_rect, plot_renderer, config);
324        }
325
326        // Draw title if enabled
327        if config.show_title {
328            if let Some(title) = &config.title {
329                self.draw_title(ui, centered_plot_rect, title);
330            }
331        }
332
333        // Draw axis labels
334        if let Some(x_label) = &config.x_label {
335            self.draw_x_label(ui, centered_plot_rect, x_label);
336        }
337        if let Some(y_label) = &config.y_label {
338            self.draw_y_label(ui, centered_plot_rect, y_label);
339        }
340
341        centered_plot_rect
342    }
343
344    /// Draw grid lines based on data bounds
345    fn draw_grid(&self, ui: &mut egui::Ui, plot_rect: Rect, plot_renderer: &PlotRenderer) {
346        if let Some(data_bounds) = plot_renderer.data_bounds() {
347            let grid_color_major = Color32::from_gray(80);
348            let _grid_color_minor = Color32::from_gray(60);
349
350            let (x_min, x_max, y_min, y_max) = data_bounds;
351            let x_range = x_max - x_min;
352            let y_range = y_max - y_min;
353
354            // Calculate tick intervals
355            let x_tick_interval = plot_utils::calculate_tick_interval(x_range);
356            let y_tick_interval = plot_utils::calculate_tick_interval(y_range);
357
358            // Draw vertical grid lines
359            let mut x_val = (x_min / x_tick_interval).ceil() * x_tick_interval;
360            while x_val <= x_max {
361                let x_screen =
362                    plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
363                ui.painter().line_segment(
364                    [
365                        Pos2::new(x_screen, plot_rect.min.y),
366                        Pos2::new(x_screen, plot_rect.max.y),
367                    ],
368                    Stroke::new(0.8, grid_color_major),
369                );
370                x_val += x_tick_interval;
371            }
372
373            // Draw horizontal grid lines
374            let mut y_val = (y_min / y_tick_interval).ceil() * y_tick_interval;
375            while y_val <= y_max {
376                let y_screen =
377                    plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
378                ui.painter().line_segment(
379                    [
380                        Pos2::new(plot_rect.min.x, y_screen),
381                        Pos2::new(plot_rect.max.x, y_screen),
382                    ],
383                    Stroke::new(0.8, grid_color_major),
384                );
385                y_val += y_tick_interval;
386            }
387        }
388    }
389
390    /// Draw axis ticks and numeric labels
391    fn draw_axes(
392        &self,
393        ui: &mut egui::Ui,
394        plot_rect: Rect,
395        plot_renderer: &PlotRenderer,
396        _config: &OverlayConfig,
397    ) {
398        if let Some(data_bounds) = plot_renderer.data_bounds() {
399            let (x_min, x_max, y_min, y_max) = data_bounds;
400            let x_range = x_max - x_min;
401            let y_range = y_max - y_min;
402            let tick_length = 6.0;
403            let label_offset = 15.0;
404
405            // Calculate tick intervals
406            let x_tick_interval = plot_utils::calculate_tick_interval(x_range);
407            let y_tick_interval = plot_utils::calculate_tick_interval(y_range);
408
409            // Draw X-axis ticks and labels
410            let mut x_val = (x_min / x_tick_interval).ceil() * x_tick_interval;
411            while x_val <= x_max {
412                let x_screen =
413                    plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
414
415                // Tick mark
416                ui.painter().line_segment(
417                    [
418                        Pos2::new(x_screen, plot_rect.max.y),
419                        Pos2::new(x_screen, plot_rect.max.y + tick_length),
420                    ],
421                    Stroke::new(1.0, Color32::WHITE),
422                );
423
424                // Label
425                ui.painter().text(
426                    Pos2::new(x_screen, plot_rect.max.y + label_offset),
427                    Align2::CENTER_CENTER,
428                    plot_utils::format_tick_label(x_val),
429                    FontId::proportional(10.0),
430                    Color32::from_gray(200),
431                );
432
433                x_val += x_tick_interval;
434            }
435
436            // Draw Y-axis ticks and labels
437            let mut y_val = (y_min / y_tick_interval).ceil() * y_tick_interval;
438            while y_val <= y_max {
439                let y_screen =
440                    plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
441
442                // Tick mark
443                ui.painter().line_segment(
444                    [
445                        Pos2::new(plot_rect.min.x - tick_length, y_screen),
446                        Pos2::new(plot_rect.min.x, y_screen),
447                    ],
448                    Stroke::new(1.0, Color32::WHITE),
449                );
450
451                // Label
452                ui.painter().text(
453                    Pos2::new(plot_rect.min.x - label_offset, y_screen),
454                    Align2::CENTER_CENTER,
455                    plot_utils::format_tick_label(y_val),
456                    FontId::proportional(10.0),
457                    Color32::from_gray(200),
458                );
459
460                y_val += y_tick_interval;
461            }
462        }
463    }
464
465    /// Draw plot title
466    fn draw_title(&self, ui: &mut egui::Ui, plot_rect: Rect, title: &str) {
467        ui.painter().text(
468            Pos2::new(plot_rect.center().x, plot_rect.min.y - 20.0),
469            Align2::CENTER_CENTER,
470            title,
471            FontId::proportional(16.0),
472            Color32::WHITE,
473        );
474    }
475
476    /// Draw X-axis label
477    fn draw_x_label(&self, ui: &mut egui::Ui, plot_rect: Rect, label: &str) {
478        ui.painter().text(
479            Pos2::new(plot_rect.center().x, plot_rect.max.y + 40.0),
480            Align2::CENTER_CENTER,
481            label,
482            FontId::proportional(14.0),
483            Color32::WHITE,
484        );
485    }
486
487    /// Draw Y-axis label
488    fn draw_y_label(&self, ui: &mut egui::Ui, plot_rect: Rect, label: &str) {
489        ui.painter().text(
490            Pos2::new(plot_rect.min.x - 40.0, plot_rect.center().y),
491            Align2::CENTER_CENTER,
492            label,
493            FontId::proportional(14.0),
494            Color32::WHITE,
495        );
496    }
497
498    /// Get the plot area from the last frame
499    pub fn plot_area(&self) -> Option<Rect> {
500        self.plot_area
501    }
502
503    /// Render the Dystr information modal
504    fn render_dystr_modal(&mut self, ctx: &Context) -> bool {
505        let mut consumed_input = false;
506
507        egui::Window::new("About Dystr")
508            .anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
509            .collapsible(false)
510            .resizable(false)
511            .default_width(400.0)
512            .show(ctx, |ui| {
513                consumed_input = true;
514
515                ui.vertical_centered(|ui| {
516                    ui.add_space(10.0);
517
518                    // Dystr logo placeholder (larger for modal)
519                    let logo_size = egui::Vec2::splat(64.0);
520                    let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::hover()).0;
521
522                    ui.painter().rect_filled(
523                        logo_rect,
524                        8.0,                             // rounded corners
525                        Color32::from_rgb(60, 130, 200), // Dystr brand color placeholder
526                    );
527
528                    ui.painter().text(
529                        logo_rect.center(),
530                        Align2::CENTER_CENTER,
531                        "D",
532                        FontId::proportional(40.0),
533                        Color32::WHITE,
534                    );
535
536                    ui.add_space(15.0);
537
538                    ui.heading("Welcome to RunMat");
539                    ui.add_space(10.0);
540
541                    ui.label("RunMat is a high-performance MATLAB-compatible");
542                    ui.label("numerical computing platform, built as part of");
543                    ui.label("the Dystr computation ecosystem.");
544
545                    ui.add_space(15.0);
546
547                    ui.label("🚀 V8-inspired JIT compilation");
548                    ui.label("⚡ BLAS/LAPACK acceleration");
549                    ui.label("🎯 Full MATLAB compatibility");
550                    ui.label("🔬 Advanced plotting & visualization");
551
552                    ui.add_space(20.0);
553
554                    ui.horizontal(|ui| {
555                        if ui.button("Visit dystr.com").clicked() {
556                            // Open dystr.com in browser
557                            if let Err(e) = webbrowser::open("https://dystr.com") {
558                                eprintln!("Failed to open browser: {e}");
559                            }
560                        }
561
562                        if ui.button("Close").clicked() {
563                            self.show_dystr_modal = false;
564                        }
565                    });
566
567                    ui.add_space(10.0);
568                });
569            });
570
571        consumed_input
572    }
573}