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, warn};
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            if save_svg {
679                warn!("SVG export is no longer supported");
680            }
681            // OS Save Dialog to select path
682            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
683            {
684                if save_png {
685                    if let Some(path) = rfd::FileDialog::new()
686                        .add_filter("PNG Image", &["png"])
687                        .set_file_name("plot.png")
688                        .save_file()
689                    {
690                        let mut fig_for_save = self.plot_renderer.export_figure_clone();
691                        let _ = std::thread::spawn(move || {
692                            let rt = tokio::runtime::Builder::new_current_thread()
693                                .enable_all()
694                                .build();
695                            if let Ok(rt) = rt {
696                                rt.block_on(async move {
697                                    if let Ok(exporter) =
698                                        crate::export::image::ImageExporter::new().await
699                                    {
700                                        let _ = exporter.export_png(&mut fig_for_save, &path).await;
701                                    }
702                                });
703                            }
704                        });
705                    }
706                }
707            }
708            #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
709            {
710                // Fallback to temp directory
711                if save_png {
712                    let mut fig = self.plot_renderer.export_figure_clone();
713                    let tmp = std::env::temp_dir().join("runmat_export.png");
714                    let _ = std::thread::spawn(move || {
715                        let rt = tokio::runtime::Builder::new_current_thread()
716                            .enable_all()
717                            .build();
718                        if let Ok(rt) = rt {
719                            rt.block_on(async move {
720                                if let Ok(exporter) =
721                                    crate::export::image::ImageExporter::new().await
722                                {
723                                    let _ = exporter.export_png(&mut fig, &tmp).await;
724                                }
725                            });
726                        }
727                    });
728                }
729            }
730        }
731
732        // Now we have the plot area, update camera and WGPU rendering accordingly
733        if let Some(plot_rect) = plot_area {
734            // Update subplot-aware camera aspect ratios to match the visible plot rectangles.
735            self.update_subplot_camera_aspects_for_rect(plot_rect);
736        }
737
738        self.egui_state
739            .handle_platform_output(&self.window, full_output.platform_output);
740
741        let tris = self
742            .egui_ctx
743            .tessellate(full_output.shapes, full_output.pixels_per_point);
744        for (id, image_delta) in &full_output.textures_delta.set {
745            self.egui_renderer.update_texture(
746                &self.plot_renderer.wgpu_renderer.device,
747                &self.plot_renderer.wgpu_renderer.queue,
748                *id,
749                image_delta,
750            );
751        }
752
753        self.egui_renderer.update_buffers(
754            &self.plot_renderer.wgpu_renderer.device,
755            &self.plot_renderer.wgpu_renderer.queue,
756            &mut encoder,
757            &tris,
758            &egui_wgpu::ScreenDescriptor {
759                size_in_pixels: [self.config.width, self.config.height],
760                pixels_per_point: full_output.pixels_per_point,
761            },
762        );
763
764        // First render the plot data into the scissored viewport (MSAA-friendly)
765        if let Some(plot_rect) = plot_area {
766            // Use egui's pixels-per-point for exact device pixel mapping
767            let ppp = self.pixels_per_point.max(0.5);
768            let (rows, cols) = self.plot_renderer.figure_axes_grid();
769            let axes_plot_rects = if rows * cols > 1 {
770                if self.plot_overlay.axes_plot_rects().len() == rows * cols {
771                    self.plot_overlay.axes_plot_rects().to_vec()
772                } else {
773                    self.plot_overlay.compute_subplot_plot_rects_snapped(
774                        plot_rect,
775                        &self.plot_renderer,
776                        1.0,
777                        ppp,
778                    )
779                }
780            } else {
781                vec![PlotOverlay::snap_rect_to_pixels(plot_rect, ppp)]
782            };
783            let axes_plot_sizes_px: Vec<(u32, u32)> = axes_plot_rects
784                .iter()
785                .map(|r| {
786                    (
787                        (r.width() * ppp).round().max(1.0) as u32,
788                        (r.height() * ppp).round().max(1.0) as u32,
789                    )
790                })
791                .collect();
792            self.plot_renderer
793                .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
794            let primary_rect = axes_plot_rects.first().copied().unwrap_or(plot_rect);
795            let vx = (primary_rect.min.x * ppp).round();
796            let vy = (primary_rect.min.y * ppp).round();
797            let vw = (primary_rect.width() * ppp).round().max(1.0);
798            let vh = (primary_rect.height() * ppp).round().max(1.0);
799
800            // Clamp to surface dimensions
801            let sw = self.config.width as f32;
802            let sh = self.config.height as f32;
803            let cvx = vx.max(0.0);
804            let cvy = vy.max(0.0);
805            let mut cvw = vw;
806            let mut cvh = vh;
807            if cvx + cvw > sw {
808                cvw = (sw - cvx).max(1.0);
809            }
810            if cvy + cvh > sh {
811                cvh = (sh - cvy).max(1.0);
812            }
813
814            // Scissor rectangle is specified in physical pixels as u32
815            let scissor = (cvx as u32, cvy as u32, cvw as u32, cvh as u32);
816
817            {
818                let bg = self
819                    .plot_renderer
820                    .theme
821                    .build_theme()
822                    .get_background_color();
823                let clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
824                    label: Some("runmat-window-overlay-clear"),
825                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
826                        view: &view,
827                        resolve_target: None,
828                        ops: wgpu::Operations {
829                            load: wgpu::LoadOp::Clear(wgpu::Color {
830                                r: bg.x as f64,
831                                g: bg.y as f64,
832                                b: bg.z as f64,
833                                a: bg.w as f64,
834                            }),
835                            store: wgpu::StoreOp::Store,
836                        },
837                    })],
838                    depth_stencil_attachment: None,
839                    occlusion_query_set: None,
840                    timestamp_writes: None,
841                });
842                drop(clear_pass);
843            }
844
845            // If this figure has a subplot grid > 1, split into axes rectangles and render each
846            if rows * cols > 1 {
847                let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::new();
848                let mut hovered_axes: Option<usize> = None;
849                // Detect hovered subplot for camera interaction
850                let mouse_pos = self.mouse_position;
851                for (i, r) in axes_plot_rects.iter().enumerate() {
852                    let rx = (r.min.x * ppp).round();
853                    let ry = (r.min.y * ppp).round();
854                    let rw = (r.width() * ppp).round().max(1.0);
855                    let rh = (r.height() * ppp).round().max(1.0);
856                    // clamp each to surface
857                    let svx = rx.max(0.0);
858                    let svy = ry.max(0.0);
859                    let mut svw = rw;
860                    let mut svh = rh;
861                    if svx + svw > sw {
862                        svw = (sw - svx).max(1.0);
863                    }
864                    if svy + svh > sh {
865                        svh = (sh - svy).max(1.0);
866                    }
867                    debug!(
868                        target: "runmat_plot.axes_viewport_native",
869                        axes_index = i,
870                        viewport_x = svx as u32,
871                        viewport_y = svy as u32,
872                        viewport_w = svw as u32,
873                        viewport_h = svh as u32,
874                        content_min_x = r.min.x,
875                        content_min_y = r.min.y,
876                        content_max_x = r.max.x,
877                        content_max_y = r.max.y,
878                        pixels_per_point = ppp,
879                        "prepared native subplot viewport"
880                    );
881                    viewports.push((svx as u32, svy as u32, svw as u32, svh as u32));
882
883                    if hovered_axes.is_none() {
884                        let px_min_x = rx;
885                        let px_min_y = ry;
886                        if mouse_pos.x >= px_min_x
887                            && mouse_pos.x <= px_min_x + rw
888                            && mouse_pos.y >= px_min_y
889                            && mouse_pos.y <= px_min_y + rh
890                        {
891                            hovered_axes = Some(i);
892                        }
893                    }
894                }
895                // Do not overwrite per-axes cameras; keep their independent state for interaction
896                let subplot_cfg = crate::core::plot_renderer::PlotRenderConfig {
897                    width: self.plot_renderer.wgpu_renderer.surface_config.width.max(1),
898                    height: self
899                        .plot_renderer
900                        .wgpu_renderer
901                        .surface_config
902                        .height
903                        .max(1),
904                    msaa_samples: 4,
905                    theme: self.plot_renderer.theme.clone(),
906                    background_color: self
907                        .plot_renderer
908                        .theme
909                        .build_theme()
910                        .get_background_color(),
911                    ..Default::default()
912                };
913                let _ = self.plot_renderer.render_axes_to_viewports(
914                    &mut encoder,
915                    &view,
916                    &viewports,
917                    4,
918                    &subplot_cfg,
919                );
920            } else {
921                debug!(
922                    target: "runmat_plot.axes_viewport_native",
923                    axes_index = 0,
924                    viewport_x = scissor.0,
925                    viewport_y = scissor.1,
926                    viewport_w = scissor.2,
927                    viewport_h = scissor.3,
928                    content_min_x = primary_rect.min.x,
929                    content_min_y = primary_rect.min.y,
930                    content_max_x = primary_rect.max.x,
931                    content_max_y = primary_rect.max.y,
932                    pixels_per_point = ppp,
933                    "prepared native single-axes viewport"
934                );
935                // Single axes fallback: Render into the scissored viewport using camera path
936                let cfg = crate::core::plot_renderer::PlotRenderConfig {
937                    width: scissor.2,
938                    height: scissor.3,
939                    msaa_samples: 4,
940                    theme: self.plot_renderer.theme.clone(),
941                    background_color: self
942                        .plot_renderer
943                        .theme
944                        .build_theme()
945                        .get_background_color(),
946                    ..Default::default()
947                };
948                let cam = self.plot_renderer.camera().clone();
949                let _ = self.plot_renderer.render_camera_to_viewport(
950                    &mut encoder,
951                    &view,
952                    scissor,
953                    &cfg,
954                    &cam,
955                    0,
956                    true,
957                );
958            }
959        }
960
961        // Then render the UI overlay on top (legend, labels, etc.)
962        {
963            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
964                label: Some("Egui Render Pass"),
965                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
966                    view: &view,
967                    resolve_target: None,
968                    ops: wgpu::Operations {
969                        load: wgpu::LoadOp::Load,
970                        store: wgpu::StoreOp::Store,
971                    },
972                })],
973                depth_stencil_attachment: None,
974                occlusion_query_set: None,
975                timestamp_writes: None,
976            });
977            self.egui_renderer.render(
978                &mut render_pass,
979                &tris,
980                &egui_wgpu::ScreenDescriptor {
981                    size_in_pixels: [self.config.width, self.config.height],
982                    pixels_per_point: full_output.pixels_per_point,
983                },
984            );
985        }
986
987        for id in &full_output.textures_delta.free {
988            self.egui_renderer.free_texture(id);
989        }
990
991        // Submit commands
992        self.plot_renderer
993            .wgpu_renderer
994            .queue
995            .submit(std::iter::once(encoder.finish()));
996        output.present();
997
998        Ok(())
999    }
1000
1001    /// Handle mouse input
1002    fn handle_mouse_input(
1003        &mut self,
1004        button: winit::event::MouseButton,
1005        state: winit::event::ElementState,
1006    ) {
1007        use winit::event::{ElementState, MouseButton};
1008
1009        match (button, state) {
1010            (MouseButton::Left, ElementState::Pressed) => {
1011                // Only start panning if press occurs inside the plot area (or a subplot rect)
1012                self.is_mouse_over_plot = false;
1013                self.active_drag_axes = None;
1014                if let Some(plot_rect) = self.plot_overlay.plot_area() {
1015                    let mx = self.mouse_position.x;
1016                    let my = self.mouse_position.y;
1017                    let (rows, cols) = self.plot_renderer.figure_axes_grid();
1018                    if rows * cols > 1 {
1019                        let rects = self.plot_overlay.compute_subplot_plot_rects(
1020                            plot_rect,
1021                            &self.plot_renderer,
1022                            1.0,
1023                        );
1024                        for (i, r) in rects.into_iter().enumerate() {
1025                            let rx = r.min.x * self.pixels_per_point;
1026                            let ry = r.min.y * self.pixels_per_point;
1027                            let rw = r.width() * self.pixels_per_point;
1028                            let rh = r.height() * self.pixels_per_point;
1029                            if mx >= rx && mx <= rx + rw && my >= ry && my <= ry + rh {
1030                                self.is_mouse_over_plot = true;
1031                                self.active_drag_axes = Some(i);
1032                                break;
1033                            }
1034                        }
1035                    } else {
1036                        let ppp = self.pixels_per_point.max(0.5);
1037                        let px_min_x = plot_rect.min.x * ppp;
1038                        let px_min_y = plot_rect.min.y * ppp;
1039                        let px_w = plot_rect.width() * ppp;
1040                        let px_h = plot_rect.height() * ppp;
1041                        self.is_mouse_over_plot = mx >= px_min_x
1042                            && mx <= px_min_x + px_w
1043                            && my >= px_min_y
1044                            && my <= px_min_y + px_h;
1045                        if self.is_mouse_over_plot {
1046                            self.active_drag_axes = Some(0);
1047                        }
1048                    }
1049                }
1050            }
1051            (MouseButton::Left, ElementState::Released) => {
1052                self.is_mouse_over_plot = false;
1053                self.active_drag_axes = None;
1054            }
1055            _ => {}
1056        }
1057    }
1058
1059    /// Handle mouse movement
1060    fn handle_mouse_move(&mut self, position: winit::dpi::PhysicalPosition<f64>) {
1061        let new_position = glam::Vec2::new(position.x as f32, position.y as f32);
1062        let delta = if self.mouse_left_down {
1063            new_position - self.mouse_position
1064        } else {
1065            glam::Vec2::ZERO
1066        };
1067        self.mouse_position = new_position;
1068
1069        // Pan when left mouse button is held down: shift orthographic bounds in world units
1070        if self.is_mouse_over_plot && delta.length() > 0.0 {
1071            if let Some(plot_rect) = self.plot_overlay.plot_area() {
1072                let (rows, cols) = self.plot_renderer.figure_axes_grid();
1073                if rows * cols > 1 {
1074                    // Pan only the subplot captured on mouse-down.
1075                    if let Some(i) = self.active_drag_axes {
1076                        let rects = self.plot_overlay.compute_subplot_plot_rects(
1077                            plot_rect,
1078                            &self.plot_renderer,
1079                            1.0,
1080                        );
1081                        if let Some(r) = rects.get(i) {
1082                            let rw = r.width() * self.pixels_per_point;
1083                            let rh = r.height() * self.pixels_per_point;
1084                            if let Some(cam) = self.plot_renderer.axes_camera_mut(i) {
1085                                if let crate::core::camera::ProjectionType::Orthographic {
1086                                    left,
1087                                    right,
1088                                    bottom,
1089                                    top,
1090                                    ..
1091                                } = cam.projection
1092                                {
1093                                    let pw = rw.max(1.0);
1094                                    let ph = rh.max(1.0);
1095                                    let world_w = right - left;
1096                                    let world_h = top - bottom;
1097                                    let dx_world = (delta.x / pw) * world_w;
1098                                    let dy_world = (delta.y / ph) * world_h;
1099                                    cam.projection =
1100                                        crate::core::camera::ProjectionType::Orthographic {
1101                                            left: left - dx_world,
1102                                            right: right - dx_world,
1103                                            bottom: bottom + dy_world,
1104                                            top: top + dy_world,
1105                                            near: -1.0,
1106                                            far: 1.0,
1107                                        };
1108                                    cam.mark_dirty();
1109                                    self.plot_renderer.note_axes_camera_interaction(i);
1110                                }
1111                            }
1112                        }
1113                    }
1114                } else {
1115                    let cam = self.plot_renderer.camera_mut();
1116                    if let crate::core::camera::ProjectionType::Orthographic {
1117                        left,
1118                        right,
1119                        bottom,
1120                        top,
1121                        ..
1122                    } = &mut cam.projection
1123                    {
1124                        let pw = (plot_rect.width() * self.pixels_per_point).max(1.0);
1125                        let ph = (plot_rect.height() * self.pixels_per_point).max(1.0);
1126                        let world_w = *right - *left;
1127                        let world_h = *top - *bottom;
1128                        let dx_world = (delta.x / pw) * world_w;
1129                        let dy_world = (delta.y / ph) * world_h;
1130                        *left -= dx_world;
1131                        *right -= dx_world;
1132                        *bottom += dy_world;
1133                        *top += dy_world;
1134                        cam.mark_dirty();
1135                        self.plot_renderer.note_axes_camera_interaction(0);
1136                    }
1137                }
1138            }
1139        }
1140    }
1141
1142    /// Handle mouse scroll
1143    fn handle_mouse_scroll(&mut self, delta: winit::event::MouseScrollDelta) {
1144        let scroll_delta = match delta {
1145            winit::event::MouseScrollDelta::LineDelta(_, y) => y,
1146            winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 100.0,
1147        };
1148
1149        // Zoom in/out by scaling the orthographic projection. Anchor zoom at cursor when inside plot area.
1150        if let Some(plot_rect) = self.plot_overlay.plot_area() {
1151            let (rows, cols) = self.plot_renderer.figure_axes_grid();
1152            if rows * cols > 1 {
1153                let rects = self.plot_overlay.compute_subplot_plot_rects(
1154                    plot_rect,
1155                    &self.plot_renderer,
1156                    1.0,
1157                );
1158                for (i, r) in rects.iter().enumerate() {
1159                    let rx = r.min.x * self.pixels_per_point;
1160                    let ry = r.min.y * self.pixels_per_point;
1161                    let rw = r.width() * self.pixels_per_point;
1162                    let rh = r.height() * self.pixels_per_point;
1163                    let mx = self.mouse_position.x;
1164                    let my = self.mouse_position.y;
1165                    if mx >= rx && mx <= rx + rw && my >= ry && my <= ry + rh {
1166                        if let Some(cam) = self.plot_renderer.axes_camera_mut(i) {
1167                            if let crate::core::camera::ProjectionType::Orthographic {
1168                                left,
1169                                right,
1170                                bottom,
1171                                top,
1172                                ..
1173                            } = cam.projection
1174                            {
1175                                let factor = (1.0 - scroll_delta * 0.1).clamp(0.2, 5.0);
1176                                let tx = (mx - rx) / rw;
1177                                let ty = (my - ry) / rh;
1178                                let w = right - left;
1179                                let h = top - bottom;
1180                                let pivot_x = left + tx * w;
1181                                let pivot_y = top - ty * h;
1182                                let new_left = pivot_x - (pivot_x - left) * factor;
1183                                let new_right = pivot_x + (right - pivot_x) * factor;
1184                                let new_bottom = pivot_y - (pivot_y - bottom) * factor;
1185                                let new_top = pivot_y + (top - pivot_y) * factor;
1186                                cam.projection =
1187                                    crate::core::camera::ProjectionType::Orthographic {
1188                                        left: new_left,
1189                                        right: new_right,
1190                                        bottom: new_bottom,
1191                                        top: new_top,
1192                                        near: -1.0,
1193                                        far: 1.0,
1194                                    };
1195                                cam.mark_dirty();
1196                                self.plot_renderer.note_axes_camera_interaction(i);
1197                            }
1198                        }
1199                        break;
1200                    }
1201                }
1202            } else {
1203                let cam = self.plot_renderer.camera_mut();
1204                if let crate::core::camera::ProjectionType::Orthographic {
1205                    left,
1206                    right,
1207                    bottom,
1208                    top,
1209                    ..
1210                } = &mut cam.projection
1211                {
1212                    let factor = (1.0 - scroll_delta * 0.1).clamp(0.2, 5.0);
1213                    let px_min_x = plot_rect.min.x * self.pixels_per_point;
1214                    let px_min_y = plot_rect.min.y * self.pixels_per_point;
1215                    let px_w = plot_rect.width() * self.pixels_per_point;
1216                    let px_h = plot_rect.height() * self.pixels_per_point;
1217                    let mx = self.mouse_position.x;
1218                    let my = self.mouse_position.y;
1219                    let mut pivot_x = (*left + *right) * 0.5;
1220                    let mut pivot_y = (*bottom + *top) * 0.5;
1221                    if mx >= px_min_x
1222                        && mx <= px_min_x + px_w
1223                        && my >= px_min_y
1224                        && my <= px_min_y + px_h
1225                    {
1226                        let tx = (mx - px_min_x) / px_w;
1227                        let ty = (my - px_min_y) / px_h;
1228                        let w = *right - *left;
1229                        let h = *top - *bottom;
1230                        pivot_x = *left + tx * w;
1231                        pivot_y = *top - ty * h;
1232                    }
1233                    let new_left = pivot_x - (pivot_x - *left) * factor;
1234                    let new_right = pivot_x + (*right - pivot_x) * factor;
1235                    let new_bottom = pivot_y - (pivot_y - *bottom) * factor;
1236                    let new_top = pivot_y + (*top - pivot_y) * factor;
1237                    *left = new_left;
1238                    *right = new_right;
1239                    *bottom = new_bottom;
1240                    *top = new_top;
1241                    cam.mark_dirty();
1242                    self.plot_renderer.note_axes_camera_interaction(0);
1243                }
1244            }
1245        }
1246    }
1247}