1#[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 pub async fn new(config: WindowConfig) -> Result<Self, Box<dyn std::error::Error>> {
57 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 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 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 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 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 let egui_ctx = egui::Context::default();
158
159 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, 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 pub fn add_test_plot(&mut self) {
202 use crate::core::vertex_utils;
203
204 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 let vertices =
210 vertex_utils::create_line_plot(&x_data, &y_data, Vec4::new(0.0, 0.5, 1.0, 1.0));
211
212 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, index_offset: None,
224 index_count: None,
225 instance_count: 1,
226 }],
227 image: None,
228 };
229
230 render_data.material.albedo = Vec4::new(0.0, 0.5, 1.0, 1.0);
232
233 let node = crate::core::SceneNode {
234 id: 0, 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 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 pub fn set_figure(&mut self, figure: crate::plots::Figure) {
267 self.plot_renderer.set_figure(figure);
269 }
270
271 pub fn install_close_signal(&mut self, signal: Arc<AtomicBool>) {
273 self.close_signal = Some(signal);
274 }
275
276 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 static mut MODIFIERS: Option<winit::keyboard::ModifiersState> = None;
297
298 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 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 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 if let Some(text) = key_event.text {
369 if text == "\u{11}" { }
370 }
371 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 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 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 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 fn resize(&mut self, width: u32, height: u32) {
503 if width == 0 || height == 0 {
504 return; }
506
507 self.config.width = width;
508 self.config.height = height;
509
510 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 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 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 fn render(&mut self, _dt: std::time::Duration) -> Result<(), wgpu::SurfaceError> {
570 let output = self.surface.get_current_texture()?;
572 let view = output
573 .texture
574 .create_view(&wgpu::TextureViewDescriptor::default());
575
576 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 let raw_input = self.egui_state.take_egui_input(&self.window);
589
590 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 let mut plot_area: Option<egui::Rect> = None;
600
601 let _ = self.plot_renderer.calculate_data_bounds();
603
604 let full_output = self.egui_ctx.run(raw_input, |ctx| {
605 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 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, fps: 60.0, };
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 let ppp_now = full_output.pixels_per_point;
656 if ppp_now > 0.0 {
657 self.pixels_per_point = ppp_now;
660 }
661 let _data_bounds = self.plot_renderer.data_bounds();
663
664 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 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 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 #[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 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 if let Some(plot_rect) = plot_area {
745 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 if let Some(plot_rect) = plot_area {
777 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 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 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 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 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 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 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 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 {
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 self.plot_renderer
1005 .wgpu_renderer
1006 .queue
1007 .submit(std::iter::once(encoder.finish()));
1008 output.present();
1009
1010 Ok(())
1011 }
1012
1013 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 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 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 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 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 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 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}