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