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