vegravis/
app.rs

1use crate::any_data::AnyData;
2use crate::common_vec_op::{CodeParser, CommonVecVisualizer, VecLineGen};
3use crate::cus_component::{toggle, CodeEditor};
4use crate::interfaces::{
5    ICodeEditor, IParser, IVisData, IVisDataGenerator, IVisualizer, ParseError,
6};
7use bincode::{Decode, Encode};
8use eframe::{egui, Storage};
9use log::error;
10use std::collections::{BTreeMap, BTreeSet};
11use std::time::Duration;
12use std::vec;
13
14use super::sample_codes_list::SAMPLE_CODES_LIST;
15use crate::egui::Sense;
16use base64::prelude::*;
17
18const WINDOW_NAMES: [[&str; 2]; 6] = [
19    ["๐Ÿ‘", "Samples"],
20    ["", ""],
21    ["โš™", "Options"],
22    ["๐Ÿ”ข", "Transform"],
23    ["๐Ÿ“„", "Code"],
24    ["โ„น", "About"],
25];
26
27struct MainAppCache {
28    code: AnyData,
29    lines: Vec<Vec<Box<dyn IVisData>>>,
30
31    params: MainAppParams,
32
33    #[cfg(target_arch = "wasm32")]
34    transfer_data: TransferData,
35}
36
37#[derive(Clone, PartialEq, Decode, Encode)]
38struct MainAppParams {
39    vis_progress_anim: bool,
40    /// true: positive, false: negative
41    vis_progress_anim_dir: bool,
42    vis_progress: i64,
43    vis_progress_max: i64,
44    lcd_coords: bool,
45    show_inter_dash: bool,
46    colorful_block: bool,
47
48    trans_matrix: [[f64; 3]; 3],
49}
50
51#[derive(Clone, PartialEq, Default, Decode, Encode)]
52struct TransferData {
53    code: String,
54    params: Option<MainAppParams>,
55}
56
57impl Default for MainAppParams {
58    fn default() -> Self {
59        Self {
60            vis_progress_anim: false,
61            vis_progress_anim_dir: true,
62            vis_progress: 0,
63            vis_progress_max: 0,
64            lcd_coords: false,
65            show_inter_dash: true,
66            colorful_block: true,
67            trans_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], // Identity matrix
68        }
69    }
70}
71
72impl Default for MainAppCache {
73    fn default() -> Self {
74        Self {
75            code: AnyData::new("".to_owned()),
76            lines: vec![],
77            params: Default::default(),
78
79            #[cfg(target_arch = "wasm32")]
80            transfer_data: Default::default(),
81        }
82    }
83}
84
85pub struct MainApp {
86    code: AnyData,
87    error: Option<ParseError>,
88
89    params: MainAppParams,
90
91    cache: MainAppCache,
92    samples_cache: BTreeMap<&'static str, MainAppCache>,
93    selected_sample: &'static str,
94    hovered_sample: &'static str,
95
96    #[cfg(target_arch = "wasm32")]
97    is_loaded_from_url: bool,
98
99    /// panel status
100    side_panel_open: bool,
101    panel_status: BTreeSet<String>,
102}
103
104impl Default for MainApp {
105    fn default() -> Self {
106        let mut app = Self {
107            code: AnyData::new(SAMPLE_CODES_LIST[0].1.to_owned()),
108            params: MainAppParams::default(),
109            cache: MainAppCache {
110                code: AnyData::new("".to_owned()),
111                lines: vec![],
112                params: MainAppParams::default(),
113
114                #[cfg(target_arch = "wasm32")]
115                transfer_data: TransferData::default(),
116            },
117            samples_cache: Default::default(),
118
119            error: None,
120
121            #[cfg(target_arch = "wasm32")]
122            is_loaded_from_url: false,
123            side_panel_open: false,
124            panel_status: Default::default(),
125            selected_sample: "",
126            hovered_sample: "",
127        };
128
129        app.side_panel_open = true;
130        app.panel_status.insert(WINDOW_NAMES[2][1].to_owned());
131
132        app
133    }
134}
135
136impl eframe::App for MainApp {
137    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
138        #[cfg(target_arch = "wasm32")]
139        if self.is_loaded_from_url == false {
140            self.load_from_url_search();
141            self.is_loaded_from_url = true;
142        }
143
144        if !self.panel_status.contains(WINDOW_NAMES[0][1]) {
145            self.selected_sample = "";
146            self.hovered_sample = "";
147        }
148
149        if self.params.vis_progress_anim {
150            ctx.request_repaint_after_secs(0.033);
151
152            self.params.vis_progress += if self.params.vis_progress_anim_dir {
153                1
154            } else {
155                -1
156            };
157
158            if self.params.vis_progress > self.params.vis_progress_max {
159                self.params.vis_progress_anim_dir = false;
160            }
161
162            if self.params.vis_progress < 0 {
163                self.params.vis_progress_anim_dir = true;
164            }
165        }
166
167        let about_resp = egui::TopBottomPanel::top("top").show(ctx, |ui| {
168            self.ui_about(ui);
169        });
170        egui::TopBottomPanel::bottom("bottom").show(ctx, |ui| {
171            self.ui_toast_bar(ui);
172        });
173        egui::SidePanel::left("Panels")
174            .resizable(false)
175            .exact_width(if self.side_panel_open { 100.0 } else { 40.0 })
176            .show(ctx, |ui| {
177                self.ui_panels(ui);
178            });
179
180        egui::SidePanel::left("Samples")
181            .resizable(false)
182            .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[0][1]), |ui| {
183                self.ui_samples_panel(ui);
184            });
185
186        egui::TopBottomPanel::bottom("SampleCodeEditor")
187            .resizable(false)
188            .exact_height(ctx.available_rect().height() / 2.0)
189            .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[0][1]), |ui| {
190                self.ui_sample_code_editor(ui);
191            });
192
193        let options_resp = egui::TopBottomPanel::top("Options").show_animated(
194            ctx,
195            self.panel_status.contains(WINDOW_NAMES[2][1]),
196            |ui| {
197                self.ui_options_panel(ui);
198            },
199        );
200
201        let mut transform_open = self.panel_status.contains(WINDOW_NAMES[3][1]);
202        egui::Window::new("Transform")
203            .title_bar(false)
204            .open(&mut transform_open)
205            .fixed_size([140.0, 120.0])
206            .anchor(
207                egui::Align2::RIGHT_TOP,
208                [
209                    0.0,
210                    about_resp.response.rect.height()
211                        + options_resp
212                            .map(|t| t.response.rect.height())
213                            .unwrap_or_default(),
214                ],
215            )
216            .show(ctx, |ui| {
217                self.ui_transform_panel(ui);
218            });
219        if !transform_open {
220            self.panel_status.remove(WINDOW_NAMES[3][1]);
221        }
222
223        if ctx.available_rect().aspect_ratio() < 1.0 {
224            egui::TopBottomPanel::bottom("CodeEditor")
225                .resizable(false)
226                .exact_height(ctx.available_rect().height() / 2.0)
227                .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[4][1]), |ui| {
228                    self.ui_code_editor(ui);
229                });
230        } else {
231            egui::SidePanel::left("CodeEditor")
232                .resizable(false)
233                .exact_width(ctx.available_rect().width() / 2.0)
234                .show_animated(ctx, self.panel_status.contains(WINDOW_NAMES[4][1]), |ui| {
235                    self.ui_code_editor(ui);
236                });
237        }
238
239        egui::CentralPanel::default().show(ctx, |ui| {
240            self.ui_visualizer(ui);
241        });
242    }
243
244    fn save(&mut self, _storage: &mut dyn Storage) {
245        #[cfg(target_arch = "wasm32")]
246        self.save_to_url_search();
247    }
248
249    fn auto_save_interval(&self) -> Duration {
250        Duration::from_millis(30)
251    }
252}
253
254impl MainApp {
255    fn ui_toast_bar(&mut self, ui: &mut egui::Ui) {
256        ui.vertical_centered(|ui| {
257            ui.horizontal(|ui| {
258                let info = self.error.as_ref().map_or_else(
259                    || "".to_owned(),
260                    |e| format!("({}, {}): Error: {}", e.cursor.row + 1, e.cursor.col, e.msg),
261                );
262                let rt = egui::RichText::new(info)
263                    .size(20.0)
264                    .color(egui::Color32::RED)
265                    .text_style(egui::TextStyle::Monospace);
266                ui.label(rt).highlight();
267            });
268        });
269    }
270
271    fn ui_panels(&mut self, ui: &mut egui::Ui) {
272        egui::ScrollArea::vertical().show(ui, |ui| {
273            ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
274                let side_panel_icon = if self.side_panel_open {
275                    "๐Ÿ‘ˆ Collapse"
276                } else {
277                    "๐Ÿ‘‰"
278                };
279                ui.toggle_value(&mut self.side_panel_open, side_panel_icon);
280                ui.separator();
281                for [icon, name] in WINDOW_NAMES {
282                    if icon.is_empty() && name.is_empty() {
283                        ui.separator();
284                        continue;
285                    }
286                    let mut is_open = self.panel_status.contains(name);
287                    ui.toggle_value(
288                        &mut is_open,
289                        if self.side_panel_open {
290                            format!("{icon} {name}")
291                        } else {
292                            icon.to_owned()
293                        },
294                    );
295                    if is_open {
296                        self.panel_status.insert(name.to_owned());
297                    } else {
298                        self.panel_status.remove(name);
299                    }
300                }
301            });
302        });
303    }
304
305    fn ui_samples_panel(&mut self, ui: &mut egui::Ui) {
306        egui::ScrollArea::vertical().show(ui, |ui| {
307            let mut hover_count = 0;
308            let plot_size = if ui.ctx().screen_rect().width() > 1000.0 {
309                250.0
310            } else {
311                ui.ctx().screen_rect().width() / 4.0
312            };
313            ui.set_width(plot_size);
314            for (name, code) in SAMPLE_CODES_LIST {
315                let selected = self.selected_sample == name;
316                egui::containers::Frame::default()
317                    .inner_margin(10.0)
318                    .outer_margin(10.0)
319                    .rounding(10.0)
320                    .show(ui, |ui| {
321                        let one_sample = ui.vertical_centered(|ui| {
322                            ui.vertical_centered(|ui| {
323                                let visualizer = CommonVecVisualizer::new([
324                                    [1.0, 0.0, 0.0],
325                                    [0.0, 1.0, 0.0],
326                                    [0.0, 0.0, 1.0],
327                                ]);
328
329                                if !self.samples_cache.contains_key(name) {
330                                    self.samples_cache.insert(name, Default::default());
331
332                                    let v = self.samples_cache.get_mut(name).unwrap();
333
334                                    let mut generator = VecLineGen::default();
335                                    let mut parser = CodeParser::new(
336                                        AnyData::new(code.to_owned()),
337                                        &mut generator,
338                                    );
339                                    let vlg = parser.parse().unwrap_or_else(|e| {
340                                        error!("Error: {:?}", e);
341                                        unreachable!("The sample code can't go wrong.");
342                                    });
343                                    let lines = vlg.gen(0..vlg.len() as i64);
344                                    v.lines = lines;
345                                }
346
347                                let v = self.samples_cache.get(name).unwrap();
348
349                                visualizer.plot(
350                                    ui,
351                                    v.lines.clone(),
352                                    false,
353                                    true,
354                                    true,
355                                    false,
356                                    |plot| {
357                                        plot.show_axes([false, false])
358                                            .id(egui::Id::from(name))
359                                            .width(plot_size)
360                                            .height(plot_size)
361                                            .allow_scroll([false, false])
362                                            .allow_drag([false, false])
363                                            .allow_zoom([false, false])
364                                            .show_x(false)
365                                            .show_y(false)
366                                    },
367                                );
368                                ui.add(egui::Label::new(name).truncate());
369                            })
370                        });
371
372                        let response = one_sample.response;
373
374                        let visuals = ui.style().interact_selectable(&response, selected);
375
376                        let rect = response.rect;
377                        let response = ui.allocate_rect(rect, Sense::click());
378                        if response.clicked() {
379                            if selected {
380                                self.selected_sample = ""
381                            } else {
382                                self.selected_sample = name;
383                            }
384                        }
385                        if response.hovered() {
386                            self.hovered_sample = name;
387                            hover_count += 1;
388                        }
389
390                        if selected
391                            || response.hovered()
392                            || response.highlighted()
393                            || response.has_focus()
394                        {
395                            let rect = rect.expand(10.0);
396                            let mut painter = ui.painter_at(rect);
397                            let rect = rect.expand(-2.0);
398                            painter.rect(
399                                rect,
400                                10.0,
401                                egui::Color32::TRANSPARENT,
402                                egui::Stroke::new(2.0, ui.style().visuals.hyperlink_color),
403                            );
404                            painter.set_opacity(0.3);
405                            painter.rect(rect, 10.0, visuals.text_color(), egui::Stroke::NONE);
406                        }
407                    });
408            }
409            if hover_count == 0 {
410                self.hovered_sample = "";
411            }
412            ui.shrink_width_to_current();
413        });
414    }
415
416    fn ui_sample_code_editor(&mut self, ui: &mut egui::Ui) {
417        if self.selected_sample.is_empty() && self.hovered_sample.is_empty() {
418            return;
419        }
420
421        let sample_to_be_chosen = if !self.hovered_sample.is_empty() {
422            self.hovered_sample
423        } else {
424            self.selected_sample
425        };
426
427        let mut sample_code = AnyData::new(
428            SAMPLE_CODES_LIST
429                .iter()
430                .find(|x| x.0 == sample_to_be_chosen)
431                .unwrap()
432                .1
433                .to_owned(),
434        );
435
436        ui.heading(format!("Sample: {}", sample_to_be_chosen));
437        ui.separator();
438        ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
439            if ui.button("๐Ÿ‘† Replaced With THIS ๐Ÿ‘‡").clicked() {
440                self.code = sample_code.clone::<String>();
441                self.panel_status.remove(WINDOW_NAMES[0][1]);
442            }
443            ui.shrink_height_to_current();
444
445            ui.separator();
446
447            if ui.button("๐Ÿ“‹ Copy Code").clicked() {
448                ui.output_mut(|o| o.copied_text = sample_code.cast_ref::<String>().clone());
449            }
450            if ui.button("๐ŸŒ Copy URL").clicked() {
451                let transfer_data = TransferData {
452                    code: sample_code.cast_ref::<String>().clone(),
453                    params: Some(self.params.clone()),
454                };
455                let t = self.create_transfer_url(&transfer_data);
456                ui.output_mut(|o| o.copied_text = format!("https://w-mai.github.io/vegravis/{t}"));
457            }
458        });
459
460        ui.separator();
461
462        CodeEditor {}.show(ui, &mut sample_code, VecLineGen::default().command_syntax());
463    }
464
465    fn ui_options_panel(&mut self, ui: &mut egui::Ui) {
466        ui.horizontal_wrapped(|ui| {
467            ui.allocate_ui_with_layout(
468                egui::Vec2::new(200.0, 20.0),
469                egui::Layout::left_to_right(egui::Align::Center),
470                |ui| {
471                    let mut anim_status = self.params.vis_progress_anim;
472                    ui.toggle_value(
473                        &mut anim_status,
474                        if self.params.vis_progress_anim {
475                            "โธ"
476                        } else {
477                            "โ–ถ"
478                        },
479                    );
480
481                    self.params.vis_progress_anim = anim_status;
482                    ui.add(
483                        egui::Slider::new(
484                            &mut self.params.vis_progress,
485                            0..=self.params.vis_progress_max,
486                        )
487                        .text("Progress")
488                        .show_value(true),
489                    );
490                },
491            );
492            ui.add(toggle("LCD Coordinates", &mut self.params.lcd_coords));
493            ui.add(toggle(
494                "Show Intermediate Dash",
495                &mut self.params.show_inter_dash,
496            ));
497            ui.add(toggle("Colorful Blocks", &mut self.params.colorful_block));
498        });
499    }
500
501    fn ui_transform_panel(&mut self, ui: &mut egui::Ui) {
502        ui.vertical_centered(|ui| {
503            ui.heading("Transform Matrix");
504            egui_extras::TableBuilder::new(ui)
505                .columns(egui_extras::Column::auto(), 3)
506                .body(|mut body| {
507                    for i in 0..3 {
508                        body.row(30.0, |mut row| {
509                            for j in 0..3 {
510                                row.col(|ui| {
511                                    ui.add(
512                                        egui::DragValue::new(&mut self.params.trans_matrix[i][j])
513                                            .speed(0.01),
514                                    )
515                                    .on_hover_text(format!("m_{i}{j}"));
516                                });
517                            }
518                        });
519                    }
520                });
521        });
522    }
523
524    fn ui_code_editor(&mut self, ui: &mut egui::Ui) {
525        ui.heading("Code Editor");
526
527        ui.separator();
528
529        ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
530            if ui.button("๐Ÿ“‹ Copy Code").clicked() {
531                ui.output_mut(|o| o.copied_text = self.code.cast_ref::<String>().clone());
532            }
533            ui.shrink_height_to_current();
534
535            if ui.button("๐ŸŒ Copy URL").clicked() {
536                let transfer_data = TransferData {
537                    code: self.code.cast_ref::<String>().clone(),
538                    params: Some(self.params.clone()),
539                };
540                let t = self.create_transfer_url(&transfer_data);
541                ui.output_mut(|o| o.copied_text = format!("https://w-mai.github.io/vegravis/{t}"));
542            }
543        });
544
545        ui.separator();
546
547        CodeEditor {}.show(ui, &mut self.code, VecLineGen::default().command_syntax());
548    }
549
550    fn ui_visualizer(&mut self, ui: &mut egui::Ui) {
551        if self.selected_sample.is_empty() && self.hovered_sample.is_empty() {
552            let mut has_error = false;
553            if !self.code.equal::<String, String>(&self.cache.code)
554                || self.params != self.cache.params
555            {
556                let mut generator = VecLineGen::default();
557                let mut parser = CodeParser::new(self.code.clone::<String>(), &mut generator);
558                // ้€š่ฟ‡parserไบง็”Ÿgenerator้œ€่ฆ็š„ๅ‰็ฝฎๆ•ฐๆฎ
559                has_error = match parser.parse() {
560                    Ok(vlg) => {
561                        let ops_count = vlg.len() as i64;
562                        self.params.vis_progress_max = ops_count;
563                        if !self.code.equal::<String, String>(&self.cache.code) {
564                            self.params.vis_progress = ops_count;
565                        }
566
567                        let parsed = vlg.gen(0..self.params.vis_progress);
568
569                        self.cache.lines = parsed.clone();
570                        self.cache.code = self.code.clone::<String>();
571                        self.cache.params = self.params.clone();
572                        false
573                    }
574                    Err(e) => {
575                        error!("Error: {:?}", e);
576                        self.error = Some(e);
577                        true
578                    }
579                }
580            }
581            if !has_error {
582                self.error = None;
583            }
584            CommonVecVisualizer::new(self.params.trans_matrix).plot(
585                ui,
586                self.cache.lines.clone(),
587                has_error,
588                self.params.show_inter_dash,
589                self.params.colorful_block,
590                self.params.lcd_coords,
591                |x| x,
592            );
593        } else {
594            let visualizer =
595                CommonVecVisualizer::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
596            let sample_to_be_chosen = if !self.hovered_sample.is_empty() {
597                self.hovered_sample
598            } else {
599                self.selected_sample
600            };
601            let v = self.samples_cache.get(sample_to_be_chosen).unwrap();
602
603            visualizer.plot(ui, v.lines.clone(), false, true, true, false, |plot| plot);
604        }
605    }
606
607    fn ui_about(&mut self, ui: &mut egui::Ui) {
608        const VERSION: &str = env!("CARGO_PKG_VERSION");
609        use egui::special_emojis::GITHUB;
610        ui.horizontal_wrapped(|ui| {
611            egui::widgets::global_theme_preference_switch(ui);
612            ui.separator();
613            ui.heading("Vector Graphics Visualizer");
614
615            ui.separator();
616            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
617                if ui.ctx().screen_rect().width() > 400.0
618                    || self.panel_status.contains(WINDOW_NAMES[5][1])
619                {
620                    ui.horizontal_wrapped(|ui| {
621                        ui.label(format!("Version: {VERSION}"));
622                        ui.hyperlink_to("๐ŸŒWeb Version", "https://w-mai.github.io/vegravis/");
623                        ui.hyperlink_to(
624                            format!("{GITHUB} vegravis on GitHub"),
625                            env!("CARGO_PKG_HOMEPAGE"),
626                        );
627                    });
628                } else if ui.add(egui::Button::new("โ„น")).clicked() {
629                    self.panel_status.insert(WINDOW_NAMES[5][1].to_owned());
630                }
631            });
632        });
633    }
634}
635
636impl MainApp {
637    fn create_transfer_url(&self, transfer_data: &TransferData) -> String {
638        let config = bincode::config::standard();
639        if let Ok(data) = bincode::encode_to_vec(transfer_data, config) {
640            let mut t = BASE64_URL_SAFE_NO_PAD.encode(data);
641
642            t.insert(0, '?');
643            return t;
644        };
645        Default::default()
646    }
647}
648
649#[cfg(target_arch = "wasm32")]
650impl MainApp {
651    fn load_from_url_search(&mut self) {
652        use eframe::web::web_location;
653        let location = web_location();
654        let query = &location.query;
655        if query.is_empty() {
656            return;
657        }
658
659        if let Ok(data) = BASE64_URL_SAFE_NO_PAD.decode(query) {
660            let config = bincode::config::standard();
661            if let Ok((t, _s)) =
662                bincode::decode_from_slice(&data, config) as Result<(TransferData, _), _>
663            {
664                self.code = AnyData::new(t.code);
665                self.params = t.params.unwrap_or_default();
666
667                return;
668            }
669        }
670
671        error!("Invalid query string");
672    }
673
674    fn save_to_url_search(&mut self) {
675        let history = web_sys::window().unwrap().history().unwrap();
676        let transfer_data = TransferData {
677            code: self.code.cast_ref::<String>().clone(),
678            params: Some(self.params.clone()),
679        };
680
681        if self.cache.transfer_data == transfer_data {
682            return;
683        }
684
685        self.cache.transfer_data = transfer_data;
686
687        let t = self.create_transfer_url(&self.cache.transfer_data);
688        if t.is_empty() {
689            return;
690        }
691
692        use eframe::wasm_bindgen::JsValue;
693        history
694            .push_state_with_url(&JsValue::NULL, "", Some(&t))
695            .unwrap()
696    }
697}