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 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 pub async fn new(config: WindowConfig) -> Result<Self, Box<dyn std::error::Error>> {
56 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 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 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 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 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 let egui_ctx = egui::Context::default();
157
158 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, 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 pub fn add_test_plot(&mut self) {
201 use crate::core::vertex_utils;
202
203 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 let vertices =
209 vertex_utils::create_line_plot(&x_data, &y_data, Vec4::new(0.0, 0.5, 1.0, 1.0));
210
211 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, index_offset: None,
223 index_count: None,
224 instance_count: 1,
225 }],
226 image: None,
227 };
228
229 render_data.material.albedo = Vec4::new(0.0, 0.5, 1.0, 1.0);
231
232 let node = crate::core::SceneNode {
233 id: 0, 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 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 pub fn set_figure(&mut self, figure: crate::plots::Figure) {
266 self.plot_renderer.set_figure(figure);
268 }
269
270 pub fn install_close_signal(&mut self, signal: Arc<AtomicBool>) {
272 self.close_signal = Some(signal);
273 }
274
275 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 static mut MODIFIERS: Option<winit::keyboard::ModifiersState> = None;
296
297 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 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 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 if let Some(text) = key_event.text {
368 if text == "\u{11}" { }
369 }
370 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 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 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 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 fn resize(&mut self, width: u32, height: u32) {
502 if width == 0 || height == 0 {
503 return; }
505
506 self.config.width = width;
507 self.config.height = height;
508
509 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 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 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 fn render(&mut self, _dt: std::time::Duration) -> Result<(), wgpu::SurfaceError> {
569 let output = self.surface.get_current_texture()?;
571 let view = output
572 .texture
573 .create_view(&wgpu::TextureViewDescriptor::default());
574
575 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 let raw_input = self.egui_state.take_egui_input(&self.window);
588
589 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 let mut plot_area: Option<egui::Rect> = None;
599
600 let _ = self.plot_renderer.calculate_data_bounds();
602
603 let full_output = self.egui_ctx.run(raw_input, |ctx| {
604 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 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, fps: 60.0, };
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 let ppp_now = full_output.pixels_per_point;
655 if ppp_now > 0.0 {
656 self.pixels_per_point = ppp_now;
659 }
660 let _data_bounds = self.plot_renderer.data_bounds();
662
663 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 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 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 #[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 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 if let Some(plot_rect) = plot_area {
744 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 if let Some(plot_rect) = plot_area {
776 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 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 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 rows * cols > 1 {
857 let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::new();
858 let mut hovered_axes: Option<usize> = None;
859 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 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 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 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 {
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 self.plot_renderer
1003 .wgpu_renderer
1004 .queue
1005 .submit(std::iter::once(encoder.finish()));
1006 output.present();
1007
1008 Ok(())
1009 }
1010
1011 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 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 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 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 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 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 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}