1use crate::core::{plot_utils, PlotRenderer};
7use crate::styling::{ModernDarkTheme, PlotThemeConfig};
8use egui::{Align2, Color32, Context, FontId, Pos2, Rect, Stroke};
9
10pub struct PlotOverlay {
12 #[allow(dead_code)] theme: PlotThemeConfig,
15
16 plot_area: Option<Rect>,
18
19 show_debug: bool,
21
22 show_dystr_modal: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct OverlayConfig {
29 pub show_sidebar: bool,
31
32 pub show_grid: bool,
34
35 pub show_axes: bool,
37
38 pub show_title: bool,
40
41 pub title: Option<String>,
43
44 pub x_label: Option<String>,
46 pub y_label: Option<String>,
47
48 pub sidebar_width: f32,
50
51 pub plot_margins: PlotMargins,
53}
54
55#[derive(Debug, Clone)]
56pub struct PlotMargins {
57 pub left: f32,
58 pub right: f32,
59 pub top: f32,
60 pub bottom: f32,
61}
62
63impl Default for OverlayConfig {
64 fn default() -> Self {
65 Self {
66 show_sidebar: true,
67 show_grid: true,
68 show_axes: true,
69 show_title: true,
70 title: Some("Plot".to_string()),
71 x_label: Some("X".to_string()),
72 y_label: Some("Y".to_string()),
73 sidebar_width: 280.0,
74 plot_margins: PlotMargins {
75 left: 60.0,
76 right: 20.0,
77 top: 40.0,
78 bottom: 60.0,
79 },
80 }
81 }
82}
83
84#[derive(Debug)]
86pub struct FrameInfo {
87 pub plot_area: Option<Rect>,
89
90 pub consumed_input: bool,
92
93 pub metrics: OverlayMetrics,
95}
96
97#[derive(Debug, Default)]
98pub struct OverlayMetrics {
99 pub vertex_count: usize,
100 pub triangle_count: usize,
101 pub render_time_ms: f64,
102 pub fps: f32,
103}
104
105impl Default for PlotOverlay {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl PlotOverlay {
112 pub fn new() -> Self {
114 Self {
115 theme: PlotThemeConfig::default(),
116 plot_area: None,
117 show_debug: false,
118 show_dystr_modal: false,
119 }
120 }
121
122 pub fn apply_theme(&self, ctx: &Context) {
124 let theme = ModernDarkTheme::default();
126 theme.apply_to_egui(ctx);
127
128 let mut visuals = ctx.style().visuals.clone();
130 visuals.window_fill = Color32::TRANSPARENT;
131 visuals.panel_fill = Color32::TRANSPARENT;
132 ctx.set_visuals(visuals);
133 }
134
135 pub fn render(
137 &mut self,
138 ctx: &Context,
139 plot_renderer: &PlotRenderer,
140 config: &OverlayConfig,
141 metrics: OverlayMetrics,
142 ) -> FrameInfo {
143 let mut consumed_input = false;
144 let mut plot_area = None;
145
146 if config.show_sidebar {
148 consumed_input |= self.render_sidebar(ctx, plot_renderer, config, &metrics);
149 }
150
151 let central_response = egui::CentralPanel::default()
153 .frame(egui::Frame::none()) .show(ctx, |ui| {
155 plot_area = Some(self.render_plot_area(ui, plot_renderer, config));
156 });
157
158 consumed_input |= central_response.response.hovered();
159
160 if self.show_dystr_modal {
162 consumed_input |= self.render_dystr_modal(ctx);
163 }
164
165 self.plot_area = plot_area;
167
168 FrameInfo {
169 plot_area,
170 consumed_input,
171 metrics,
172 }
173 }
174
175 fn render_sidebar(
177 &mut self,
178 ctx: &Context,
179 plot_renderer: &PlotRenderer,
180 config: &OverlayConfig,
181 metrics: &OverlayMetrics,
182 ) -> bool {
183 let mut consumed_input = false;
184
185 let sidebar_response = egui::SidePanel::left("plot_controls")
186 .resizable(true)
187 .default_width(config.sidebar_width)
188 .min_width(200.0)
189 .show(ctx, |ui| {
190 ui.style_mut().visuals.widgets.noninteractive.bg_fill = Color32::from_gray(25);
191 ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::from_gray(35);
192 ui.style_mut().visuals.widgets.hovered.bg_fill = Color32::from_gray(45);
193
194 ui.horizontal(|ui| {
196 let logo_size = egui::Vec2::splat(32.0);
198 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::click()).0;
199
200 ui.painter().rect_filled(
202 logo_rect,
203 4.0, Color32::from_rgb(100, 100, 100),
205 );
206
207 ui.painter().text(
209 logo_rect.center(),
210 Align2::CENTER_CENTER,
211 "D",
212 FontId::proportional(20.0),
213 Color32::WHITE,
214 );
215
216 ui.vertical(|ui| {
217 ui.heading("RunMat");
218 ui.horizontal(|ui| {
219 ui.small("a community project by ");
220 if ui.small_button("dystr.com").clicked() {
221 self.show_dystr_modal = true;
222 }
223 });
224 });
225 });
226 ui.separator();
227 ui.label("GC Stats: [not available]");
228
229 ui.collapsing("📷 Camera", |ui| {
231 let camera = plot_renderer.camera();
232 ui.label(format!(
233 "Position: {:.2}, {:.2}, {:.2}",
234 camera.position.x, camera.position.y, camera.position.z
235 ));
236 ui.label(format!(
237 "Target: {:.2}, {:.2}, {:.2}",
238 camera.target.x, camera.target.y, camera.target.z
239 ));
240
241 if let Some(bounds) = plot_renderer.data_bounds() {
242 ui.label(format!("Data X: {:.2} to {:.2}", bounds.0, bounds.1));
243 ui.label(format!("Data Y: {:.2} to {:.2}", bounds.2, bounds.3));
244 }
245 });
246
247 ui.collapsing("🎬 Scene", |ui| {
249 let stats = plot_renderer.scene_statistics();
250 ui.label(format!("Nodes: {}", stats.total_nodes));
251 ui.label(format!("Visible: {}", stats.visible_nodes));
252 ui.label(format!("Vertices: {}", stats.total_vertices));
253 ui.label(format!("Triangles: {}", stats.total_triangles));
254 });
255
256 ui.collapsing("⚡ Performance", |ui| {
258 ui.label(format!("FPS: {:.1}", metrics.fps));
259 ui.label(format!("Render: {:.2}ms", metrics.render_time_ms));
260 ui.label(format!("Vertices: {}", metrics.vertex_count));
261 ui.label(format!("Triangles: {}", metrics.triangle_count));
262 });
263
264 ui.collapsing("🎨 Theme", |ui| {
266 ui.label("Modern Dark (Active)");
267 ui.checkbox(&mut self.show_debug, "Show Debug Info");
268 });
269
270 ui.separator();
271
272 ui.collapsing("🔧 Controls", |ui| {
274 ui.label("🖱️ Left drag: Rotate");
275 ui.label("🖱️ Right drag: Pan");
276 ui.label("🖱️ Scroll: Zoom");
277 ui.label("📱 Touch: Pinch to zoom");
278 });
279 });
280
281 consumed_input |= sidebar_response.response.hovered();
282 consumed_input
283 }
284
285 fn render_plot_area(
287 &mut self,
288 ui: &mut egui::Ui,
289 plot_renderer: &PlotRenderer,
290 config: &OverlayConfig,
291 ) -> Rect {
292 let available_rect = ui.available_rect_before_wrap();
293
294 let plot_rect = Rect::from_min_size(
296 available_rect.min + egui::Vec2::new(config.plot_margins.left, config.plot_margins.top),
297 available_rect.size()
298 - egui::Vec2::new(
299 config.plot_margins.left + config.plot_margins.right,
300 config.plot_margins.top + config.plot_margins.bottom,
301 ),
302 );
303
304 let size = plot_rect.width().min(plot_rect.height());
306 let centered_plot_rect =
307 Rect::from_center_size(plot_rect.center(), egui::Vec2::splat(size));
308
309 ui.painter().rect_stroke(
311 centered_plot_rect,
312 0.0,
313 Stroke::new(1.5, Color32::from_gray(180)),
314 );
315
316 if config.show_grid {
318 self.draw_grid(ui, centered_plot_rect, plot_renderer);
319 }
320
321 if config.show_axes {
323 self.draw_axes(ui, centered_plot_rect, plot_renderer, config);
324 }
325
326 if config.show_title {
328 if let Some(title) = &config.title {
329 self.draw_title(ui, centered_plot_rect, title);
330 }
331 }
332
333 if let Some(x_label) = &config.x_label {
335 self.draw_x_label(ui, centered_plot_rect, x_label);
336 }
337 if let Some(y_label) = &config.y_label {
338 self.draw_y_label(ui, centered_plot_rect, y_label);
339 }
340
341 centered_plot_rect
342 }
343
344 fn draw_grid(&self, ui: &mut egui::Ui, plot_rect: Rect, plot_renderer: &PlotRenderer) {
346 if let Some(data_bounds) = plot_renderer.data_bounds() {
347 let grid_color_major = Color32::from_gray(80);
348 let _grid_color_minor = Color32::from_gray(60);
349
350 let (x_min, x_max, y_min, y_max) = data_bounds;
351 let x_range = x_max - x_min;
352 let y_range = y_max - y_min;
353
354 let x_tick_interval = plot_utils::calculate_tick_interval(x_range);
356 let y_tick_interval = plot_utils::calculate_tick_interval(y_range);
357
358 let mut x_val = (x_min / x_tick_interval).ceil() * x_tick_interval;
360 while x_val <= x_max {
361 let x_screen =
362 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
363 ui.painter().line_segment(
364 [
365 Pos2::new(x_screen, plot_rect.min.y),
366 Pos2::new(x_screen, plot_rect.max.y),
367 ],
368 Stroke::new(0.8, grid_color_major),
369 );
370 x_val += x_tick_interval;
371 }
372
373 let mut y_val = (y_min / y_tick_interval).ceil() * y_tick_interval;
375 while y_val <= y_max {
376 let y_screen =
377 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
378 ui.painter().line_segment(
379 [
380 Pos2::new(plot_rect.min.x, y_screen),
381 Pos2::new(plot_rect.max.x, y_screen),
382 ],
383 Stroke::new(0.8, grid_color_major),
384 );
385 y_val += y_tick_interval;
386 }
387 }
388 }
389
390 fn draw_axes(
392 &self,
393 ui: &mut egui::Ui,
394 plot_rect: Rect,
395 plot_renderer: &PlotRenderer,
396 _config: &OverlayConfig,
397 ) {
398 if let Some(data_bounds) = plot_renderer.data_bounds() {
399 let (x_min, x_max, y_min, y_max) = data_bounds;
400 let x_range = x_max - x_min;
401 let y_range = y_max - y_min;
402 let tick_length = 6.0;
403 let label_offset = 15.0;
404
405 let x_tick_interval = plot_utils::calculate_tick_interval(x_range);
407 let y_tick_interval = plot_utils::calculate_tick_interval(y_range);
408
409 let mut x_val = (x_min / x_tick_interval).ceil() * x_tick_interval;
411 while x_val <= x_max {
412 let x_screen =
413 plot_rect.min.x + ((x_val - x_min) / x_range) as f32 * plot_rect.width();
414
415 ui.painter().line_segment(
417 [
418 Pos2::new(x_screen, plot_rect.max.y),
419 Pos2::new(x_screen, plot_rect.max.y + tick_length),
420 ],
421 Stroke::new(1.0, Color32::WHITE),
422 );
423
424 ui.painter().text(
426 Pos2::new(x_screen, plot_rect.max.y + label_offset),
427 Align2::CENTER_CENTER,
428 plot_utils::format_tick_label(x_val),
429 FontId::proportional(10.0),
430 Color32::from_gray(200),
431 );
432
433 x_val += x_tick_interval;
434 }
435
436 let mut y_val = (y_min / y_tick_interval).ceil() * y_tick_interval;
438 while y_val <= y_max {
439 let y_screen =
440 plot_rect.max.y - ((y_val - y_min) / y_range) as f32 * plot_rect.height();
441
442 ui.painter().line_segment(
444 [
445 Pos2::new(plot_rect.min.x - tick_length, y_screen),
446 Pos2::new(plot_rect.min.x, y_screen),
447 ],
448 Stroke::new(1.0, Color32::WHITE),
449 );
450
451 ui.painter().text(
453 Pos2::new(plot_rect.min.x - label_offset, y_screen),
454 Align2::CENTER_CENTER,
455 plot_utils::format_tick_label(y_val),
456 FontId::proportional(10.0),
457 Color32::from_gray(200),
458 );
459
460 y_val += y_tick_interval;
461 }
462 }
463 }
464
465 fn draw_title(&self, ui: &mut egui::Ui, plot_rect: Rect, title: &str) {
467 ui.painter().text(
468 Pos2::new(plot_rect.center().x, plot_rect.min.y - 20.0),
469 Align2::CENTER_CENTER,
470 title,
471 FontId::proportional(16.0),
472 Color32::WHITE,
473 );
474 }
475
476 fn draw_x_label(&self, ui: &mut egui::Ui, plot_rect: Rect, label: &str) {
478 ui.painter().text(
479 Pos2::new(plot_rect.center().x, plot_rect.max.y + 40.0),
480 Align2::CENTER_CENTER,
481 label,
482 FontId::proportional(14.0),
483 Color32::WHITE,
484 );
485 }
486
487 fn draw_y_label(&self, ui: &mut egui::Ui, plot_rect: Rect, label: &str) {
489 ui.painter().text(
490 Pos2::new(plot_rect.min.x - 40.0, plot_rect.center().y),
491 Align2::CENTER_CENTER,
492 label,
493 FontId::proportional(14.0),
494 Color32::WHITE,
495 );
496 }
497
498 pub fn plot_area(&self) -> Option<Rect> {
500 self.plot_area
501 }
502
503 fn render_dystr_modal(&mut self, ctx: &Context) -> bool {
505 let mut consumed_input = false;
506
507 egui::Window::new("About Dystr")
508 .anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
509 .collapsible(false)
510 .resizable(false)
511 .default_width(400.0)
512 .show(ctx, |ui| {
513 consumed_input = true;
514
515 ui.vertical_centered(|ui| {
516 ui.add_space(10.0);
517
518 let logo_size = egui::Vec2::splat(64.0);
520 let logo_rect = ui.allocate_exact_size(logo_size, egui::Sense::hover()).0;
521
522 ui.painter().rect_filled(
523 logo_rect,
524 8.0, Color32::from_rgb(60, 130, 200), );
527
528 ui.painter().text(
529 logo_rect.center(),
530 Align2::CENTER_CENTER,
531 "D",
532 FontId::proportional(40.0),
533 Color32::WHITE,
534 );
535
536 ui.add_space(15.0);
537
538 ui.heading("Welcome to RunMat");
539 ui.add_space(10.0);
540
541 ui.label("RunMat is a high-performance MATLAB-compatible");
542 ui.label("numerical computing platform, built as part of");
543 ui.label("the Dystr computation ecosystem.");
544
545 ui.add_space(15.0);
546
547 ui.label("🚀 V8-inspired JIT compilation");
548 ui.label("⚡ BLAS/LAPACK acceleration");
549 ui.label("🎯 Full MATLAB compatibility");
550 ui.label("🔬 Advanced plotting & visualization");
551
552 ui.add_space(20.0);
553
554 ui.horizontal(|ui| {
555 if ui.button("Visit dystr.com").clicked() {
556 if let Err(e) = webbrowser::open("https://dystr.com") {
558 eprintln!("Failed to open browser: {e}");
559 }
560 }
561
562 if ui.button("Close").clicked() {
563 self.show_dystr_modal = false;
564 }
565 });
566
567 ui.add_space(10.0);
568 });
569 });
570
571 consumed_input
572 }
573}