Skip to main content

runmat_plot/gui/
window_impl.rs

1//! Implementation methods for the GUI plot window
2
3#[cfg(feature = "gui")]
4use super::plot_overlay::{OverlayConfig, OverlayMetrics, PlotOverlay};
5#[cfg(feature = "gui")]
6use super::{PlotWindow, WindowConfig};
7#[cfg(feature = "gui")]
8use crate::core::PipelineType;
9#[cfg(feature = "gui")]
10use egui_winit::State as EguiState;
11#[cfg(feature = "gui")]
12use glam::{Mat4, Vec2, Vec3, Vec4};
13#[cfg(feature = "gui")]
14use runmat_time::Instant;
15#[cfg(feature = "gui")]
16use std::sync::atomic::{AtomicBool, Ordering};
17#[cfg(feature = "gui")]
18use std::sync::Arc;
19#[cfg(feature = "gui")]
20use tracing::debug;
21#[cfg(feature = "gui")]
22use winit::{dpi::PhysicalSize, event::Event, event_loop::EventLoop, window::WindowBuilder};
23#[cfg(feature = "gui")]
24impl<'window> PlotWindow<'window> {
25    fn update_subplot_camera_aspects_for_rect(&mut self, plot_rect: egui::Rect) {
26        let (rows, cols) = self.plot_renderer.figure_axes_grid();
27        if rows * cols <= 1 {
28            let plot_width = plot_rect.width();
29            let plot_height = plot_rect.height();
30            if plot_width > 0.0 && plot_height > 0.0 {
31                self.plot_renderer
32                    .camera_mut()
33                    .update_aspect_ratio(plot_width / plot_height);
34            }
35            return;
36        }
37        let rects: Vec<egui::Rect> = if self.plot_overlay.axes_plot_rects().len() == rows * cols {
38            self.plot_overlay.axes_plot_rects().to_vec()
39        } else {
40            self.plot_overlay
41                .compute_subplot_plot_rects(plot_rect, &self.plot_renderer, 1.0)
42        };
43        for (i, r) in rects.iter().enumerate() {
44            let w = r.width();
45            let h = r.height();
46            if w > 0.0 && h > 0.0 {
47                if let Some(cam) = self.plot_renderer.axes_camera_mut(i) {
48                    cam.update_aspect_ratio(w / h);
49                }
50            }
51        }
52    }
53
54    /// Create a new interactive plot window
55    pub async fn new(config: WindowConfig) -> Result<Self, Box<dyn std::error::Error>> {
56        // Create a new EventLoop (assumes this is the only EventLoop creation)
57        let event_loop =
58            EventLoop::new().map_err(|e| format!("Failed to create EventLoop: {e}"))?;
59        let window = WindowBuilder::new()
60            .with_title(&config.title)
61            .with_inner_size(PhysicalSize::new(config.width, config.height))
62            .with_resizable(config.resizable)
63            .with_maximized(config.maximized)
64            .build(&event_loop)?;
65        let window = Arc::new(window);
66
67        // Reuse shared context when available; fall back to creating a dedicated device otherwise.
68        let shared_ctx = crate::context::shared_wgpu_context();
69        let (instance, surface, shared_ctx) = if let Some(ctx) = shared_ctx {
70            let surface = ctx.instance.create_surface(window.clone())?;
71            (ctx.instance.clone(), surface, Some(ctx))
72        } else {
73            let instance = Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor {
74                backends: wgpu::Backends::all(),
75                ..Default::default()
76            }));
77            let surface = instance.create_surface(window.clone())?;
78            (instance, surface, None)
79        };
80
81        let (adapter, device, queue) = if let Some(ctx) = shared_ctx {
82            (ctx.adapter, ctx.device, ctx.queue)
83        } else {
84            let adapter = instance
85                .request_adapter(&wgpu::RequestAdapterOptions {
86                    power_preference: wgpu::PowerPreference::HighPerformance,
87                    compatible_surface: Some(&surface),
88                    force_fallback_adapter: false,
89                })
90                .await
91                .ok_or("Failed to request adapter")?;
92
93            let (device, queue) = adapter
94                .request_device(
95                    &wgpu::DeviceDescriptor {
96                        label: Some("RunMat Plot Device"),
97                        required_features: wgpu::Features::empty(),
98                        required_limits: wgpu::Limits::default(),
99                    },
100                    None,
101                )
102                .await?;
103
104            (Arc::new(adapter), Arc::new(device), Arc::new(queue))
105        };
106
107        // Configure surface
108        let surface_caps = surface.get_capabilities(adapter.as_ref());
109        let surface_format = surface_caps
110            .formats
111            .iter()
112            .find(|f| f.is_srgb())
113            .copied()
114            .unwrap_or(surface_caps.formats[0]);
115
116        let surface_config = wgpu::SurfaceConfiguration {
117            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
118            format: surface_format,
119            width: config.width,
120            height: config.height,
121            present_mode: if config.vsync {
122                wgpu::PresentMode::AutoVsync
123            } else {
124                wgpu::PresentMode::AutoNoVsync
125            },
126            alpha_mode: surface_caps.alpha_modes[0],
127            view_formats: vec![],
128            desired_maximum_frame_latency: 2,
129        };
130        surface.configure(&device, &surface_config);
131
132        // Create depth texture
133        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
134            label: Some("Depth Texture"),
135            size: wgpu::Extent3d {
136                width: config.width,
137                height: config.height,
138                depth_or_array_layers: 1,
139            },
140            mip_level_count: 1,
141            sample_count: 1,
142            dimension: wgpu::TextureDimension::D2,
143            format: wgpu::TextureFormat::Depth32Float,
144            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
145            view_formats: &[],
146        });
147
148        let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
149
150        // Create unified plot renderer
151        let plot_renderer =
152            crate::core::PlotRenderer::new(device.clone(), queue.clone(), surface_config).await?;
153        let plot_overlay = crate::gui::PlotOverlay::new();
154
155        // Setup egui with modern dark theme
156        let egui_ctx = egui::Context::default();
157
158        // Apply our beautiful modern dark theme to egui
159        let theme = crate::styling::ModernDarkTheme::default();
160        theme.apply_to_egui(&egui_ctx);
161
162        let egui_state = EguiState::new(
163            egui_ctx.clone(),
164            egui::viewport::ViewportId::ROOT,
165            &window,
166            Some(window.scale_factor() as f32),
167            None,
168        );
169
170        let egui_renderer = egui_wgpu::Renderer::new(
171            &device,
172            surface_format,
173            None, // egui doesn't need depth buffer
174            1,
175        );
176
177        Ok(Self {
178            window,
179            event_loop: Some(event_loop),
180            plot_renderer,
181            plot_overlay,
182            surface,
183            depth_texture,
184            depth_view,
185            egui_ctx,
186            egui_state,
187            egui_renderer,
188            config,
189            mouse_position: Vec2::ZERO,
190            is_mouse_over_plot: true,
191            needs_initial_redraw: true,
192            pixels_per_point: 1.0,
193            mouse_left_down: false,
194            active_drag_axes: None,
195            close_signal: None,
196        })
197    }
198
199    /// Add a simple line plot to the scene for testing
200    pub fn add_test_plot(&mut self) {
201        use crate::core::vertex_utils;
202
203        // Create some test data
204        let x_data: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
205        let y_data: Vec<f64> = x_data.iter().map(|x| x.sin()).collect();
206
207        // Create vertices for the line plot
208        let vertices =
209            vertex_utils::create_line_plot(&x_data, &y_data, Vec4::new(0.0, 0.5, 1.0, 1.0));
210
211        // Create a scene node
212        let mut render_data = crate::core::RenderData {
213            pipeline_type: PipelineType::Lines,
214            vertices,
215            indices: None,
216            gpu_vertices: None,
217            bounds: None,
218            material: crate::core::Material::default(),
219            draw_calls: vec![crate::core::DrawCall {
220                vertex_offset: 0,
221                vertex_count: (x_data.len() - 1) * 2, // Each line segment has 2 vertices
222                index_offset: None,
223                index_count: None,
224                instance_count: 1,
225            }],
226            image: None,
227        };
228
229        // Set material color
230        render_data.material.albedo = Vec4::new(0.0, 0.5, 1.0, 1.0);
231
232        let node = crate::core::SceneNode {
233            id: 0, // Will be set by scene
234            name: "Test Line Plot".to_string(),
235            transform: Mat4::IDENTITY,
236            visible: true,
237            cast_shadows: false,
238            receive_shadows: false,
239            axes_index: 0,
240            parent: None,
241            children: Vec::new(),
242            render_data: Some(render_data),
243            bounds: crate::core::BoundingBox::from_points(
244                &x_data
245                    .iter()
246                    .zip(y_data.iter())
247                    .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
248                    .collect::<Vec<_>>(),
249            ),
250            lod_levels: Vec::new(),
251            current_lod: 0,
252        };
253
254        self.plot_renderer.scene.add_node(node);
255
256        // Fit camera to show the plot
257        let bounds_min = Vec3::new(-1.0, -1.5, -1.0);
258        let bounds_max = Vec3::new(10.0, 1.5, 1.0);
259        self.plot_renderer
260            .camera_mut()
261            .fit_bounds(bounds_min, bounds_max);
262    }
263
264    /// Set the figure to display in this window (clears existing content)
265    pub fn set_figure(&mut self, figure: crate::plots::Figure) {
266        // Use the unified plot renderer
267        self.plot_renderer.set_figure(figure);
268    }
269
270    /// Attach a signal that lets external callers request the window to close.
271    pub fn install_close_signal(&mut self, signal: Arc<AtomicBool>) {
272        self.close_signal = Some(signal);
273    }
274
275    /// Run the interactive plot window event loop
276    pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
277        let event_loop = self
278            .event_loop
279            .take()
280            .ok_or("Event loop already consumed")?;
281        let window = self.window.clone();
282        let mut last_render_time = Instant::now();
283        let close_signal = self.close_signal.clone();
284
285        event_loop.run(move |event, target| {
286            if let Some(signal) = close_signal.as_ref() {
287                if signal.load(Ordering::Relaxed) {
288                    target.exit();
289                    return;
290                }
291            }
292            target.set_control_flow(winit::event_loop::ControlFlow::Poll);
293
294            // Track current modifiers for Command/Ctrl shortcuts
295            static mut MODIFIERS: Option<winit::keyboard::ModifiersState> = None;
296
297            // Handle egui events and record consumption
298            let mut repaint = false;
299            let mut egui_consumed = false;
300            if let Event::WindowEvent { ref event, .. } = event {
301                let response = self.egui_state.on_window_event(&window, event);
302                repaint = response.repaint;
303                egui_consumed = response.consumed;
304            }
305            if repaint {
306                window.request_redraw();
307            }
308
309            match event {
310                winit::event::Event::WindowEvent {
311                    window_id,
312                    event: winit::event::WindowEvent::ModifiersChanged(mods),
313                } if window_id == window.id() => unsafe {
314                    MODIFIERS = Some(mods.state());
315                },
316                winit::event::Event::WindowEvent {
317                    window_id,
318                    event: winit::event::WindowEvent::CloseRequested,
319                } if window_id == window.id() => {
320                    target.exit();
321                }
322
323                winit::event::Event::WindowEvent {
324                    window_id,
325                    event: winit::event::WindowEvent::Resized(new_size),
326                } if window_id == window.id() => {
327                    // Resize surface and depth texture
328                    if new_size.width > 0 && new_size.height > 0 {
329                        self.resize(new_size.width, new_size.height);
330                    }
331                }
332
333                winit::event::Event::WindowEvent {
334                    window_id,
335                    event: winit::event::WindowEvent::RedrawRequested,
336                } if window_id == window.id() => {
337                    let now = Instant::now();
338                    let dt = now - last_render_time;
339                    last_render_time = now;
340
341                    match self.render(dt) {
342                        Ok(_) => {}
343                        Err(wgpu::SurfaceError::Lost) => {
344                            self.resize(self.config.width, self.config.height)
345                        }
346                        Err(wgpu::SurfaceError::OutOfMemory) => target.exit(),
347                        Err(e) => eprintln!("Render error: {e:?}"),
348                    }
349                }
350
351                // Exit on Escape key for quick UX
352                winit::event::Event::WindowEvent {
353                    window_id,
354                    event:
355                        winit::event::WindowEvent::KeyboardInput {
356                            event: key_event, ..
357                        },
358                } if window_id == window.id() => {
359                    if key_event.state == winit::event::ElementState::Pressed {
360                        if let winit::keyboard::PhysicalKey::Code(
361                            winit::keyboard::KeyCode::Escape,
362                        ) = key_event.physical_key
363                        {
364                            target.exit();
365                        }
366                        // macOS-like Command+Q (and Ctrl+Q on other platforms) to quit
367                        if let Some(text) = key_event.text {
368                            if text == "\u{11}" { /* ignore control chars */ }
369                        }
370                        // Handle Q with Command or Control modifier
371                        if let winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::KeyQ) =
372                            key_event.physical_key
373                        {
374                            let mods = unsafe {
375                                MODIFIERS.unwrap_or_else(winit::keyboard::ModifiersState::empty)
376                            };
377                            if mods.super_key() || mods.control_key() {
378                                target.exit();
379                            }
380                        }
381                    }
382                }
383
384                winit::event::Event::WindowEvent {
385                    window_id,
386                    event: winit::event::WindowEvent::MouseInput { button, state, .. },
387                } if window_id == window.id() => {
388                    // Allow interactions inside plot even if egui reports consumed elsewhere
389                    let mut route = !egui_consumed;
390                    if let Some(plot_rect) = self.plot_overlay.plot_area() {
391                        let ppp = self.pixels_per_point.max(0.5);
392                        let mx = self.mouse_position.x;
393                        let my = self.mouse_position.y;
394                        let px_min_x = plot_rect.min.x * ppp;
395                        let px_min_y = plot_rect.min.y * ppp;
396                        let px_w = plot_rect.width() * ppp;
397                        let px_h = plot_rect.height() * ppp;
398                        if mx >= px_min_x
399                            && mx <= px_min_x + px_w
400                            && my >= px_min_y
401                            && my <= px_min_y + px_h
402                        {
403                            route = true;
404                            if let Some(tb) = self.plot_overlay.toolbar_rect() {
405                                if my >= tb.min.y * ppp && my <= tb.max.y * ppp {
406                                    route = false;
407                                }
408                            }
409                            if let Some(sb) = self.plot_overlay.sidebar_rect() {
410                                if mx >= sb.min.x * ppp
411                                    && mx <= sb.max.x * ppp
412                                    && my >= sb.min.y * ppp
413                                    && my <= sb.max.y * ppp
414                                {
415                                    route = false;
416                                }
417                            }
418                        }
419                    }
420                    if route {
421                        // Track left button state to avoid stray pan starts
422                        use winit::event::{ElementState, MouseButton};
423                        if button == MouseButton::Left {
424                            self.mouse_left_down = state == ElementState::Pressed;
425                        }
426                        self.handle_mouse_input(button, state);
427                        window.request_redraw();
428                    }
429                }
430
431                winit::event::Event::WindowEvent {
432                    window_id,
433                    event: winit::event::WindowEvent::CursorMoved { position, .. },
434                } if window_id == window.id() => {
435                    let mut route = !egui_consumed;
436                    if let Some(plot_rect) = self.plot_overlay.plot_area() {
437                        let ppp = self.pixels_per_point.max(0.5);
438                        let mx = position.x as f32;
439                        let my = position.y as f32;
440                        let px_min_x = plot_rect.min.x * ppp;
441                        let px_min_y = plot_rect.min.y * ppp;
442                        let px_w = plot_rect.width() * ppp;
443                        let px_h = plot_rect.height() * ppp;
444                        if mx >= px_min_x
445                            && mx <= px_min_x + px_w
446                            && my >= px_min_y
447                            && my <= px_min_y + px_h
448                        {
449                            route = true;
450                        }
451                    }
452                    if route {
453                        self.handle_mouse_move(position);
454                        window.request_redraw();
455                    }
456                }
457
458                winit::event::Event::WindowEvent {
459                    window_id,
460                    event: winit::event::WindowEvent::MouseWheel { delta, .. },
461                } if window_id == window.id() => {
462                    let mut route = !egui_consumed;
463                    if let Some(plot_rect) = self.plot_overlay.plot_area() {
464                        let ppp = self.pixels_per_point.max(0.5);
465                        let mx = self.mouse_position.x;
466                        let my = self.mouse_position.y;
467                        let px_min_x = plot_rect.min.x * ppp;
468                        let px_min_y = plot_rect.min.y * ppp;
469                        let px_w = plot_rect.width() * ppp;
470                        let px_h = plot_rect.height() * ppp;
471                        if mx >= px_min_x
472                            && mx <= px_min_x + px_w
473                            && my >= px_min_y
474                            && my <= px_min_y + px_h
475                        {
476                            route = true;
477                        }
478                    }
479                    if route {
480                        self.handle_mouse_scroll(delta);
481                        window.request_redraw();
482                    }
483                }
484
485                winit::event::Event::AboutToWait => {
486                    // Always request the first redraw; afterwards, only redraw when needed
487                    if self.needs_initial_redraw || repaint {
488                        self.needs_initial_redraw = false;
489                        window.request_redraw();
490                    }
491                }
492
493                _ => {}
494            }
495        })?;
496
497        Ok(())
498    }
499
500    /// Handle window resize
501    fn resize(&mut self, width: u32, height: u32) {
502        if width == 0 || height == 0 {
503            return; // Skip invalid sizes that could cause crashes
504        }
505
506        self.config.width = width;
507        self.config.height = height;
508
509        // Recreate surface configuration with error handling
510        let surface_config = wgpu::SurfaceConfiguration {
511            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
512            format: self.plot_renderer.wgpu_renderer.surface_config.format,
513            width,
514            height,
515            present_mode: if self.config.vsync {
516                wgpu::PresentMode::AutoVsync
517            } else {
518                wgpu::PresentMode::AutoNoVsync
519            },
520            alpha_mode: wgpu::CompositeAlphaMode::Auto,
521            view_formats: vec![],
522            desired_maximum_frame_latency: 2,
523        };
524
525        // Update renderer's surface config
526        self.plot_renderer.wgpu_renderer.surface_config = surface_config.clone();
527        self.surface
528            .configure(&self.plot_renderer.wgpu_renderer.device, &surface_config);
529
530        // Recreate depth texture
531        self.depth_texture =
532            self.plot_renderer
533                .wgpu_renderer
534                .device
535                .create_texture(&wgpu::TextureDescriptor {
536                    label: Some("Depth Texture"),
537                    size: wgpu::Extent3d {
538                        width,
539                        height,
540                        depth_or_array_layers: 1,
541                    },
542                    mip_level_count: 1,
543                    sample_count: 1,
544                    dimension: wgpu::TextureDimension::D2,
545                    format: wgpu::TextureFormat::Depth32Float,
546                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
547                        | wgpu::TextureUsages::TEXTURE_BINDING,
548                    view_formats: &[],
549                });
550
551        self.depth_view = self
552            .depth_texture
553            .create_view(&wgpu::TextureViewDescriptor::default());
554
555        let (rows, cols) = self.plot_renderer.figure_axes_grid();
556        if rows * cols > 1 {
557            if let Some(plot_rect) = self.plot_overlay.plot_area() {
558                self.update_subplot_camera_aspects_for_rect(plot_rect);
559            }
560        } else {
561            self.plot_renderer
562                .camera_mut()
563                .update_aspect_ratio(width as f32 / height as f32);
564        }
565    }
566
567    /// Render a frame
568    fn render(&mut self, _dt: std::time::Duration) -> Result<(), wgpu::SurfaceError> {
569        // Get the next frame
570        let output = self.surface.get_current_texture()?;
571        let view = output
572            .texture
573            .create_view(&wgpu::TextureViewDescriptor::default());
574
575        // Camera updates will be handled by simple interaction code
576
577        // Create command encoder
578        let mut encoder = self
579            .plot_renderer
580            .wgpu_renderer
581            .device
582            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
583                label: Some("Render Encoder"),
584            });
585
586        // Render egui
587        let raw_input = self.egui_state.take_egui_input(&self.window);
588
589        // Get UI data before borrowing
590        let scene_stats = self.plot_renderer.scene.statistics();
591        let _camera_pos = self
592            .plot_renderer
593            .axes_camera(0)
594            .unwrap_or_else(|| self.plot_renderer.camera())
595            .position;
596
597        // Track the plot area for WGPU rendering
598        let mut plot_area: Option<egui::Rect> = None;
599
600        // Ensure data bounds are current before drawing overlay (keeps axes in sync with render)
601        let _ = self.plot_renderer.calculate_data_bounds();
602
603        let full_output = self.egui_ctx.run(raw_input, |ctx| {
604            // Use PlotOverlay for unified UI rendering - no more duplicate sidebar code!
605            let overlay_config = OverlayConfig {
606                // Grid drawn under data in WGPU; overlay handles axes/labels/titles only
607                show_grid: self.plot_renderer.overlay_show_grid(),
608                show_axes: true,
609                show_title: true,
610                title: self
611                    .plot_renderer
612                    .overlay_title()
613                    .cloned()
614                    .or(Some("Plot".to_string())),
615                x_label: self
616                    .plot_renderer
617                    .overlay_x_label()
618                    .cloned()
619                    .or(Some("X".to_string())),
620                y_label: self
621                    .plot_renderer
622                    .overlay_y_label()
623                    .cloned()
624                    .or(Some("Y".to_string())),
625                ..Default::default()
626            };
627            let overlay_metrics = OverlayMetrics {
628                vertex_count: scene_stats.total_vertices,
629                triangle_count: scene_stats.total_triangles,
630                render_time_ms: 0.0, // TODO: Add timing
631                fps: 60.0,           // TODO: Calculate actual FPS
632            };
633
634            let frame_info = self.plot_overlay.render(
635                ctx,
636                &self.plot_renderer,
637                &overlay_config,
638                overlay_metrics,
639            );
640            plot_area = frame_info.plot_area;
641        });
642
643        // Update pixels-per-point for input mapping and calculate data bounds
644        let ppp_now = full_output.pixels_per_point;
645        if ppp_now > 0.0 {
646            // store for later mapping
647            // SAFETY: field exists in window struct
648            self.pixels_per_point = ppp_now;
649        }
650        // Calculate data bounds (kept for potential overlay/tick use)
651        let _data_bounds = self.plot_renderer.data_bounds();
652
653        // Handle toolbar actions requested by overlay
654        let (save_png, save_svg, reset_view, toggle_grid_opt, toggle_legend_opt) =
655            self.plot_overlay.take_toolbar_actions();
656        if let Some(show) = toggle_grid_opt {
657            // mutate last_figure and overlay flag
658            if let Some(mut fig) = self.plot_renderer.last_figure.clone() {
659                let (rows, cols) = fig.axes_grid();
660                let axes_count = (rows * cols).max(1);
661                for idx in 0..axes_count {
662                    fig.set_axes_grid_enabled(idx, show);
663                }
664                self.plot_renderer.set_figure(fig);
665            }
666        }
667        if let Some(show) = toggle_legend_opt {
668            if let Some(mut fig) = self.plot_renderer.last_figure.clone() {
669                fig.legend_enabled = show;
670                self.plot_renderer.set_figure(fig);
671            }
672        }
673        if reset_view {
674            // Refit camera to data (explicit Fit Extents)
675            self.plot_renderer.fit_extents();
676        }
677        if save_png || save_svg {
678            // OS Save Dialog to select path
679            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
680            {
681                if save_png {
682                    if let Some(path) = rfd::FileDialog::new()
683                        .add_filter("PNG Image", &["png"])
684                        .set_file_name("plot.png")
685                        .save_file()
686                    {
687                        let mut fig_for_save = self.plot_renderer.export_figure_clone();
688                        let _ = std::thread::spawn(move || {
689                            let rt = tokio::runtime::Builder::new_current_thread()
690                                .enable_all()
691                                .build();
692                            if let Ok(rt) = rt {
693                                rt.block_on(async move {
694                                    if let Ok(exporter) =
695                                        crate::export::image::ImageExporter::new().await
696                                    {
697                                        let _ = exporter.export_png(&mut fig_for_save, &path).await;
698                                    }
699                                });
700                            }
701                        });
702                    }
703                }
704                if save_svg {
705                    if let Some(path) = rfd::FileDialog::new()
706                        .add_filter("SVG", &["svg"])
707                        .set_file_name("plot.svg")
708                        .save_file()
709                    {
710                        let mut fig_for_save = self.plot_renderer.export_figure_clone();
711                        let exporter = crate::export::vector::VectorExporter::new();
712                        let _ = exporter.export_svg(&mut fig_for_save, &path);
713                    }
714                }
715            }
716            #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
717            {
718                // Fallback to temp directory
719                if save_png {
720                    let mut fig = self.plot_renderer.export_figure_clone();
721                    let tmp = std::env::temp_dir().join("runmat_export.png");
722                    let _ = std::thread::spawn(move || {
723                        let rt = tokio::runtime::Builder::new_current_thread()
724                            .enable_all()
725                            .build();
726                        if let Ok(rt) = rt {
727                            rt.block_on(async move {
728                                if let Ok(exporter) =
729                                    crate::export::image::ImageExporter::new().await
730                                {
731                                    let _ = exporter.export_png(&mut fig, &tmp).await;
732                                }
733                            });
734                        }
735                    });
736                }
737                if save_svg {
738                    let mut fig = self.plot_renderer.export_figure_clone();
739                    let tmp = std::env::temp_dir().join("runmat_export.svg");
740                    let exporter = crate::export::vector::VectorExporter::new();
741                    let _ = exporter.export_svg(&mut fig, &tmp);
742                }
743            }
744        }
745
746        // Now we have the plot area, update camera and WGPU rendering accordingly
747        if let Some(plot_rect) = plot_area {
748            // Update subplot-aware camera aspect ratios to match the visible plot rectangles.
749            self.update_subplot_camera_aspects_for_rect(plot_rect);
750        }
751
752        self.egui_state
753            .handle_platform_output(&self.window, full_output.platform_output);
754
755        let tris = self
756            .egui_ctx
757            .tessellate(full_output.shapes, full_output.pixels_per_point);
758        for (id, image_delta) in &full_output.textures_delta.set {
759            self.egui_renderer.update_texture(
760                &self.plot_renderer.wgpu_renderer.device,
761                &self.plot_renderer.wgpu_renderer.queue,
762                *id,
763                image_delta,
764            );
765        }
766
767        self.egui_renderer.update_buffers(
768            &self.plot_renderer.wgpu_renderer.device,
769            &self.plot_renderer.wgpu_renderer.queue,
770            &mut encoder,
771            &tris,
772            &egui_wgpu::ScreenDescriptor {
773                size_in_pixels: [self.config.width, self.config.height],
774                pixels_per_point: full_output.pixels_per_point,
775            },
776        );
777
778        // First render the plot data into the scissored viewport (MSAA-friendly)
779        if let Some(plot_rect) = plot_area {
780            // Use egui's pixels-per-point for exact device pixel mapping
781            let ppp = self.pixels_per_point.max(0.5);
782            let (rows, cols) = self.plot_renderer.figure_axes_grid();
783            let axes_plot_rects = if rows * cols > 1 {
784                if self.plot_overlay.axes_plot_rects().len() == rows * cols {
785                    self.plot_overlay.axes_plot_rects().to_vec()
786                } else {
787                    self.plot_overlay.compute_subplot_plot_rects_snapped(
788                        plot_rect,
789                        &self.plot_renderer,
790                        1.0,
791                        ppp,
792                    )
793                }
794            } else {
795                vec![PlotOverlay::snap_rect_to_pixels(plot_rect, ppp)]
796            };
797            let axes_plot_sizes_px: Vec<(u32, u32)> = axes_plot_rects
798                .iter()
799                .map(|r| {
800                    (
801                        (r.width() * ppp).round().max(1.0) as u32,
802                        (r.height() * ppp).round().max(1.0) as u32,
803                    )
804                })
805                .collect();
806            self.plot_renderer
807                .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
808            let primary_rect = axes_plot_rects.first().copied().unwrap_or(plot_rect);
809            let vx = (primary_rect.min.x * ppp).round();
810            let vy = (primary_rect.min.y * ppp).round();
811            let vw = (primary_rect.width() * ppp).round().max(1.0);
812            let vh = (primary_rect.height() * ppp).round().max(1.0);
813
814            // Clamp to surface dimensions
815            let sw = self.config.width as f32;
816            let sh = self.config.height as f32;
817            let cvx = vx.max(0.0);
818            let cvy = vy.max(0.0);
819            let mut cvw = vw;
820            let mut cvh = vh;
821            if cvx + cvw > sw {
822                cvw = (sw - cvx).max(1.0);
823            }
824            if cvy + cvh > sh {
825                cvh = (sh - cvy).max(1.0);
826            }
827
828            // Scissor rectangle is specified in physical pixels as u32
829            let scissor = (cvx as u32, cvy as u32, cvw as u32, cvh as u32);
830
831            {
832                let bg = self
833                    .plot_renderer
834                    .theme
835                    .build_theme()
836                    .get_background_color();
837                let clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
838                    label: Some("runmat-window-overlay-clear"),
839                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
840                        view: &view,
841                        resolve_target: None,
842                        ops: wgpu::Operations {
843                            load: wgpu::LoadOp::Clear(wgpu::Color {
844                                r: bg.x as f64,
845                                g: bg.y as f64,
846                                b: bg.z as f64,
847                                a: bg.w as f64,
848                            }),
849                            store: wgpu::StoreOp::Store,
850                        },
851                    })],
852                    depth_stencil_attachment: None,
853                    occlusion_query_set: None,
854                    timestamp_writes: None,
855                });
856                drop(clear_pass);
857            }
858
859            // If this figure has a subplot grid > 1, split into axes rectangles and render each
860            if rows * cols > 1 {
861                let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::new();
862                let mut hovered_axes: Option<usize> = None;
863                // Detect hovered subplot for camera interaction
864                let mouse_pos = self.mouse_position;
865                for (i, r) in axes_plot_rects.iter().enumerate() {
866                    let rx = (r.min.x * ppp).round();
867                    let ry = (r.min.y * ppp).round();
868                    let rw = (r.width() * ppp).round().max(1.0);
869                    let rh = (r.height() * ppp).round().max(1.0);
870                    // clamp each to surface
871                    let svx = rx.max(0.0);
872                    let svy = ry.max(0.0);
873                    let mut svw = rw;
874                    let mut svh = rh;
875                    if svx + svw > sw {
876                        svw = (sw - svx).max(1.0);
877                    }
878                    if svy + svh > sh {
879                        svh = (sh - svy).max(1.0);
880                    }
881                    debug!(
882                        target: "runmat_plot.axes_viewport_native",
883                        axes_index = i,
884                        viewport_x = svx as u32,
885                        viewport_y = svy as u32,
886                        viewport_w = svw as u32,
887                        viewport_h = svh as u32,
888                        content_min_x = r.min.x,
889                        content_min_y = r.min.y,
890                        content_max_x = r.max.x,
891                        content_max_y = r.max.y,
892                        pixels_per_point = ppp,
893                        "prepared native subplot viewport"
894                    );
895                    viewports.push((svx as u32, svy as u32, svw as u32, svh as u32));
896
897                    if hovered_axes.is_none() {
898                        let px_min_x = rx;
899                        let px_min_y = ry;
900                        if mouse_pos.x >= px_min_x
901                            && mouse_pos.x <= px_min_x + rw
902                            && mouse_pos.y >= px_min_y
903                            && mouse_pos.y <= px_min_y + rh
904                        {
905                            hovered_axes = Some(i);
906                        }
907                    }
908                }
909                // Do not overwrite per-axes cameras; keep their independent state for interaction
910                let subplot_cfg = crate::core::plot_renderer::PlotRenderConfig {
911                    width: self.plot_renderer.wgpu_renderer.surface_config.width.max(1),
912                    height: self
913                        .plot_renderer
914                        .wgpu_renderer
915                        .surface_config
916                        .height
917                        .max(1),
918                    msaa_samples: 4,
919                    theme: self.plot_renderer.theme.clone(),
920                    background_color: self
921                        .plot_renderer
922                        .theme
923                        .build_theme()
924                        .get_background_color(),
925                    ..Default::default()
926                };
927                let _ = self.plot_renderer.render_axes_to_viewports(
928                    &mut encoder,
929                    &view,
930                    &viewports,
931                    4,
932                    &subplot_cfg,
933                );
934            } else {
935                debug!(
936                    target: "runmat_plot.axes_viewport_native",
937                    axes_index = 0,
938                    viewport_x = scissor.0,
939                    viewport_y = scissor.1,
940                    viewport_w = scissor.2,
941                    viewport_h = scissor.3,
942                    content_min_x = primary_rect.min.x,
943                    content_min_y = primary_rect.min.y,
944                    content_max_x = primary_rect.max.x,
945                    content_max_y = primary_rect.max.y,
946                    pixels_per_point = ppp,
947                    "prepared native single-axes viewport"
948                );
949                // Single axes fallback: Render into the scissored viewport using camera path
950                let cfg = crate::core::plot_renderer::PlotRenderConfig {
951                    width: scissor.2,
952                    height: scissor.3,
953                    msaa_samples: 4,
954                    theme: self.plot_renderer.theme.clone(),
955                    background_color: self
956                        .plot_renderer
957                        .theme
958                        .build_theme()
959                        .get_background_color(),
960                    ..Default::default()
961                };
962                let cam = self.plot_renderer.camera().clone();
963                let _ = self.plot_renderer.render_camera_to_viewport(
964                    &mut encoder,
965                    &view,
966                    scissor,
967                    &cfg,
968                    &cam,
969                    0,
970                    true,
971                );
972            }
973        }
974
975        // Then render the UI overlay on top (legend, labels, etc.)
976        {
977            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
978                label: Some("Egui Render Pass"),
979                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
980                    view: &view,
981                    resolve_target: None,
982                    ops: wgpu::Operations {
983                        load: wgpu::LoadOp::Load,
984                        store: wgpu::StoreOp::Store,
985                    },
986                })],
987                depth_stencil_attachment: None,
988                occlusion_query_set: None,
989                timestamp_writes: None,
990            });
991            self.egui_renderer.render(
992                &mut render_pass,
993                &tris,
994                &egui_wgpu::ScreenDescriptor {
995                    size_in_pixels: [self.config.width, self.config.height],
996                    pixels_per_point: full_output.pixels_per_point,
997                },
998            );
999        }
1000
1001        for id in &full_output.textures_delta.free {
1002            self.egui_renderer.free_texture(id);
1003        }
1004
1005        // Submit commands
1006        self.plot_renderer
1007            .wgpu_renderer
1008            .queue
1009            .submit(std::iter::once(encoder.finish()));
1010        output.present();
1011
1012        Ok(())
1013    }
1014
1015    /// Handle mouse input
1016    fn handle_mouse_input(
1017        &mut self,
1018        button: winit::event::MouseButton,
1019        state: winit::event::ElementState,
1020    ) {
1021        use winit::event::{ElementState, MouseButton};
1022
1023        match (button, state) {
1024            (MouseButton::Left, ElementState::Pressed) => {
1025                // Only start panning if press occurs inside the plot area (or a subplot rect)
1026                self.is_mouse_over_plot = false;
1027                self.active_drag_axes = None;
1028                if let Some(plot_rect) = self.plot_overlay.plot_area() {
1029                    let mx = self.mouse_position.x;
1030                    let my = self.mouse_position.y;
1031                    let (rows, cols) = self.plot_renderer.figure_axes_grid();
1032                    if rows * cols > 1 {
1033                        let rects = self.plot_overlay.compute_subplot_plot_rects(
1034                            plot_rect,
1035                            &self.plot_renderer,
1036                            1.0,
1037                        );
1038                        for (i, r) in rects.into_iter().enumerate() {
1039                            let rx = r.min.x * self.pixels_per_point;
1040                            let ry = r.min.y * self.pixels_per_point;
1041                            let rw = r.width() * self.pixels_per_point;
1042                            let rh = r.height() * self.pixels_per_point;
1043                            if mx >= rx && mx <= rx + rw && my >= ry && my <= ry + rh {
1044                                self.is_mouse_over_plot = true;
1045                                self.active_drag_axes = Some(i);
1046                                break;
1047                            }
1048                        }
1049                    } else {
1050                        let ppp = self.pixels_per_point.max(0.5);
1051                        let px_min_x = plot_rect.min.x * ppp;
1052                        let px_min_y = plot_rect.min.y * ppp;
1053                        let px_w = plot_rect.width() * ppp;
1054                        let px_h = plot_rect.height() * ppp;
1055                        self.is_mouse_over_plot = mx >= px_min_x
1056                            && mx <= px_min_x + px_w
1057                            && my >= px_min_y
1058                            && my <= px_min_y + px_h;
1059                        if self.is_mouse_over_plot {
1060                            self.active_drag_axes = Some(0);
1061                        }
1062                    }
1063                }
1064            }
1065            (MouseButton::Left, ElementState::Released) => {
1066                self.is_mouse_over_plot = false;
1067                self.active_drag_axes = None;
1068            }
1069            _ => {}
1070        }
1071    }
1072
1073    /// Handle mouse movement
1074    fn handle_mouse_move(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
1075        let new_position = glam::Vec2::new(position.x as f32, position.y as f32);
1076        let delta = if self.mouse_left_down {
1077            new_position - self.mouse_position
1078        } else {
1079            glam::Vec2::ZERO
1080        };
1081        self.mouse_position = new_position;
1082
1083        // Pan when left mouse button is held down: shift orthographic bounds in world units
1084        if self.is_mouse_over_plot && delta.length() > 0.0 {
1085            if let Some(plot_rect) = self.plot_overlay.plot_area() {
1086                let (rows, cols) = self.plot_renderer.figure_axes_grid();
1087                if rows * cols > 1 {
1088                    // Pan only the subplot captured on mouse-down.
1089                    if let Some(i) = self.active_drag_axes {
1090                        let rects = self.plot_overlay.compute_subplot_plot_rects(
1091                            plot_rect,
1092                            &self.plot_renderer,
1093                            1.0,
1094                        );
1095                        if let Some(r) = rects.get(i) {
1096                            let rw = r.width() * self.pixels_per_point;
1097                            let rh = r.height() * self.pixels_per_point;
1098                            if let Some(cam) = self.plot_renderer.axes_camera_mut(i) {
1099                                if let crate::core::camera::ProjectionType::Orthographic {
1100                                    left,
1101                                    right,
1102                                    bottom,
1103                                    top,
1104                                    ..
1105                                } = cam.projection
1106                                {
1107                                    let pw = rw.max(1.0);
1108                                    let ph = rh.max(1.0);
1109                                    let world_w = right - left;
1110                                    let world_h = top - bottom;
1111                                    let dx_world = (delta.x / pw) * world_w;
1112                                    let dy_world = (delta.y / ph) * world_h;
1113                                    cam.projection =
1114                                        crate::core::camera::ProjectionType::Orthographic {
1115                                            left: left - dx_world,
1116                                            right: right - dx_world,
1117                                            bottom: bottom + dy_world,
1118                                            top: top + dy_world,
1119                                            near: -1.0,
1120                                            far: 1.0,
1121                                        };
1122                                    cam.mark_dirty();
1123                                    self.plot_renderer.note_axes_camera_interaction(i);
1124                                }
1125                            }
1126                        }
1127                    }
1128                } else {
1129                    let cam = self.plot_renderer.camera_mut();
1130                    if let crate::core::camera::ProjectionType::Orthographic {
1131                        left,
1132                        right,
1133                        bottom,
1134                        top,
1135                        ..
1136                    } = &mut cam.projection
1137                    {
1138                        let pw = (plot_rect.width() * self.pixels_per_point).max(1.0);
1139                        let ph = (plot_rect.height() * self.pixels_per_point).max(1.0);
1140                        let world_w = *right - *left;
1141                        let world_h = *top - *bottom;
1142                        let dx_world = (delta.x / pw) * world_w;
1143                        let dy_world = (delta.y / ph) * world_h;
1144                        *left -= dx_world;
1145                        *right -= dx_world;
1146                        *bottom += dy_world;
1147                        *top += dy_world;
1148                        cam.mark_dirty();
1149                        self.plot_renderer.note_axes_camera_interaction(0);
1150                    }
1151                }
1152            }
1153        }
1154    }
1155
1156    /// Handle mouse scroll
1157    fn handle_mouse_scroll(&mut self, delta: winit::event::MouseScrollDelta) {
1158        let scroll_delta = match delta {
1159            winit::event::MouseScrollDelta::LineDelta(_, y) => y,
1160            winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 100.0,
1161        };
1162
1163        // Zoom in/out by scaling the orthographic projection. Anchor zoom at cursor when inside plot area.
1164        if let Some(plot_rect) = self.plot_overlay.plot_area() {
1165            let (rows, cols) = self.plot_renderer.figure_axes_grid();
1166            if rows * cols > 1 {
1167                let rects = self.plot_overlay.compute_subplot_plot_rects(
1168                    plot_rect,
1169                    &self.plot_renderer,
1170                    1.0,
1171                );
1172                for (i, r) in rects.iter().enumerate() {
1173                    let rx = r.min.x * self.pixels_per_point;
1174                    let ry = r.min.y * self.pixels_per_point;
1175                    let rw = r.width() * self.pixels_per_point;
1176                    let rh = r.height() * self.pixels_per_point;
1177                    let mx = self.mouse_position.x;
1178                    let my = self.mouse_position.y;
1179                    if mx >= rx && mx <= rx + rw && my >= ry && my <= ry + rh {
1180                        if let Some(cam) = self.plot_renderer.axes_camera_mut(i) {
1181                            if let crate::core::camera::ProjectionType::Orthographic {
1182                                left,
1183                                right,
1184                                bottom,
1185                                top,
1186                                ..
1187                            } = cam.projection
1188                            {
1189                                let factor = (1.0 - scroll_delta * 0.1).clamp(0.2, 5.0);
1190                                let tx = (mx - rx) / rw;
1191                                let ty = (my - ry) / rh;
1192                                let w = right - left;
1193                                let h = top - bottom;
1194                                let pivot_x = left + tx * w;
1195                                let pivot_y = top - ty * h;
1196                                let new_left = pivot_x - (pivot_x - left) * factor;
1197                                let new_right = pivot_x + (right - pivot_x) * factor;
1198                                let new_bottom = pivot_y - (pivot_y - bottom) * factor;
1199                                let new_top = pivot_y + (top - pivot_y) * factor;
1200                                cam.projection =
1201                                    crate::core::camera::ProjectionType::Orthographic {
1202                                        left: new_left,
1203                                        right: new_right,
1204                                        bottom: new_bottom,
1205                                        top: new_top,
1206                                        near: -1.0,
1207                                        far: 1.0,
1208                                    };
1209                                cam.mark_dirty();
1210                                self.plot_renderer.note_axes_camera_interaction(i);
1211                            }
1212                        }
1213                        break;
1214                    }
1215                }
1216            } else {
1217                let cam = self.plot_renderer.camera_mut();
1218                if let crate::core::camera::ProjectionType::Orthographic {
1219                    left,
1220                    right,
1221                    bottom,
1222                    top,
1223                    ..
1224                } = &mut cam.projection
1225                {
1226                    let factor = (1.0 - scroll_delta * 0.1).clamp(0.2, 5.0);
1227                    let px_min_x = plot_rect.min.x * self.pixels_per_point;
1228                    let px_min_y = plot_rect.min.y * self.pixels_per_point;
1229                    let px_w = plot_rect.width() * self.pixels_per_point;
1230                    let px_h = plot_rect.height() * self.pixels_per_point;
1231                    let mx = self.mouse_position.x;
1232                    let my = self.mouse_position.y;
1233                    let mut pivot_x = (*left + *right) * 0.5;
1234                    let mut pivot_y = (*bottom + *top) * 0.5;
1235                    if mx >= px_min_x
1236                        && mx <= px_min_x + px_w
1237                        && my >= px_min_y
1238                        && my <= px_min_y + px_h
1239                    {
1240                        let tx = (mx - px_min_x) / px_w;
1241                        let ty = (my - px_min_y) / px_h;
1242                        let w = *right - *left;
1243                        let h = *top - *bottom;
1244                        pivot_x = *left + tx * w;
1245                        pivot_y = *top - ty * h;
1246                    }
1247                    let new_left = pivot_x - (pivot_x - *left) * factor;
1248                    let new_right = pivot_x + (*right - pivot_x) * factor;
1249                    let new_bottom = pivot_y - (pivot_y - *bottom) * factor;
1250                    let new_top = pivot_y + (*top - pivot_y) * factor;
1251                    *left = new_left;
1252                    *right = new_right;
1253                    *bottom = new_bottom;
1254                    *top = new_top;
1255                    cam.mark_dirty();
1256                    self.plot_renderer.note_axes_camera_interaction(0);
1257                }
1258            }
1259        }
1260    }
1261}